001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.guitools.controlpanel.ui;
028
029import static org.opends.messages.AdminToolMessages.*;
030
031import java.awt.Component;
032import java.awt.GridBagConstraints;
033import java.awt.Insets;
034import java.awt.Point;
035import java.awt.event.ActionEvent;
036import java.awt.event.ActionListener;
037import java.io.IOException;
038import java.io.StringReader;
039import java.util.ArrayList;
040import java.util.Collection;
041import java.util.Comparator;
042import java.util.HashSet;
043import java.util.LinkedHashSet;
044import java.util.List;
045import java.util.Set;
046import java.util.SortedSet;
047import java.util.TreeSet;
048
049import javax.swing.Box;
050import javax.swing.JCheckBox;
051import javax.swing.JLabel;
052import javax.swing.JScrollPane;
053import javax.swing.JTable;
054import javax.swing.SwingUtilities;
055import javax.swing.tree.TreePath;
056
057import org.opends.guitools.controlpanel.datamodel.BinaryValue;
058import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
059import org.opends.guitools.controlpanel.datamodel.ObjectClassValue;
060import org.opends.guitools.controlpanel.datamodel.SortableTableModel;
061import org.opends.guitools.controlpanel.task.OnlineUpdateException;
062import org.opends.guitools.controlpanel.ui.renderer.AttributeCellEditor;
063import org.opends.guitools.controlpanel.ui.renderer.LDAPEntryTableCellRenderer;
064import org.opends.guitools.controlpanel.util.Utilities;
065import org.forgerock.i18n.LocalizableMessage;
066import org.forgerock.opendj.ldap.ByteString;
067import org.opends.server.types.*;
068import org.opends.server.util.LDIFReader;
069import org.opends.server.util.ServerConstants;
070
071/**
072 * The panel displaying a table view of an LDAP entry.
073 */
074public class TableViewEntryPanel extends ViewEntryPanel
075{
076  private static final long serialVersionUID = 2135331526526472175L;
077  private CustomSearchResult searchResult;
078  private LDAPEntryTableModel tableModel;
079  private LDAPEntryTableCellRenderer renderer;
080  private JTable table;
081  private boolean isReadOnly;
082  private TreePath treePath;
083  private JScrollPane scroll;
084  private AttributeCellEditor editor;
085  private JLabel requiredLabel;
086  private JCheckBox showOnlyAttrsWithValues;
087
088  /**
089   * Default constructor.
090   *
091   */
092  public TableViewEntryPanel()
093  {
094    super();
095    createLayout();
096  }
097
098  /** {@inheritDoc} */
099  public Component getPreferredFocusComponent()
100  {
101    return table;
102  }
103
104  /**
105   * Creates the layout of the panel (but the contents are not populated here).
106   */
107  private void createLayout()
108  {
109    GridBagConstraints gbc = new GridBagConstraints();
110    gbc.gridx = 0;
111    gbc.gridy = 0;
112    gbc.gridwidth = 2;
113    gbc.fill = GridBagConstraints.NONE;
114    gbc.anchor = GridBagConstraints.WEST;
115    gbc.weightx = 1.0;
116
117    addTitlePanel(this, gbc);
118
119    gbc.gridy ++;
120    gbc.insets.top = 5;
121    gbc.gridwidth = 1;
122    showOnlyAttrsWithValues = Utilities.createCheckBox(
123        INFO_CTRL_PANEL_SHOW_ATTRS_WITH_VALUES_LABEL.get());
124    showOnlyAttrsWithValues.setSelected(displayOnlyWithAttrs);
125    showOnlyAttrsWithValues.addActionListener(new ActionListener()
126    {
127       /** {@inheritDoc} */
128       public void actionPerformed(ActionEvent ev)
129       {
130         updateAttributeVisibility();
131         displayOnlyWithAttrs = showOnlyAttrsWithValues.isSelected();
132       }
133    });
134    gbc.weightx = 0.0;
135    gbc.anchor = GridBagConstraints.WEST;
136    add(showOnlyAttrsWithValues, gbc);
137
138    gbc.gridx ++;
139    gbc.anchor = GridBagConstraints.EAST;
140    gbc.fill = GridBagConstraints.NONE;
141    requiredLabel = createRequiredLabel();
142    add(requiredLabel, gbc);
143    gbc.insets = new Insets(0, 0, 0, 0);
144    add(Box.createVerticalStrut(10), gbc);
145
146    showOnlyAttrsWithValues.setFont(requiredLabel.getFont());
147
148    gbc.gridy ++;
149    gbc.gridx = 0;
150    gbc.insets.top = 10;
151    gbc.gridwidth = 2;
152    tableModel = new LDAPEntryTableModel();
153    renderer = new LDAPEntryTableCellRenderer();
154    table = Utilities.createSortableTable(tableModel, renderer);
155    renderer.setTable(table);
156    editor = new AttributeCellEditor();
157    table.getColumnModel().getColumn(1).setCellEditor(editor);
158    gbc.weighty = 1.0;
159    gbc.fill = GridBagConstraints.BOTH;
160    gbc.gridy ++;
161    scroll = Utilities.createScrollPane(table);
162    add(scroll, gbc);
163  }
164
165  /** {@inheritDoc} */
166  public void update(CustomSearchResult sr, boolean isReadOnly, TreePath path)
167  {
168    boolean sameEntry = false;
169    if (searchResult != null && sr != null)
170    {
171      sameEntry = searchResult.getDN().equals(sr.getDN());
172    }
173
174    searchResult = sr;
175    final Point p = sameEntry ? scroll.getViewport().getViewPosition() :
176      new Point(0, 0);
177    renderer.setSchema(getInfo().getServerDescriptor().getSchema());
178    editor.setInfo(getInfo());
179    requiredLabel.setVisible(!isReadOnly);
180    this.isReadOnly = isReadOnly;
181    this.treePath = path;
182    updateTitle(sr, path);
183    ignoreEntryChangeEvents = true;
184    tableModel.displayEntry();
185    Utilities.updateTableSizes(table);
186    Utilities.updateScrollMode(scroll, table);
187    SwingUtilities.invokeLater(new Runnable()
188    {
189      public void run()
190      {
191        if (p != null && scroll.getViewport().contains(p))
192        {
193          scroll.getViewport().setViewPosition(p);
194        }
195        ignoreEntryChangeEvents = false;
196      }
197    });
198  }
199
200  /** {@inheritDoc} */
201  public GenericDialog.ButtonType getButtonType()
202  {
203    return GenericDialog.ButtonType.NO_BUTTON;
204  }
205
206  /** {@inheritDoc} */
207  public Entry getEntry() throws OpenDsException
208  {
209    if (SwingUtilities.isEventDispatchThread())
210    {
211      editor.stopCellEditing();
212    }
213    else
214    {
215      try
216      {
217        SwingUtilities.invokeAndWait(new Runnable()
218        {
219          public void run()
220          {
221            editor.stopCellEditing();
222          }
223        });
224      }
225      catch (Throwable t)
226      {
227      }
228    }
229    Entry entry = null;
230    LDIFImportConfig ldifImportConfig = null;
231    try
232    {
233      String ldif = getLDIF();
234
235      ldifImportConfig = new LDIFImportConfig(new StringReader(ldif));
236      LDIFReader reader = new LDIFReader(ldifImportConfig);
237      entry = reader.readEntry(checkSchema());
238      addValuesInRDN(entry);
239
240    }
241    catch (IOException ioe)
242    {
243      throw new OnlineUpdateException(
244          ERR_CTRL_PANEL_ERROR_CHECKING_ENTRY.get(ioe), ioe);
245    }
246    finally
247    {
248      if (ldifImportConfig != null)
249      {
250        ldifImportConfig.close();
251      }
252    }
253    return entry;
254  }
255
256  /**
257   * Returns the LDIF representation of the displayed entry.
258   * @return the LDIF representation of the displayed entry.
259   */
260  private String getLDIF()
261  {
262    StringBuilder sb = new StringBuilder();
263    sb.append("dn: ").append(getDisplayedDN());
264    for (int i=0; i<tableModel.getRowCount(); i++)
265    {
266      String attrName = (String)tableModel.getValueAt(i, 0);
267      if (!schemaReadOnlyAttributesLowerCase.contains(attrName.toLowerCase()))
268      {
269        Object value = tableModel.getValueAt(i, 1);
270        appendLDIFLine(sb, attrName, value);
271      }
272    }
273    return sb.toString();
274  }
275
276  /** {@inheritDoc} */
277  protected String getDisplayedDN()
278  {
279    StringBuilder sb = new StringBuilder();
280    try
281    {
282      DN oldDN = DN.valueOf(searchResult.getDN());
283      if (oldDN.size() > 0)
284      {
285        RDN rdn = oldDN.rdn();
286        List<AttributeType> attributeTypes = new ArrayList<>();
287        List<String> attributeNames = new ArrayList<>();
288        List<ByteString> attributeValues = new ArrayList<>();
289        for (int i=0; i<rdn.getNumValues(); i++)
290        {
291          String attrName = rdn.getAttributeName(i);
292          ByteString value = rdn.getAttributeValue(i);
293
294          Set<String> values = getDisplayedStringValues(attrName);
295          if (!values.contains(value.toString()))
296          {
297            if (!values.isEmpty())
298            {
299              String firstNonEmpty = getFirstNonEmpty(values);
300              if (firstNonEmpty != null)
301              {
302                AttributeType attr = rdn.getAttributeType(i);
303                attributeTypes.add(attr);
304                attributeNames.add(rdn.getAttributeName(i));
305                attributeValues.add(ByteString.valueOfUtf8(firstNonEmpty));
306              }
307            }
308          }
309          else
310          {
311            attributeTypes.add(rdn.getAttributeType(i));
312            attributeNames.add(rdn.getAttributeName(i));
313            attributeValues.add(value);
314          }
315        }
316        if (attributeTypes.isEmpty())
317        {
318          // Check the attributes in the order that we display them and use
319          // the first one.
320          Schema schema = getInfo().getServerDescriptor().getSchema();
321          if (schema != null)
322          {
323            for (int i=0; i<table.getRowCount(); i++)
324            {
325              String attrName = (String)table.getValueAt(i, 0);
326              if (isPassword(attrName) ||
327                  attrName.equals(
328                      ServerConstants.OBJECTCLASS_ATTRIBUTE_TYPE_NAME) ||
329                  !table.isCellEditable(i, 1))
330              {
331                continue;
332              }
333              Object o = table.getValueAt(i, 1);
334              if (o instanceof String)
335              {
336                String aName =
337                  Utilities.getAttributeNameWithoutOptions(attrName);
338                AttributeType attr =
339                  schema.getAttributeType(aName.toLowerCase());
340                if (attr != null)
341                {
342                  attributeTypes.add(attr);
343                  attributeNames.add(attrName);
344                  attributeValues.add(ByteString.valueOfUtf8((String) o));
345                }
346                break;
347              }
348            }
349          }
350        }
351        DN parent = oldDN.parent();
352        if (!attributeTypes.isEmpty())
353        {
354          RDN newRDN = new RDN(attributeTypes, attributeNames, attributeValues);
355
356          DN newDN;
357          if (parent == null)
358          {
359            newDN = new DN(new RDN[]{newRDN});
360          }
361          else
362          {
363            newDN = parent.child(newRDN);
364          }
365          sb.append(newDN);
366        }
367        else
368        {
369          if (parent != null)
370          {
371            sb.append(",").append(parent);
372          }
373        }
374      }
375    }
376    catch (Throwable t)
377    {
378      throw new RuntimeException("Unexpected error: "+t, t);
379    }
380    return sb.toString();
381  }
382
383  private String getFirstNonEmpty(Set<String> values)
384  {
385    for (String v : values)
386    {
387      v = v.trim();
388      if (v.length() > 0)
389      {
390        return v;
391      }
392    }
393    return null;
394  }
395
396  private Set<String> getDisplayedStringValues(String attrName)
397  {
398    Set<String> values = new LinkedHashSet<>();
399    for (int i=0; i<table.getRowCount(); i++)
400    {
401      if (attrName.equalsIgnoreCase((String)table.getValueAt(i, 0)))
402      {
403        Object o = table.getValueAt(i, 1);
404        if (o instanceof String)
405        {
406          values.add((String)o);
407        }
408      }
409    }
410    return values;
411  }
412
413  private void updateAttributeVisibility()
414  {
415    tableModel.updateAttributeVisibility();
416  }
417
418  /** {@inheritDoc} */
419  protected List<Object> getValues(String attrName)
420  {
421    return tableModel.getValues(attrName);
422  }
423
424  /** The table model used by the tree in the panel. */
425  protected class LDAPEntryTableModel extends SortableTableModel
426  implements Comparator<AttributeValuePair>
427  {
428    private static final long serialVersionUID = -1240282431326505113L;
429    private ArrayList<AttributeValuePair> dataArray = new ArrayList<>();
430    private SortedSet<AttributeValuePair> allSortedValues = new TreeSet<>(this);
431    private Set<String> requiredAttrs = new HashSet<>();
432    private final String[] COLUMN_NAMES = new String[] {
433        getHeader(LocalizableMessage.raw("Attribute"), 40),
434        getHeader(LocalizableMessage.raw("Value", 40))};
435    private int sortColumn;
436    private boolean sortAscending = true;
437
438    /**
439     * Updates the contents of the table model with the
440     * {@code TableViewEntryPanel.searchResult} object.
441     */
442    public void displayEntry()
443    {
444      updateDataArray();
445      fireTableDataChanged();
446    }
447
448    /**
449     * Updates the table model contents and sorts its contents depending on the
450     * sort options set by the user.
451     */
452    public void forceResort()
453    {
454      updateDataArray();
455      fireTableDataChanged();
456    }
457
458    /** {@inheritDoc} */
459    public int compare(AttributeValuePair desc1, AttributeValuePair desc2)
460    {
461      int result;
462      int[] possibleResults = {
463          desc1.attrName.compareTo(desc2.attrName),
464          compareValues(desc1.value, desc2.value)};
465      result = possibleResults[sortColumn];
466      if (result == 0)
467      {
468        for (int i : possibleResults)
469        {
470          if (i != 0)
471          {
472            result = i;
473            break;
474          }
475        }
476      }
477      if (!sortAscending)
478      {
479        result = -result;
480      }
481      return result;
482    }
483
484    private int compareValues(Object o1, Object o2)
485    {
486      if (o1 == null)
487      {
488        if (o2 == null)
489        {
490          return 0;
491        }
492        else
493        {
494          return -1;
495        }
496      }
497      else if (o2 == null)
498      {
499        return 1;
500      }
501      if (o1 instanceof ObjectClassValue)
502      {
503        o1 = renderer.getString((ObjectClassValue)o1);
504      }
505      else if (o1 instanceof BinaryValue)
506      {
507        o1 = renderer.getString((BinaryValue)o1);
508      }
509      else if (o1 instanceof byte[])
510      {
511        o1 = renderer.getString((byte[])o1);
512      }
513      if (o2 instanceof ObjectClassValue)
514      {
515        o2 = renderer.getString((ObjectClassValue)o2);
516      }
517      else if (o2 instanceof BinaryValue)
518      {
519        o2 = renderer.getString((BinaryValue)o2);
520      }
521      else if (o2 instanceof byte[])
522      {
523        o2 = renderer.getString((byte[])o2);
524      }
525      if (o1.getClass().equals(o2.getClass()))
526      {
527        if (o1 instanceof String)
528        {
529          return ((String)o1).compareTo((String)o2);
530        }
531        else if (o1 instanceof Integer)
532        {
533          return ((Integer)o1).compareTo((Integer)o2);
534        }
535        else if (o1 instanceof Long)
536        {
537          return ((Long)o1).compareTo((Long)o2);
538        }
539        else
540        {
541          return String.valueOf(o1).compareTo(String.valueOf(o2));
542        }
543      }
544      else
545      {
546        return String.valueOf(o1).compareTo(String.valueOf(o2));
547      }
548    }
549
550    /** {@inheritDoc} */
551    public int getColumnCount()
552    {
553      return COLUMN_NAMES.length;
554    }
555
556    /** {@inheritDoc} */
557    public int getRowCount()
558    {
559      return dataArray.size();
560    }
561
562    /** {@inheritDoc} */
563    public Object getValueAt(int row, int col)
564    {
565      if (col == 0)
566      {
567        return dataArray.get(row).attrName;
568      }
569      else
570      {
571        return dataArray.get(row).value;
572      }
573    }
574
575    /** {@inheritDoc} */
576    public String getColumnName(int col) {
577      return COLUMN_NAMES[col];
578    }
579
580
581    /**
582     * Returns whether the sort is ascending or descending.
583     * @return <CODE>true</CODE> if the sort is ascending and <CODE>false</CODE>
584     * otherwise.
585     */
586    public boolean isSortAscending()
587    {
588      return sortAscending;
589    }
590
591    /**
592     * Sets whether to sort ascending of descending.
593     * @param sortAscending whether to sort ascending or descending.
594     */
595    public void setSortAscending(boolean sortAscending)
596    {
597      this.sortAscending = sortAscending;
598    }
599
600    /**
601     * Returns the column index used to sort.
602     * @return the column index used to sort.
603     */
604    public int getSortColumn()
605    {
606      return sortColumn;
607    }
608
609    /**
610     * Sets the column index used to sort.
611     * @param sortColumn column index used to sort..
612     */
613    public void setSortColumn(int sortColumn)
614    {
615      this.sortColumn = sortColumn;
616    }
617
618    /** {@inheritDoc} */
619    public boolean isCellEditable(int row, int col) {
620      return col != 0
621          && !isReadOnly
622          && !schemaReadOnlyAttributesLowerCase.contains(dataArray.get(row).attrName.toLowerCase());
623    }
624
625    /** {@inheritDoc} */
626    public void setValueAt(Object value, int row, int col)
627    {
628      dataArray.get(row).value = value;
629      if (value instanceof ObjectClassValue)
630      {
631        updateObjectClass((ObjectClassValue)value);
632      }
633      else
634      {
635        fireTableCellUpdated(row, col);
636
637        notifyListeners();
638      }
639    }
640
641    private void updateDataArray()
642    {
643      allSortedValues.clear();
644      requiredAttrs.clear();
645      List<String> addedAttrs = new ArrayList<>();
646      Schema schema = getInfo().getServerDescriptor().getSchema();
647      List<Object> ocs = null;
648      for (String attrName : searchResult.getAttributeNames())
649      {
650        if (attrName.equalsIgnoreCase(
651            ServerConstants.OBJECTCLASS_ATTRIBUTE_TYPE_NAME))
652        {
653          if (schema != null)
654          {
655            ocs = searchResult.getAttributeValues(attrName);
656            ObjectClassValue ocValue = getObjectClassDescriptor(
657                ocs, schema);
658            allSortedValues.add(new AttributeValuePair(attrName, ocValue));
659          }
660        }
661        else
662        {
663          for (Object v : searchResult.getAttributeValues(attrName))
664          {
665            allSortedValues.add(new AttributeValuePair(attrName, v));
666          }
667        }
668        addedAttrs.add(
669            Utilities.getAttributeNameWithoutOptions(attrName).toLowerCase());
670      }
671      if (ocs != null && schema != null)
672      {
673        for (Object o : ocs)
674        {
675          String oc = (String)o;
676          ObjectClass objectClass = schema.getObjectClass(oc.toLowerCase());
677          if (objectClass != null)
678          {
679            for (AttributeType attr : objectClass.getRequiredAttributeChain())
680            {
681              String attrName = attr.getNameOrOID();
682              if (!addedAttrs.contains(attrName.toLowerCase()))
683              {
684                if (isBinary(attrName) || isPassword(attrName))
685                {
686                  allSortedValues.add(new AttributeValuePair(attrName,
687                      new byte[]{}));
688                }
689                else
690                {
691                  allSortedValues.add(new AttributeValuePair(attrName, ""));
692                }
693              }
694              requiredAttrs.add(attrName.toLowerCase());
695            }
696            for (AttributeType attr : objectClass.getOptionalAttributeChain())
697            {
698              String attrName = attr.getNameOrOID();
699              if (!addedAttrs.contains(attrName.toLowerCase()))
700              {
701                if (isBinary(attrName) || isPassword(attrName))
702                {
703                  allSortedValues.add(new AttributeValuePair(attrName,
704                      new byte[]{}));
705                }
706                else
707                {
708                  allSortedValues.add(new AttributeValuePair(attrName, ""));
709                }
710              }
711            }
712          }
713        }
714      }
715      dataArray.clear();
716      for (AttributeValuePair value : allSortedValues)
717      {
718        if (!showOnlyAttrsWithValues.isSelected() ||
719            isRequired(value) || hasValue(value))
720        {
721          dataArray.add(value);
722        }
723      }
724      renderer.setRequiredAttrs(requiredAttrs);
725    }
726
727    /**
728     * Checks if we have to display all the attributes or only those that
729     * contain a value and updates the contents of the model accordingly.  Note
730     * that even if the required attributes have no value they will be
731     * displayed.
732     *
733     */
734    void updateAttributeVisibility()
735    {
736      dataArray.clear();
737      for (AttributeValuePair value : allSortedValues)
738      {
739        if (!showOnlyAttrsWithValues.isSelected() ||
740            isRequired(value) || hasValue(value))
741        {
742          dataArray.add(value);
743        }
744      }
745      fireTableDataChanged();
746
747      Utilities.updateTableSizes(table);
748      Utilities.updateScrollMode(scroll, table);
749    }
750
751    /**
752     * Returns the list of values associated with a given attribute.
753     * @param attrName the name of the attribute.
754     * @return the list of values associated with a given attribute.
755     */
756    public List<Object> getValues(String attrName)
757    {
758      List<Object> values = new ArrayList<>();
759      for (AttributeValuePair valuePair : dataArray)
760      {
761        if (valuePair.attrName.equalsIgnoreCase(attrName)
762            && hasValue(valuePair))
763        {
764          if (valuePair.value instanceof Collection<?>)
765          {
766            values.addAll((Collection<?>) valuePair.value);
767          }
768          else
769          {
770            values.add(valuePair.value);
771          }
772        }
773      }
774      return values;
775    }
776
777    private void updateObjectClass(ObjectClassValue newValue)
778    {
779      CustomSearchResult oldResult = searchResult;
780      CustomSearchResult newResult =
781        new CustomSearchResult(searchResult.getDN());
782
783      for (String attrName : schemaReadOnlyAttributesLowerCase)
784      {
785        List<Object> values = searchResult.getAttributeValues(attrName);
786        if (!values.isEmpty())
787        {
788          newResult.set(attrName, values);
789        }
790      }
791      ignoreEntryChangeEvents = true;
792
793      Schema schema = getInfo().getServerDescriptor().getSchema();
794      if (schema != null)
795      {
796        ArrayList<String> attributes = new ArrayList<>();
797        ArrayList<String> ocs = new ArrayList<>();
798        if (newValue.getStructural() != null)
799        {
800          ocs.add(newValue.getStructural().toLowerCase());
801        }
802        for (String oc : newValue.getAuxiliary())
803        {
804          ocs.add(oc.toLowerCase());
805        }
806        for (String oc : ocs)
807        {
808          ObjectClass objectClass = schema.getObjectClass(oc);
809          if (objectClass != null)
810          {
811            for (AttributeType attr : objectClass.getRequiredAttributeChain())
812            {
813              attributes.add(attr.getNameOrOID().toLowerCase());
814            }
815            for (AttributeType attr : objectClass.getOptionalAttributeChain())
816            {
817              attributes.add(attr.getNameOrOID().toLowerCase());
818            }
819          }
820        }
821        for (String attrName : editableOperationalAttrNames)
822        {
823          attributes.add(attrName.toLowerCase());
824        }
825        for (AttributeValuePair currValue : allSortedValues)
826        {
827          String attrNoOptions = Utilities.getAttributeNameWithoutOptions(
828              currValue.attrName).toLowerCase();
829          if (!attributes.contains(attrNoOptions))
830          {
831            continue;
832          }
833          else if (!schemaReadOnlyAttributesLowerCase.contains(
834              currValue.attrName.toLowerCase()))
835          {
836            setValues(newResult, currValue.attrName);
837          }
838        }
839      }
840      update(newResult, isReadOnly, treePath);
841      ignoreEntryChangeEvents = false;
842      searchResult = oldResult;
843      notifyListeners();
844    }
845
846    private boolean isRequired(AttributeValuePair value)
847    {
848      return requiredAttrs.contains(
849          Utilities.getAttributeNameWithoutOptions(
850              value.attrName.toLowerCase()));
851    }
852
853    private boolean hasValue(AttributeValuePair value)
854    {
855      boolean hasValue = value.value != null;
856      if (hasValue)
857      {
858        if (value.value instanceof String)
859        {
860          hasValue = ((String)value.value).length() > 0;
861        }
862        else if (value.value instanceof byte[])
863        {
864          hasValue = ((byte[])value.value).length > 0;
865        }
866      }
867      return hasValue;
868    }
869  }
870
871  /**
872   * A simple class that contains an attribute name and a single value.  It is
873   * used by the table model to be able to retrieve more easily all the values
874   * for a given attribute.
875   *
876   */
877  class AttributeValuePair
878  {
879    /**
880     * The attribute name.
881     */
882    String attrName;
883    /**
884     * The value.
885     */
886    Object value;
887    /**
888     * Constructor.
889     * @param attrName the attribute name.
890     * @param value the value.
891     */
892    public AttributeValuePair(String attrName, Object value)
893    {
894      this.attrName = attrName;
895      this.value = value;
896    }
897  }
898}