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