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 2011-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.ui;
018
019import static org.opends.messages.AdminToolMessages.*;
020import static org.opends.server.util.CollectionUtils.*;
021
022import java.awt.Container;
023import java.awt.GridBagConstraints;
024import java.text.ParseException;
025import java.util.ArrayList;
026import java.util.Iterator;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Set;
030import java.util.SortedSet;
031import java.util.TreeSet;
032
033import javax.swing.JLabel;
034import javax.swing.tree.TreePath;
035
036import org.forgerock.i18n.LocalizableMessage;
037import org.forgerock.opendj.ldap.AVA;
038import org.forgerock.opendj.ldap.AttributeDescription;
039import org.forgerock.opendj.ldap.ByteString;
040import org.forgerock.opendj.ldap.schema.AttributeType;
041import org.forgerock.opendj.ldap.schema.ObjectClass;
042import org.forgerock.opendj.ldap.schema.ObjectClassType;
043import org.opends.guitools.controlpanel.datamodel.BinaryValue;
044import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
045import org.opends.guitools.controlpanel.datamodel.ObjectClassValue;
046import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent;
047import org.opends.guitools.controlpanel.event.LDAPEntryChangedEvent;
048import org.opends.guitools.controlpanel.event.LDAPEntryChangedListener;
049import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
050import org.opends.guitools.controlpanel.util.Utilities;
051import org.opends.server.schema.SchemaConstants;
052import org.opends.server.types.Attributes;
053import org.opends.server.types.Entry;
054import org.opends.server.types.OpenDsException;
055import org.opends.server.types.Schema;
056import org.opends.server.util.Base64;
057import org.opends.server.util.ServerConstants;
058
059/**
060 * Abstract class containing code shared by the different LDAP entry view
061 * panels (Simplified View, Attribute View and LDIF View).
062 */
063public abstract class ViewEntryPanel extends StatusGenericPanel
064{
065  private static final long serialVersionUID = -1908757626234678L;
066  /** The read-only attributes as they appear on the schema. */
067  protected SortedSet<String> schemaReadOnlyAttributes = new TreeSet<>();
068  /** The read-only attributes in lower case. */
069  protected SortedSet<String> schemaReadOnlyAttributesLowerCase = new TreeSet<>();
070  /** The editable operational attributes. */
071  protected SortedSet<String> editableOperationalAttrNames = new TreeSet<>();
072  private JLabel title= Utilities.createDefaultLabel();
073
074  private Set<LDAPEntryChangedListener> listeners = new LinkedHashSet<>();
075
076  /** Whether the entry change events should be ignored or not. */
077  protected boolean ignoreEntryChangeEvents;
078
079  /** Static boolean used to know whether only attributes with values should be displayed or not. */
080  protected static boolean displayOnlyWithAttrs = true;
081
082  @Override
083  public void okClicked()
084  {
085    // No ok button
086  }
087
088  /**
089   * Returns an Entry object representing what the panel is displaying.
090   * @return an Entry object representing what the panel is displaying.
091   * @throws OpenDsException if the entry cannot be generated (in particular if
092   * the user provided invalid data).
093   */
094  public abstract Entry getEntry() throws OpenDsException;
095
096  /**
097   * Updates the contents of the panel.
098   * @param sr the search result to be used to update the panel.
099   * @param isReadOnly whether the entry is read-only or not.
100   * @param path the tree path associated with the entry in the tree.
101   */
102  public abstract void update(CustomSearchResult sr, boolean isReadOnly, TreePath path);
103
104  /**
105   * Adds a title panel to the container.
106   * @param c the container where the title panel must be added.
107   * @param gbc the grid bag constraints to be used.
108   */
109  protected void addTitlePanel(Container c, GridBagConstraints gbc)
110  {
111    c.add(title, gbc);
112  }
113
114  /**
115   * Whether the schema must be checked or not.
116   * @return <CODE>true</CODE> if the server is configured to check schema and
117   * <CODE>false</CODE> otherwise.
118   */
119  protected boolean checkSchema()
120  {
121    return getInfo().getServerDescriptor().isSchemaEnabled();
122  }
123
124  /**
125   * Adds an LDAP entry change listener.
126   * @param listener the listener.
127   */
128  public void addLDAPEntryChangedListener(LDAPEntryChangedListener listener)
129  {
130    listeners.add(listener);
131  }
132
133  /**
134   * Removes an LDAP entry change listener.
135   * @param listener the listener.
136   */
137  public void removeLDAPEntryChangedListener(LDAPEntryChangedListener listener)
138  {
139    listeners.remove(listener);
140  }
141
142  @Override
143  public boolean requiresBorder()
144  {
145    return true;
146  }
147
148  /**
149   * Returns the DN of the entry that the user is editing (it might differ
150   * from the DN of the entry in the tree if the user modified the DN).
151   * @return the DN of the entry that the user is editing.
152   */
153  protected abstract String getDisplayedDN();
154
155  /** Notifies the entry changed listeners that the entry changed. */
156  protected void notifyListeners()
157  {
158    if (ignoreEntryChangeEvents)
159    {
160      return;
161    }
162    // TODO: With big entries this is pretty slow.  Until there is a fix, try
163    // simply to update the dn
164    String dn = getDisplayedDN();
165    if (dn != null && !dn.equals(title.getText()))
166    {
167      title.setText(dn);
168    }
169    LDAPEntryChangedEvent ev = new LDAPEntryChangedEvent(this, null);
170    for (LDAPEntryChangedListener listener : listeners)
171    {
172      listener.entryChanged(ev);
173    }
174  }
175
176  /**
177   * Updates the title panel with the provided entry.
178   * @param sr the search result.
179   * @param path the path to the node of the entry selected in the tree.  Used
180   * to display the same icon as in the tree.
181   */
182  protected void updateTitle(CustomSearchResult sr, TreePath path)
183  {
184    String dn = sr.getDN();
185    if (dn != null && dn.length() > 0)
186    {
187      title.setText(sr.getDN());
188    }
189    else if (path != null)
190    {
191      BasicNode node = (BasicNode)path.getLastPathComponent();
192      title.setText(node.getDisplayName());
193    }
194
195    if (path != null)
196    {
197      BasicNode node = (BasicNode)path.getLastPathComponent();
198      title.setIcon(node.getIcon());
199    }
200    else
201    {
202      title.setIcon(null);
203    }
204
205    List<Object> ocs =
206      sr.getAttributeValues(ServerConstants.OBJECTCLASS_ATTRIBUTE_TYPE_NAME);
207    Schema schema = getInfo().getServerDescriptor().getSchema();
208    if (!ocs.isEmpty() && schema != null)
209    {
210      ObjectClassValue ocDesc = getObjectClassDescriptor(ocs, schema);
211      StringBuilder sb = new StringBuilder();
212      sb.append("<html>");
213      if (ocDesc.getStructural() != null)
214      {
215        sb.append(INFO_CTRL_OBJECTCLASS_DESCRIPTOR.get(ocDesc.getStructural()));
216      }
217      if (!ocDesc.getAuxiliary().isEmpty())
218      {
219        if (sb.length() > 0)
220        {
221          sb.append("<br>");
222        }
223        sb.append(INFO_CTRL_AUXILIARY_OBJECTCLASS_DESCRIPTOR.get(
224            Utilities.getStringFromCollection(ocDesc.getAuxiliary(), ", ")));
225      }
226      title.setToolTipText(sb.toString());
227    }
228    else
229    {
230      title.setToolTipText(null);
231    }
232  }
233
234  /**
235   * Returns an object class value representing all the object class values of
236   * the entry.
237   * @param ocValues the list of object class values.
238   * @param schema the schema.
239   * @return an object class value representing all the object class values of
240   * the entry.
241   */
242  protected ObjectClassValue getObjectClassDescriptor(List<Object> ocValues,
243      Schema schema)
244  {
245    ObjectClass structuralObjectClass = null;
246    SortedSet<String> auxiliaryClasses = new TreeSet<>();
247    for (Object o : ocValues)
248    {
249      ObjectClass objectClass = schema.getObjectClass(((String) o));
250      if (!objectClass.isPlaceHolder())
251      {
252        if (objectClass.getObjectClassType() == ObjectClassType.STRUCTURAL)
253        {
254          if (structuralObjectClass == null || objectClass.isDescendantOf(structuralObjectClass))
255          {
256            structuralObjectClass = objectClass;
257          }
258        }
259        else
260        {
261          String name = objectClass.getNameOrOID();
262          if (!SchemaConstants.TOP_OBJECTCLASS_NAME.equals(name))
263          {
264            auxiliaryClasses.add(objectClass.getNameOrOID());
265          }
266        }
267      }
268    }
269    String structural = structuralObjectClass != null ? structuralObjectClass.getNameOrOID() : null;
270    return new ObjectClassValue(structural, auxiliaryClasses);
271  }
272
273  /**
274   * Adds the values in the RDN to the entry definition.
275   * @param entry the entry to be updated.
276   */
277  protected void addValuesInRDN(Entry entry)
278  {
279    // Add the values in the RDN if they are not there
280    for (AVA ava : entry.getName().rdn())
281    {
282      String attrName = ava.getAttributeName();
283      ByteString value = ava.getAttributeValue();
284      boolean done = false;
285      for (org.opends.server.types.Attribute attr : entry.getAttribute(attrName))
286      {
287        // TODO JNR use Entry.getAttribute(AttributeDescription) instead?
288        if (attr.getAttributeDescription().toString().equals(attrName))
289        {
290          List<ByteString> newValues = getValues(attr);
291          newValues.add(value);
292          entry.addAttribute(attr, newValues);
293          done = true;
294          break;
295        }
296      }
297      if (!done)
298      {
299        entry.addAttribute(Attributes.create(ava.getAttributeType(), value), newArrayList(value));
300      }
301    }
302  }
303
304  private List<ByteString> getValues(org.opends.server.types.Attribute attr)
305  {
306    List<ByteString> newValues = new ArrayList<>();
307    Iterator<ByteString> it = attr.iterator();
308    while (it.hasNext())
309    {
310      newValues.add(it.next());
311    }
312    return newValues;
313  }
314
315  @Override
316  public LocalizableMessage getTitle()
317  {
318    return INFO_CTRL_PANEL_EDIT_LDAP_ENTRY_TITLE.get();
319  }
320
321  @Override
322  public void configurationChanged(ConfigurationChangeEvent ev)
323  {
324    Schema schema = ev.getNewDescriptor().getSchema();
325    if (schema != null && schemaReadOnlyAttributes.isEmpty())
326    {
327      schemaReadOnlyAttributes.clear();
328      schemaReadOnlyAttributesLowerCase.clear();
329      for (AttributeType attr : schema.getAttributeTypes())
330      {
331        if (attr.isNoUserModification())
332        {
333          String attrName = attr.getNameOrOID();
334          schemaReadOnlyAttributes.add(attrName);
335          schemaReadOnlyAttributesLowerCase.add(attrName.toLowerCase());
336        }
337        else if (attr.isOperational())
338        {
339          editableOperationalAttrNames.add(attr.getNameOrOID());
340        }
341      }
342    }
343  }
344
345  /**
346   * Appends the LDIF line corresponding to the value of an
347   * attribute to the provided StringBuilder.
348   * @param sb the StringBuilder that must be updated.
349   * @param attrName the attribute name.
350   * @param value the attribute value.
351   */
352  protected void appendLDIFLine(StringBuilder sb, String attrName, Object value)
353  {
354    if (value instanceof ObjectClassValue)
355    {
356      ObjectClassValue ocValue = (ObjectClassValue)value;
357      if (ocValue.getStructural() != null)
358      {
359        sb.append("\n");
360        sb.append(attrName).append(": ").append(ocValue.getStructural());
361        Schema schema = getInfo().getServerDescriptor().getSchema();
362        if (schema != null)
363        {
364          ObjectClass oc = schema.getObjectClass(ocValue.getStructural());
365          if (!oc.isPlaceHolder())
366          {
367            Set<String> names = getObjectClassSuperiorValues(oc);
368            for (String name : names)
369            {
370              sb.append("\n");
371              sb.append(attrName).append(": ").append(name);
372            }
373          }
374        }
375      }
376      for (String v : ocValue.getAuxiliary())
377      {
378        sb.append("\n");
379        sb.append(attrName).append(": ").append(v);
380      }
381    }
382    else if (value instanceof byte[])
383    {
384      if (((byte[])value).length > 0)
385      {
386        sb.append("\n");
387        sb.append(attrName).append(":: ").append(Base64.encode((byte[])value));
388      }
389    }
390    else if (value instanceof BinaryValue)
391    {
392      sb.append("\n");
393      sb.append(attrName).append(":: ").append(((BinaryValue)value).getBase64());
394    }
395    else if (String.valueOf(value).trim().length() > 0)
396    {
397      sb.append("\n");
398      sb.append(attrName).append(": ").append(value);
399    }
400  }
401
402  /**
403   * Returns <CODE>true</CODE> if the provided attribute name has binary syntax
404   * and <CODE>false</CODE> otherwise.
405   * @param attrName the attribute name.
406   * @return <CODE>true</CODE> if the provided attribute name has binary syntax
407   * and <CODE>false</CODE> otherwise.
408   */
409  protected boolean isBinary(String attrName)
410  {
411    Schema schema = getInfo().getServerDescriptor().getSchema();
412    return Utilities.hasBinarySyntax(attrName, schema);
413  }
414
415  /**
416   * Returns <CODE>true</CODE> if the provided attribute name has password
417   * syntax and <CODE>false</CODE> otherwise.
418   * @param attrName the attribute name.
419   * @return <CODE>true</CODE> if the provided attribute name has password
420   * syntax and <CODE>false</CODE> otherwise.
421   */
422  protected boolean isPassword(String attrName)
423  {
424    Schema schema = getInfo().getServerDescriptor().getSchema();
425    return Utilities.hasPasswordSyntax(attrName, schema);
426  }
427
428  /**
429   * Gets the values associated with a given attribute.  The values are the
430   * ones displayed in the panel.
431   * @param attrName the attribute name.
432   * @return the values associated with a given attribute.
433   */
434  protected abstract List<Object> getValues(String attrName);
435
436  /**
437   * Sets the values displayed in the panel for a given attribute in the
438   * provided search result.
439   * @param sr the search result to be updated.
440   * @param attrName the attribute name.
441   */
442  protected void setValues(CustomSearchResult sr, String attrName)
443  {
444    List<Object> values = getValues(attrName);
445    List<Object> valuesToSet = new ArrayList<>();
446    for (Object value : values)
447    {
448      if (value instanceof ObjectClassValue)
449      {
450        ObjectClassValue ocValue = (ObjectClassValue)value;
451        if (ocValue.getStructural() != null)
452        {
453          valuesToSet.add(ocValue.getStructural());
454        }
455        valuesToSet.addAll(ocValue.getAuxiliary());
456      }
457      else if (value instanceof byte[])
458      {
459        valuesToSet.add(value);
460      }
461      else if (value instanceof BinaryValue)
462      {
463        try
464        {
465          valuesToSet.add(((BinaryValue)value).getBytes());
466        }
467        catch (ParseException pe)
468        {
469         throw new RuntimeException("Unexpected error: "+pe, pe);
470        }
471      }
472      else if (String.valueOf(value).trim().length() > 0)
473      {
474        valuesToSet.add(String.valueOf(value));
475      }
476    }
477    if (!valuesToSet.isEmpty())
478    {
479      sr.set(attrName, valuesToSet);
480    }
481  }
482
483  /**
484   * Returns <CODE>true</CODE> if the provided attribute name is an editable
485   * attribute and <CODE>false</CODE> otherwise.
486   * @param attrName the attribute name.
487   * @param schema the schema.
488   * @return <CODE>true</CODE> if the provided attribute name is an editable
489   * attribute and <CODE>false</CODE> otherwise.
490   */
491  public static boolean isEditable(String attrName, Schema schema)
492  {
493    attrName = AttributeDescription.valueOf(attrName).getNameOrOID();
494    if (schema != null && schema.hasAttributeType(attrName))
495    {
496      AttributeType attrType = schema.getAttributeType(attrName);
497      return !attrType.isNoUserModification();
498    }
499    return false;
500  }
501
502  /**
503   * Returns the list of superior object classes (to top) for a given object class.
504   * @param oc the object class.
505   * @return the set of superior object classes for a given object classes.
506   */
507  protected Set<String> getObjectClassSuperiorValues(ObjectClass oc)
508  {
509    Set<String> names = new LinkedHashSet<>();
510    Set<ObjectClass> parents = oc.getSuperiorClasses();
511    if (parents != null && !parents.isEmpty())
512    {
513      for (ObjectClass parent : parents)
514      {
515        names.add(parent.getNameOrOID());
516        names.addAll(getObjectClassSuperiorValues(parent));
517      }
518    }
519    return names;
520  }
521}