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 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2013-2015 ForgeRock AS
026 */
027package org.opends.server.types;
028
029import java.util.Collection;
030import java.util.Collections;
031import java.util.LinkedHashMap;
032import java.util.LinkedList;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.opendj.ldap.ResultCode;
039
040import static org.forgerock.util.Reject.*;
041import static org.opends.messages.SchemaMessages.*;
042import static org.opends.server.util.CollectionUtils.*;
043import static org.opends.server.util.ServerConstants.*;
044import static org.opends.server.util.StaticUtils.*;
045
046/**
047 * An abstract base class for LDAP schema definitions which contain an
048 * OID, optional names, description, an obsolete flag, and an optional
049 * set of extra properties.
050 * <p>
051 * This class defines common properties and behaviour of the various
052 * types of schema definitions (e.g. object class definitions, and
053 * attribute type definitions).
054 * <p>
055 * Any methods which accesses the set of names associated with this
056 * definition, will retrieve the primary name as the first name,
057 * regardless of whether or not it was contained in the original set
058 * of <code>names</code> passed to the constructor.
059 * <p>
060 * Where ordered sets of names, or extra properties are provided, the
061 * ordering will be preserved when the associated fields are accessed
062 * via their getters or via the {@link #toString()} methods.
063 * <p>
064 * Note that these schema elements are not completely immutable, as
065 * the set of extra properties for the schema element may be altered
066 * after the element is created.  Among other things, this allows the
067 * associated schema file to be edited so that an element created over
068 * protocol may be associated with a particular schema file.
069 */
070@org.opends.server.types.PublicAPI(
071     stability=org.opends.server.types.StabilityLevel.VOLATILE,
072     mayInstantiate=false,
073     mayExtend=false,
074     mayInvoke=true)
075public abstract class CommonSchemaElements implements SchemaFileElement {
076  /** Indicates whether this definition is declared "obsolete". */
077  private final boolean isObsolete;
078
079  /** The hash code for this definition. */
080  private final int hashCode;
081
082  /** The set of additional name-value pairs associated with this definition. */
083  private final Map<String, List<String>> extraProperties;
084
085  /**
086   * The set of names for this definition, in a mapping between
087   * the all-lowercase form and the user-defined form.
088   */
089  private final Map<String, String> names;
090
091  /** The description for this definition. */
092  private final String description;
093
094  /** The OID that may be used to reference this definition. */
095  private final String oid;
096
097  /** The primary name to use for this definition. */
098  private final String primaryName;
099
100  /** The lower case name for this definition. */
101  private final String lowerName;
102
103  /**
104   * Creates a new definition with the provided information.
105   * <p>
106   * If no <code>primaryName</code> is specified, but a set of
107   * <code>names</code> is specified, then the first name retrieved
108   * from the set of <code>names</code> will be used as the primary
109   * name.
110   *
111   * @param primaryName
112   *          The primary name for this definition, or
113   *          <code>null</code> if there is no primary name.
114   * @param names
115   *          The full set of names for this definition, or
116   *          <code>null</code> if there are no names.
117   * @param oid
118   *          The OID for this definition (must not be
119   *          <code>null</code>).
120   * @param description
121   *          The description for the definition, or <code>null</code>
122   *          if there is no description.
123   * @param isObsolete
124   *          Indicates whether this definition is declared
125   *          "obsolete".
126   * @param extraProperties
127   *          A set of extra properties for this definition, or
128   *          <code>null</code> if there are no extra properties.
129   * @throws NullPointerException
130   *           If the provided OID was <code>null</code>.
131   */
132  protected CommonSchemaElements(String primaryName,
133      Collection<String> names, String oid, String description,
134      boolean isObsolete, Map<String, List<String>> extraProperties)
135      throws NullPointerException {
136    // Make sure mandatory parameters are specified.
137    if (oid == null) {
138      throw new NullPointerException(
139          "No oid specified in constructor");
140    }
141
142    this.oid = oid;
143    this.description = description;
144    this.isObsolete = isObsolete;
145
146    // Make sure we have a primary name if possible.
147    if (primaryName != null) {
148      this.primaryName = primaryName;
149    } else if (names != null && !names.isEmpty()) {
150      this.primaryName = names.iterator().next();
151    } else {
152      this.primaryName = null;
153    }
154    this.lowerName = this.primaryName != null ? toLowerCase(this.primaryName) : oid;
155
156    // OPENDJ-1645: oid changes during server bootstrap, so prefer using lowername if available
157    hashCode = this.lowerName.hashCode();
158
159    // Construct the normalized attribute name mapping.
160    if (names != null) {
161      this.names = new LinkedHashMap<>(names.size());
162
163      // Make sure the primary name is first (never null).
164      this.names.put(lowerName, this.primaryName);
165
166      // Add the remaining names in the order specified.
167      for (String name : names) {
168        this.names.put(toLowerCase(name), name);
169      }
170    } else if (this.primaryName != null) {
171      this.names = Collections.singletonMap(lowerName, this.primaryName);
172    } else {
173      this.names = Collections.emptyMap();
174    }
175
176    // FIXME: should really be a deep-copy.
177    if (extraProperties != null) {
178      this.extraProperties = new LinkedHashMap<>(extraProperties);
179    } else {
180      this.extraProperties = Collections.emptyMap();
181    }
182  }
183
184  /**
185   * Check if the extra schema properties contain safe filenames.
186   *
187   * @param extraProperties
188   *          The schema properties to check.
189   *
190   * @throws DirectoryException
191   *          If a provided value was unsafe.
192   */
193  public static void checkSafeProperties(Map <String,List<String>>
194      extraProperties)
195      throws DirectoryException
196  {
197    // Check that X-SCHEMA-FILE doesn't contain unsafe characters
198    List<String> filenames = extraProperties.get(SCHEMA_PROPERTY_FILENAME);
199    if (filenames != null && !filenames.isEmpty()) {
200      String filename = filenames.get(0);
201      if (filename.indexOf('/') != -1 || filename.indexOf('\\') != -1)
202      {
203        LocalizableMessage message = ERR_ATTR_SYNTAX_ILLEGAL_X_SCHEMA_FILE.get(filename);
204        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
205            message);
206      }
207    }
208  }
209
210  /**
211   * Retrieves the primary name for this schema definition.
212   *
213   * @return The primary name for this schema definition, or
214   *         <code>null</code> if there is no primary name.
215   */
216  public final String getPrimaryName() {
217    return primaryName;
218  }
219
220  /**
221   * Retrieves an iterable over the set of normalized names that may
222   * be used to reference this schema definition. The normalized form
223   * of an attribute name is defined as the user-defined name
224   * converted to lower-case.
225   *
226   * @return Returns an iterable over the set of normalized names that
227   *         may be used to reference this schema definition.
228   */
229  public final Set<String> getNormalizedNames() {
230    return names.keySet();
231  }
232
233  /**
234   * Retrieves an iterable over the set of user-defined names that may
235   * be used to reference this schema definition.
236   *
237   * @return Returns an iterable over the set of user-defined names
238   *         that may be used to reference this schema definition.
239   */
240  public final Iterable<String> getUserDefinedNames() {
241    return names.values();
242  }
243
244  /**
245   * Indicates whether this schema definition has the specified name.
246   *
247   * @param lowerName
248   *          The lowercase name for which to make the determination.
249   * @return <code>true</code> if the specified name is assigned to
250   *         this schema definition, or <code>false</code> if not.
251   */
252  public final boolean hasName(String lowerName) {
253    return names.containsKey(lowerName);
254  }
255
256  /**
257   * Retrieves the OID for this schema definition.
258   *
259   * @return The OID for this schema definition.
260   */
261  public final String getOID() {
262    return oid;
263  }
264
265  /**
266   * Retrieves the name or OID for this schema definition. If it has
267   * one or more names, then the primary name will be returned. If it
268   * does not have any names, then the OID will be returned.
269   *
270   * @return The name or OID for this schema definition.
271   */
272  public final String getNameOrOID() {
273    if (primaryName != null) {
274      return primaryName;
275    }
276    // Guaranteed not to be null.
277    return oid;
278  }
279
280  /**
281   * Retrieves the normalized primary name or OID for this schema
282   * definition. If it does not have any names, then the OID will be
283   * returned.
284   *
285   * @return The name or OID for this schema definition.
286   */
287  public final String getNormalizedPrimaryNameOrOID() {
288    return lowerName;
289  }
290
291  /**
292   * Indicates whether this schema definition has the specified name
293   * or OID.
294   *
295   * @param lowerValue
296   *          The lowercase value for which to make the determination.
297   * @return <code>true</code> if the provided value matches the OID
298   *         or one of the names assigned to this schema definition,
299   *         or <code>false</code> if not.
300   */
301  public final boolean hasNameOrOID(String lowerValue) {
302    return names.containsKey(lowerValue) || oid.equals(lowerValue);
303  }
304
305  /**
306   * Retrieves the name of the schema file that contains the
307   * definition for this schema definition.
308   *
309   * @param elem The element where to get the schema file from
310   * @return The name of the schema file that contains the definition
311   *         for this schema definition, or <code>null</code> if it
312   *         is not known or if it is not stored in any schema file.
313   */
314  public static String getSchemaFile(SchemaFileElement elem)
315  {
316    return getSingleValueProperty(elem, SCHEMA_PROPERTY_FILENAME);
317  }
318
319  /**
320   * Retrieves the name of a single value property for this schema element.
321   *
322   * @param elem The element where to get the single value property from
323   * @param propertyName The name of the property to get
324   * @return The single value for this property, or <code>null</code> if it
325   *         is this property is not set.
326   */
327  public static String getSingleValueProperty(SchemaFileElement elem,
328      String propertyName)
329  {
330    List<String> values = elem.getExtraProperties().get(propertyName);
331    if (values != null && !values.isEmpty()) {
332      return values.get(0);
333    }
334    return null;
335  }
336
337  /**
338   * Specifies the name of the schema file that contains the
339   * definition for this schema element.  If a schema file is already
340   * defined in the set of extra properties, then it will be
341   * overwritten.  If the provided schema file value is {@code null},
342   * then any existing schema file definition will be removed.
343   *
344   * @param elem The element where to set the schema file
345   * @param  schemaFile  The name of the schema file that contains the
346   *                     definition for this schema element.
347   */
348  public static void setSchemaFile(SchemaFileElement elem, String schemaFile)
349  {
350    setExtraProperty(elem, SCHEMA_PROPERTY_FILENAME, schemaFile);
351  }
352
353  /**
354   * Retrieves the description for this schema definition.
355   *
356   * @return The description for this schema definition, or
357   *         <code>null</code> if there is no description.
358   */
359  public final String getDescription() {
360    return description;
361  }
362
363  /**
364   * Indicates whether this schema definition is declared "obsolete".
365   *
366   * @return <code>true</code> if this schema definition is declared
367   *         "obsolete", or <code>false</code> if not.
368   */
369  public final boolean isObsolete() {
370    return isObsolete;
371  }
372
373  @Override
374  public final Map<String, List<String>> getExtraProperties()
375  {
376    return extraProperties;
377  }
378
379  /**
380   * Sets the value for an "extra" property for this schema element.
381   * If a property already exists with the specified name, then it
382   * will be overwritten.  If the value is {@code null}, then any
383   * existing property with the given name will be removed.
384   *
385   * @param elem The element where to set the extra property
386   * @param  name   The name for the "extra" property.  It must not be
387   *                {@code null}.
388   * @param  value  The value for the "extra" property.  If it is
389   *                {@code null}, then any existing definition will be removed.
390   */
391  public static void setExtraProperty(SchemaFileElement elem,
392      String name, String value)
393  {
394    ifNull(name);
395
396    if (value == null)
397    {
398      elem.getExtraProperties().remove(name);
399    }
400    else
401    {
402      elem.getExtraProperties().put(name, newLinkedList(value));
403    }
404  }
405
406  /**
407   * Sets the values for an "extra" property for this schema element.
408   * If a property already exists with the specified name, then it
409   * will be overwritten.  If the set of values is {@code null} or
410   * empty, then any existing property with the given name will be
411   * removed.
412   *
413   * @param  name    The name for the "extra" property.  It must not
414   *                 be {@code null}.
415   * @param  values  The set of values for the "extra" property.  If
416   *                 it is {@code null} or empty, then any existing
417   *                 definition will be removed.
418   */
419  public final void setExtraProperty(String name,
420                                     List<String> values) {
421    ifNull(name);
422
423    if (values == null || values.isEmpty())
424    {
425      extraProperties.remove(name);
426    }
427    else
428    {
429      LinkedList<String> valuesCopy = new LinkedList<>(values);
430      extraProperties.put(name, valuesCopy);
431    }
432  }
433
434  /**
435   * Indicates whether the provided object is equal to this attribute
436   * type. The object will be considered equal if it is an attribute
437   * type with the same OID as the current type.
438   *
439   * @param o
440   *          The object for which to make the determination.
441   * @return <code>true</code> if the provided object is equal to
442   *         this schema definition, or <code>false</code> if not.
443   */
444  @Override
445  public final boolean equals(Object o) {
446    if (this == o) {
447      return true;
448    }
449
450    if (o instanceof CommonSchemaElements) {
451      CommonSchemaElements other = (CommonSchemaElements) o;
452      return lowerName.equals(other.lowerName);
453    }
454    return false;
455  }
456
457  /**
458   * Retrieves the hash code for this schema definition. It will be
459   * based on the sum of the bytes of the OID.
460   *
461   * @return The hash code for this schema definition.
462   */
463  @Override
464  public final int hashCode() {
465    return hashCode;
466  }
467
468  /**
469   * Retrieves the definition string used to create this attribute
470   * type and including the X-SCHEMA-FILE extension.
471   *
472   * @param elem The element where to get definition from
473   * @return  The definition string used to create this attribute
474   *          type including the X-SCHEMA-FILE extension.
475   */
476  public static String getDefinitionWithFileName(SchemaFileElement elem)
477  {
478    final String schemaFile = getSchemaFile(elem);
479    final String definition = elem.toString();
480    if (schemaFile != null)
481    {
482      int pos = definition.lastIndexOf(')');
483      return definition.substring(0, pos).trim() + " "
484          + SCHEMA_PROPERTY_FILENAME + " '" + schemaFile + "' )";
485    }
486    return definition;
487  }
488}