/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2014-2016 ForgeRock AS. */ package org.opends.guitools.controlpanel.ui; import static org.forgerock.opendj.ldap.schema.CoreSchema.*; import static org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.*; import static org.opends.messages.AdminToolMessages.*; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.swing.Box; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.AVA; import org.forgerock.opendj.ldap.Attribute; import org.forgerock.opendj.ldap.AttributeDescription; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.LinkedAttribute; import org.forgerock.opendj.ldap.LinkedHashMapEntry; import org.forgerock.opendj.ldap.RDN; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.ldap.schema.ObjectClass; import org.forgerock.opendj.ldif.LDIFEntryReader; import org.opends.guitools.controlpanel.datamodel.BinaryValue; import org.opends.guitools.controlpanel.datamodel.ObjectClassValue; import org.opends.guitools.controlpanel.datamodel.SortableTableModel; import org.opends.guitools.controlpanel.task.OnlineUpdateException; import org.opends.guitools.controlpanel.ui.renderer.AttributeCellEditor; import org.opends.guitools.controlpanel.ui.renderer.LDAPEntryTableCellRenderer; import org.opends.guitools.controlpanel.util.Utilities; import org.opends.server.types.OpenDsException; import org.forgerock.opendj.ldap.schema.Schema; import org.opends.server.util.ServerConstants; /** The panel displaying a table view of an LDAP entry. */ class TableViewEntryPanel extends ViewEntryPanel { private static final long serialVersionUID = 2135331526526472175L; private Entry searchResult; private LDAPEntryTableModel tableModel; private LDAPEntryTableCellRenderer renderer; private JTable table; private boolean isReadOnly; private TreePath treePath; private JScrollPane scroll; private AttributeCellEditor editor; private JLabel requiredLabel; private JCheckBox showOnlyAttrsWithValues; /** Default constructor. */ public TableViewEntryPanel() { super(); createLayout(); } @Override public Component getPreferredFocusComponent() { return table; } /** Creates the layout of the panel (but the contents are not populated here). */ private void createLayout() { GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; gbc.weightx = 1.0; addTitlePanel(this, gbc); gbc.gridy ++; gbc.insets.top = 5; gbc.gridwidth = 1; showOnlyAttrsWithValues = Utilities.createCheckBox( INFO_CTRL_PANEL_SHOW_ATTRS_WITH_VALUES_LABEL.get()); showOnlyAttrsWithValues.setSelected(displayOnlyWithAttrs); showOnlyAttrsWithValues.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ev) { updateAttributeVisibility(); displayOnlyWithAttrs = showOnlyAttrsWithValues.isSelected(); } }); gbc.weightx = 0.0; gbc.anchor = GridBagConstraints.WEST; add(showOnlyAttrsWithValues, gbc); gbc.gridx ++; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.NONE; requiredLabel = createRequiredLabel(); add(requiredLabel, gbc); gbc.insets = new Insets(0, 0, 0, 0); add(Box.createVerticalStrut(10), gbc); showOnlyAttrsWithValues.setFont(requiredLabel.getFont()); gbc.gridy ++; gbc.gridx = 0; gbc.insets.top = 10; gbc.gridwidth = 2; tableModel = new LDAPEntryTableModel(); renderer = new LDAPEntryTableCellRenderer(); table = Utilities.createSortableTable(tableModel, renderer); renderer.setTable(table); editor = new AttributeCellEditor(); table.getColumnModel().getColumn(1).setCellEditor(editor); gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH; gbc.gridy ++; scroll = Utilities.createScrollPane(table); add(scroll, gbc); } @Override public void update(Entry sr, boolean isReadOnly, TreePath path) { boolean sameEntry = false; if (searchResult != null && sr != null) { sameEntry = searchResult.getName().equals(sr.getName()); } searchResult = sr; final Point p = sameEntry ? scroll.getViewport().getViewPosition() : new Point(0, 0); renderer.setSchema(getInfo().getServerDescriptor().getSchema()); editor.setInfo(getInfo()); requiredLabel.setVisible(!isReadOnly); this.isReadOnly = isReadOnly; this.treePath = path; updateTitle(sr, path); ignoreEntryChangeEvents = true; tableModel.displayEntry(); Utilities.updateTableSizes(table); Utilities.updateScrollMode(scroll, table); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (p != null && scroll.getViewport().contains(p)) { scroll.getViewport().setViewPosition(p); } ignoreEntryChangeEvents = false; } }); } @Override public GenericDialog.ButtonType getButtonType() { return GenericDialog.ButtonType.NO_BUTTON; } @Override public Entry getEntry() throws OpenDsException { if (SwingUtilities.isEventDispatchThread()) { editor.stopCellEditing(); } else { try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { editor.stopCellEditing(); } }); } catch (Throwable ignore) { // ignored } } String ldif = getLDIF(); try (LDIFEntryReader reader = new LDIFEntryReader(ldif) .setSchemaValidationPolicy(checkSchema() ? defaultPolicy() : ignoreAll())) { Entry entry = reader.readEntry(); addValuesInRDN(entry); return entry; } catch (IOException ioe) { throw new OnlineUpdateException( ERR_CTRL_PANEL_ERROR_CHECKING_ENTRY.get(ioe), ioe); } } /** * Returns the LDIF representation of the displayed entry. * @return the LDIF representation of the displayed entry. */ private String getLDIF() { StringBuilder sb = new StringBuilder(); sb.append("dn: ").append(getDisplayedDN()); for (int i=0; i 0) { RDN rdn = oldDN.rdn(); List avas = new ArrayList<>(); for (AVA ava : rdn) { AttributeType attrType = ava.getAttributeType(); String attrName = ava.getAttributeName(); ByteString value = ava.getAttributeValue(); Set values = getDisplayedStringValues(attrName); if (!values.contains(value.toString())) { if (!values.isEmpty()) { String firstNonEmpty = getFirstNonEmpty(values); if (firstNonEmpty != null) { avas.add(new AVA(attrType, attrName, ByteString.valueOfUtf8(firstNonEmpty))); } } } else { avas.add(new AVA(attrType, attrName, value)); } } if (avas.isEmpty()) { // Check the attributes in the order that we display them and use // the first one. Schema schema = getInfo().getServerDescriptor().getSchema(); if (schema != null) { for (int i=0; i values) { for (String v : values) { v = v.trim(); if (v.length() > 0) { return v; } } return null; } private Set getDisplayedStringValues(String attrName) { Set values = new LinkedHashSet<>(); for (int i=0; i getValues(String attrName) { return tableModel.getValues(attrName); } /** The table model used by the tree in the panel. */ private class LDAPEntryTableModel extends SortableTableModel implements Comparator { private static final long serialVersionUID = -1240282431326505113L; private final List dataArray = new ArrayList<>(); private final SortedSet allSortedValues = new TreeSet<>(this); private final Set requiredAttrs = new HashSet<>(); private final String[] COLUMN_NAMES = new String[] { getHeader(LocalizableMessage.raw("Attribute"), 40), getHeader(LocalizableMessage.raw("Value", 40))}; private int sortColumn; private boolean sortAscending = true; /** * Updates the contents of the table model with the * {@code TableViewEntryPanel.searchResult} object. */ private void displayEntry() { updateDataArray(); fireTableDataChanged(); } /** * Updates the table model contents and sorts its contents depending on the * sort options set by the user. */ @Override public void forceResort() { updateDataArray(); fireTableDataChanged(); } @Override public int compare(AttributeValuePair desc1, AttributeValuePair desc2) { int result; int[] possibleResults = { desc1.attrDesc.compareTo(desc2.attrDesc), compareValues(desc1.value, desc2.value)}; result = possibleResults[sortColumn]; if (result == 0) { for (int i : possibleResults) { if (i != 0) { result = i; break; } } } if (!sortAscending) { result = -result; } return result; } private int compareValues(Object o1, Object o2) { if (o1 == null) { return o2 == null ? 0 : -1; } else if (o2 == null) { return 1; } if (o1 instanceof ObjectClassValue) { o1 = renderer.getString((ObjectClassValue)o1); } else if (o1 instanceof BinaryValue) { o1 = renderer.getString((BinaryValue)o1); } else if (o1 instanceof byte[]) { o1 = renderer.getString((byte[])o1); } if (o2 instanceof ObjectClassValue) { o2 = renderer.getString((ObjectClassValue)o2); } else if (o2 instanceof BinaryValue) { o2 = renderer.getString((BinaryValue)o2); } else if (o2 instanceof byte[]) { o2 = renderer.getString((byte[])o2); } if (o1.getClass().equals(o2.getClass())) { if (o1 instanceof String) { return ((String)o1).compareTo((String)o2); } else if (o1 instanceof Integer) { return ((Integer)o1).compareTo((Integer)o2); } else if (o1 instanceof Long) { return ((Long)o1).compareTo((Long)o2); } } return String.valueOf(o1).compareTo(String.valueOf(o2)); } @Override public int getColumnCount() { return COLUMN_NAMES.length; } @Override public int getRowCount() { return dataArray.size(); } @Override public Object getValueAt(int row, int col) { AttributeValuePair attrValuePair = dataArray.get(row); return col == 0 ? attrValuePair.attrDesc.toString() : attrValuePair.value; } @Override public String getColumnName(int col) { return COLUMN_NAMES[col]; } /** * Returns whether the sort is ascending or descending. * @return true if the sort is ascending and false * otherwise. */ @Override public boolean isSortAscending() { return sortAscending; } /** * Sets whether to sort ascending of descending. * @param sortAscending whether to sort ascending or descending. */ @Override public void setSortAscending(boolean sortAscending) { this.sortAscending = sortAscending; } /** * Returns the column index used to sort. * @return the column index used to sort. */ @Override public int getSortColumn() { return sortColumn; } /** * Sets the column index used to sort. * @param sortColumn column index used to sort.. */ @Override public void setSortColumn(int sortColumn) { this.sortColumn = sortColumn; } @Override public boolean isCellEditable(int row, int col) { return col != 0 && !isReadOnly && !schemaReadOnlyAttributesLowerCase.contains(dataArray.get(row).attrDesc.toString().toLowerCase()); } @Override public void setValueAt(Object value, int row, int col) { dataArray.get(row).value = value; if (value instanceof ObjectClassValue) { updateObjectClass((ObjectClassValue)value); } else { fireTableCellUpdated(row, col); notifyListeners(); } } private void updateDataArray() { allSortedValues.clear(); requiredAttrs.clear(); List addedAttrs = new ArrayList<>(); Schema schema = getInfo().getServerDescriptor().getSchema(); Attribute ocs = null; for (Attribute attr : searchResult.getAllAttributes()) { AttributeDescription attrDesc = attr.getAttributeDescription(); if (attrDesc.equals(getObjectClassAttributeType())) { if (schema != null) { ocs = attr; ObjectClassValue ocValue = getObjectClassDescriptor(ocs, schema); allSortedValues.add(new AttributeValuePair(attrDesc, ocValue)); } } else { for (Object v : attr) { allSortedValues.add(new AttributeValuePair(attrDesc, v)); } } addedAttrs.add(attrDesc.getNameOrOID().toLowerCase()); } if (ocs != null && schema != null) { for (ByteString oc : ocs) { ObjectClass objectClass = schema.getObjectClass(oc.toString()); if (!objectClass.isPlaceHolder()) { for (AttributeType attrType : objectClass.getRequiredAttributes()) { String lowerCase = attrType.getNameOrOID().toLowerCase(); if (!addedAttrs.contains(lowerCase)) { allSortedValues.add(newAttributeValuePair(AttributeDescription.create(attrType))); } requiredAttrs.add(lowerCase); } for (AttributeType attrType : objectClass.getOptionalAttributes()) { if (!addedAttrs.contains(attrType.getNameOrOID().toLowerCase())) { allSortedValues.add(newAttributeValuePair(AttributeDescription.create(attrType))); } } } } } dataArray.clear(); for (AttributeValuePair value : allSortedValues) { if (!showOnlyAttrsWithValues.isSelected() || isRequired(value) || hasValue(value)) { dataArray.add(value); } } renderer.setRequiredAttrs(requiredAttrs); } private AttributeValuePair newAttributeValuePair(AttributeDescription attrDesc) { String attrName = attrDesc.toString(); if (isBinary(attrName) || isPassword(attrName)) { return new AttributeValuePair(attrDesc, new byte[] {}); } else { return new AttributeValuePair(attrDesc, ""); } } /** * Checks if we have to display all the attributes or only those that * contain a value and updates the contents of the model accordingly. Note * that even if the required attributes have no value they will be * displayed. */ void updateAttributeVisibility() { dataArray.clear(); for (AttributeValuePair value : allSortedValues) { if (!showOnlyAttrsWithValues.isSelected() || isRequired(value) || hasValue(value)) { dataArray.add(value); } } fireTableDataChanged(); Utilities.updateTableSizes(table); Utilities.updateScrollMode(scroll, table); } /** * Returns the list of values associated with a given attribute. * @param attrName the name of the attribute. * @return the list of values associated with a given attribute. */ public List getValues(String attrName) { List values = new ArrayList<>(); for (AttributeValuePair valuePair : dataArray) { if (valuePair.attrDesc.equals(AttributeDescription.valueOf(attrName)) && hasValue(valuePair)) { if (valuePair.value instanceof Collection) { values.addAll((Collection) valuePair.value); } else { values.add(valuePair.value); } } } return values; } private void updateObjectClass(ObjectClassValue newValue) { Entry oldResult = searchResult; Entry newResult = new LinkedHashMapEntry(searchResult.getName()); for (String attrName : schemaReadOnlyAttributesLowerCase) { Attribute attr = searchResult.getAttribute(attrName); if (attr != null && !attr.isEmpty()) { final Attribute newAttr = new LinkedAttribute(attr); newResult.removeAttribute(newAttr.getAttributeDescription()); newResult.addAttribute(newAttr); } } ignoreEntryChangeEvents = true; Schema schema = getInfo().getServerDescriptor().getSchema(); if (schema != null) { ArrayList attributes = new ArrayList<>(); ArrayList ocs = new ArrayList<>(); if (newValue.getStructural() != null) { ocs.add(newValue.getStructural().toLowerCase()); } for (String oc : newValue.getAuxiliary()) { ocs.add(oc.toLowerCase()); } for (String oc : ocs) { ObjectClass objectClass = schema.getObjectClass(oc); if (!objectClass.isPlaceHolder()) { for (AttributeType attr : objectClass.getRequiredAttributes()) { attributes.add(attr.getNameOrOID().toLowerCase()); } for (AttributeType attr : objectClass.getOptionalAttributes()) { attributes.add(attr.getNameOrOID().toLowerCase()); } } } for (String attrName : editableOperationalAttrNames) { attributes.add(attrName.toLowerCase()); } for (AttributeValuePair currValue : allSortedValues) { String attrNoOptions = currValue.attrDesc.getNameOrOID().toLowerCase(); if (attributes.contains(attrNoOptions) && !schemaReadOnlyAttributesLowerCase.contains(currValue.attrDesc.toString().toLowerCase())) { setValues(newResult, currValue.attrDesc.toString()); } } } update(newResult, isReadOnly, treePath); ignoreEntryChangeEvents = false; searchResult = oldResult; notifyListeners(); } private boolean isRequired(AttributeValuePair value) { return requiredAttrs.contains(value.attrDesc.getNameOrOID()); } private boolean hasValue(AttributeValuePair value) { boolean hasValue = value.value != null; if (hasValue) { if (value.value instanceof String) { return ((String) value.value).length() > 0; } else if (value.value instanceof byte[]) { return ((byte[]) value.value).length > 0; } } return hasValue; } } /** * A simple class that contains an attribute name and a single value. It is * used by the table model to be able to retrieve more easily all the values * for a given attribute. */ private static class AttributeValuePair { private final AttributeDescription attrDesc; private Object value; private AttributeValuePair(AttributeDescription attrDesc, Object value) { this.attrDesc = attrDesc; this.value = value; } } }