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.LinkedList;
032import java.util.LinkedHashMap;
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      if (names != null && !names.isEmpty()) {
149        this.primaryName = names.iterator().next();
150      } else {
151        this.primaryName = null;
152      }
153    } else {
154      this.primaryName = primaryName;
155    }
156    this.lowerName = toLowerCase(primaryName);
157
158    // OPENDJ-1645: oid changes during server bootstrap, so prefer using name if available
159    hashCode = getNameOrOID().hashCode();
160
161    // Construct the normalized attribute name mapping.
162    if (names != null) {
163      this.names = new LinkedHashMap<>(names.size());
164
165      // Make sure the primary name is first (never null).
166      this.names.put(lowerName, this.primaryName);
167
168      // Add the remaining names in the order specified.
169      for (String name : names) {
170        this.names.put(toLowerCase(name), name);
171      }
172    } else if (this.primaryName != null) {
173      this.names = Collections.singletonMap(lowerName, this.primaryName);
174    } else {
175      this.names = Collections.emptyMap();
176    }
177
178    // FIXME: should really be a deep-copy.
179    if (extraProperties != null) {
180      this.extraProperties = new LinkedHashMap<>(extraProperties);
181    } else {
182      this.extraProperties = Collections.emptyMap();
183    }
184  }
185
186  /**
187   * Check if the extra schema properties contain safe filenames.
188   *
189   * @param extraProperties
190   *          The schema properties to check.
191   *
192   * @throws DirectoryException
193   *          If a provided value was unsafe.
194   */
195  public static void checkSafeProperties(Map <String,List<String>>
196      extraProperties)
197      throws DirectoryException
198  {
199    // Check that X-SCHEMA-FILE doesn't contain unsafe characters
200    List<String> filenames = extraProperties.get(SCHEMA_PROPERTY_FILENAME);
201    if (filenames != null && !filenames.isEmpty()) {
202      String filename = filenames.get(0);
203      if (filename.indexOf('/') != -1 || filename.indexOf('\\') != -1)
204      {
205        LocalizableMessage message = ERR_ATTR_SYNTAX_ILLEGAL_X_SCHEMA_FILE.get(filename);
206        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
207            message);
208      }
209    }
210  }
211
212  /**
213   * Retrieves the primary name for this schema definition.
214   *
215   * @return The primary name for this schema definition, or
216   *         <code>null</code> if there is no primary name.
217   */
218  public final String getPrimaryName() {
219    return primaryName;
220  }
221
222  /**
223   * Retrieve the normalized primary name for this schema definition.
224   *
225   * @return Returns the normalized primary name for this attribute
226   *         type, or <code>null</code> if there is no primary name.
227   */
228  public final String getNormalizedPrimaryName() {
229    return lowerName;
230  }
231
232  /**
233   * Retrieves an iterable over the set of normalized names that may
234   * be used to reference this schema definition. The normalized form
235   * of an attribute name is defined as the user-defined name
236   * converted to lower-case.
237   *
238   * @return Returns an iterable over the set of normalized names that
239   *         may be used to reference this schema definition.
240   */
241  public final Set<String> getNormalizedNames() {
242    return names.keySet();
243  }
244
245  /**
246   * Retrieves an iterable over the set of user-defined names that may
247   * be used to reference this schema definition.
248   *
249   * @return Returns an iterable over the set of user-defined names
250   *         that may be used to reference this schema definition.
251   */
252  public final Iterable<String> getUserDefinedNames() {
253    return names.values();
254  }
255
256  /**
257   * Indicates whether this schema definition has the specified name.
258   *
259   * @param lowerName
260   *          The lowercase name for which to make the determination.
261   * @return <code>true</code> if the specified name is assigned to
262   *         this schema definition, or <code>false</code> if not.
263   */
264  public final boolean hasName(String lowerName) {
265    return names.containsKey(lowerName);
266  }
267
268  /**
269   * Retrieves the OID for this schema definition.
270   *
271   * @return The OID for this schema definition.
272   */
273  public final String getOID() {
274    return oid;
275  }
276
277  /**
278   * Retrieves the name or OID for this schema definition. If it has
279   * one or more names, then the primary name will be returned. If it
280   * does not have any names, then the OID will be returned.
281   *
282   * @return The name or OID for this schema definition.
283   */
284  public final String getNameOrOID() {
285    if (primaryName != null) {
286      return primaryName;
287    }
288    // Guaranteed not to be null.
289    return oid;
290  }
291
292  /**
293   * Retrieves the normalized primary name or OID for this schema
294   * definition. If it does not have any names, then the OID will be
295   * returned.
296   *
297   * @return The name or OID for this schema definition.
298   */
299  public final String getNormalizedPrimaryNameOrOID() {
300    if (lowerName != null) {
301      return lowerName;
302    }
303    // Guaranteed not to be null.
304    return oid;
305  }
306
307  /**
308   * Indicates whether this schema definition has the specified name
309   * or OID.
310   *
311   * @param lowerValue
312   *          The lowercase value for which to make the determination.
313   * @return <code>true</code> if the provided value matches the OID
314   *         or one of the names assigned to this schema definition,
315   *         or <code>false</code> if not.
316   */
317  public final boolean hasNameOrOID(String lowerValue) {
318    return names.containsKey(lowerValue) || oid.equals(lowerValue);
319  }
320
321  /**
322   * Retrieves the name of the schema file that contains the
323   * definition for this schema definition.
324   *
325   * @param elem The element where to get the schema file from
326   * @return The name of the schema file that contains the definition
327   *         for this schema definition, or <code>null</code> if it
328   *         is not known or if it is not stored in any schema file.
329   */
330  public static String getSchemaFile(SchemaFileElement elem)
331  {
332    return getSingleValueProperty(elem, SCHEMA_PROPERTY_FILENAME);
333  }
334
335  /**
336   * Retrieves the name of a single value property for this schema element.
337   *
338   * @param elem The element where to get the single value property from
339   * @param propertyName The name of the property to get
340   * @return The single value for this property, or <code>null</code> if it
341   *         is this property is not set.
342   */
343  public static String getSingleValueProperty(SchemaFileElement elem,
344      String propertyName)
345  {
346    List<String> values = elem.getExtraProperties().get(propertyName);
347    if (values != null && !values.isEmpty()) {
348      return values.get(0);
349    }
350    return null;
351  }
352
353  /**
354   * Specifies the name of the schema file that contains the
355   * definition for this schema element.  If a schema file is already
356   * defined in the set of extra properties, then it will be
357   * overwritten.  If the provided schema file value is {@code null},
358   * then any existing schema file definition will be removed.
359   *
360   * @param elem The element where to set the schema file
361   * @param  schemaFile  The name of the schema file that contains the
362   *                     definition for this schema element.
363   */
364  public static void setSchemaFile(SchemaFileElement elem, String schemaFile)
365  {
366    setExtraProperty(elem, SCHEMA_PROPERTY_FILENAME, schemaFile);
367  }
368
369  /**
370   * Retrieves the description for this schema definition.
371   *
372   * @return The description for this schema definition, or
373   *         <code>null</code> if there is no description.
374   */
375  public final String getDescription() {
376    return description;
377  }
378
379  /**
380   * Indicates whether this schema definition is declared "obsolete".
381   *
382   * @return <code>true</code> if this schema definition is declared
383   *         "obsolete", or <code>false</code> if not.
384   */
385  public final boolean isObsolete() {
386    return isObsolete;
387  }
388
389  @Override
390  public final Map<String, List<String>> getExtraProperties()
391  {
392    return extraProperties;
393  }
394
395  /**
396   * Sets the value for an "extra" property for this schema element.
397   * If a property already exists with the specified name, then it
398   * will be overwritten.  If the value is {@code null}, then any
399   * existing property with the given name will be removed.
400   *
401   * @param elem The element where to set the extra property
402   * @param  name   The name for the "extra" property.  It must not be
403   *                {@code null}.
404   * @param  value  The value for the "extra" property.  If it is
405   *                {@code null}, then any existing definition will be removed.
406   */
407  public static void setExtraProperty(SchemaFileElement elem,
408      String name, String value)
409  {
410    ifNull(name);
411
412    if (value == null)
413    {
414      elem.getExtraProperties().remove(name);
415    }
416    else
417    {
418      elem.getExtraProperties().put(name, newLinkedList(value));
419    }
420  }
421
422  /**
423   * Sets the values for an "extra" property for this schema element.
424   * If a property already exists with the specified name, then it
425   * will be overwritten.  If the set of values is {@code null} or
426   * empty, then any existing property with the given name will be
427   * removed.
428   *
429   * @param  name    The name for the "extra" property.  It must not
430   *                 be {@code null}.
431   * @param  values  The set of values for the "extra" property.  If
432   *                 it is {@code null} or empty, then any existing
433   *                 definition will be removed.
434   */
435  public final void setExtraProperty(String name,
436                                     List<String> values) {
437    ifNull(name);
438
439    if (values == null || values.isEmpty())
440    {
441      extraProperties.remove(name);
442    }
443    else
444    {
445      LinkedList<String> valuesCopy = new LinkedList<>(values);
446      extraProperties.put(name, valuesCopy);
447    }
448  }
449
450  /**
451   * Indicates whether the provided object is equal to this attribute
452   * type. The object will be considered equal if it is an attribute
453   * type with the same OID as the current type.
454   *
455   * @param o
456   *          The object for which to make the determination.
457   * @return <code>true</code> if the provided object is equal to
458   *         this schema definition, or <code>false</code> if not.
459   */
460  @Override
461  public final boolean equals(Object o) {
462    if (this == o) {
463      return true;
464    }
465
466    if (o instanceof CommonSchemaElements) {
467      CommonSchemaElements other = (CommonSchemaElements) o;
468      return getNameOrOID().equals(other.getNameOrOID());
469    }
470
471    return false;
472  }
473
474  /**
475   * Retrieves the hash code for this schema definition. It will be
476   * based on the sum of the bytes of the OID.
477   *
478   * @return The hash code for this schema definition.
479   */
480  @Override
481  public final int hashCode() {
482    return hashCode;
483  }
484
485  /**
486   * Retrieves the definition string used to create this attribute
487   * type and including the X-SCHEMA-FILE extension.
488   *
489   * @param elem The element where to get definition from
490   * @return  The definition string used to create this attribute
491   *          type including the X-SCHEMA-FILE extension.
492   */
493  public static String getDefinitionWithFileName(SchemaFileElement elem)
494  {
495    final String schemaFile = getSchemaFile(elem);
496    final String definition = elem.toString();
497    if (schemaFile != null)
498    {
499      int pos = definition.lastIndexOf(')');
500      return definition.substring(0, pos).trim() + " "
501          + SCHEMA_PROPERTY_FILENAME + " '" + schemaFile + "' )";
502    }
503    return definition;
504  }
505}