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