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 2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2013-2015 ForgeRock AS.
026 */
027package org.opends.server.api;
028
029import static org.opends.messages.CoreMessages.*;
030import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
031
032import java.util.AbstractMap.SimpleImmutableEntry;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.Iterator;
036import java.util.LinkedHashMap;
037import java.util.LinkedHashSet;
038import java.util.List;
039import java.util.Map;
040import java.util.Map.Entry;
041import java.util.Set;
042import java.util.concurrent.ConcurrentHashMap;
043import java.util.concurrent.CopyOnWriteArrayList;
044
045import org.opends.server.core.DirectoryServer;
046import org.opends.server.types.Attribute;
047import org.opends.server.types.AttributeBuilder;
048import org.opends.server.types.AttributeType;
049import org.forgerock.opendj.ldap.ByteString;
050import org.opends.server.types.Attributes;
051import org.forgerock.opendj.ldap.ByteSequence;
052import org.forgerock.opendj.ldap.ByteSequenceReader;
053import org.forgerock.opendj.ldap.ByteStringBuilder;
054import org.opends.server.types.DirectoryException;
055import org.opends.server.types.ObjectClass;
056
057/**
058 * This class provides a utility for interacting with compressed representations
059 * of schema elements. The default implementation does not persist encoded
060 * attributes and object classes.
061 */
062@org.opends.server.types.PublicAPI(
063    stability = org.opends.server.types.StabilityLevel.UNCOMMITTED,
064    mayInstantiate = false,
065    mayExtend = true,
066    mayInvoke = false)
067public class CompressedSchema
068{
069  /** Maps attribute description to ID. */
070  private final List<Entry<AttributeType, Set<String>>> adDecodeMap = new CopyOnWriteArrayList<>();
071  /** Maps ID to attribute description. */
072  private final Map<Entry<AttributeType, Set<String>>, Integer> adEncodeMap = new ConcurrentHashMap<>();
073  /** The map between encoded representations and object class sets. */
074  private final List<Map<ObjectClass, String>> ocDecodeMap = new CopyOnWriteArrayList<>();
075  /** The map between object class sets and encoded representations. */
076  private final Map<Map<ObjectClass, String>, Integer> ocEncodeMap = new ConcurrentHashMap<>();
077
078  /**
079   * Decodes the contents of the provided array as an attribute at the current
080   * position.
081   *
082   * @param reader
083   *          The byte string reader containing the encoded entry.
084   * @return The decoded attribute.
085   * @throws DirectoryException
086   *           If the attribute could not be decoded properly for some reason.
087   */
088  public final Attribute decodeAttribute(final ByteSequenceReader reader)
089      throws DirectoryException
090  {
091    // First decode the encoded attribute description id.
092    final int length = reader.readBERLength();
093    final byte[] idBytes = new byte[length];
094    reader.readBytes(idBytes);
095    final int id = decodeId(idBytes);
096
097    // Look up the attribute description.
098    Entry<AttributeType, Set<String>> ad = adDecodeMap.get(id);
099    if (ad == null)
100    {
101      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
102          ERR_COMPRESSEDSCHEMA_UNRECOGNIZED_AD_TOKEN.get(id));
103    }
104
105    // Before returning the attribute, make sure that the attribute type is not
106    // stale.
107    AttributeType attrType = ad.getKey();
108    Set<String> options = ad.getValue();
109    if (attrType.isDirty())
110    {
111      ad = loadAttribute(idBytes, attrType.getNameOrOID(), options);
112      attrType = ad.getKey();
113      options = ad.getValue();
114    }
115
116    // Determine the number of values for the attribute.
117    final int numValues = reader.readBERLength();
118
119    // For the common case of a single value with no options, generate
120    // less garbage.
121    if (numValues == 1 && options.isEmpty())
122    {
123      final int valueLength = reader.readBERLength();
124      final ByteSequence valueBytes = reader.readByteSequence(valueLength);
125      return Attributes.create(attrType, valueBytes.toByteString());
126    }
127    else
128    {
129      // Read the appropriate number of values.
130      final AttributeBuilder builder = new AttributeBuilder(attrType);
131      builder.setOptions(options);
132      for (int i = 0; i < numValues; i++)
133      {
134        final int valueLength = reader.readBERLength();
135        final ByteSequence valueBytes = reader.readByteSequence(valueLength);
136        builder.add(valueBytes.toByteString());
137      }
138      return builder.toAttribute();
139    }
140  }
141
142
143
144  /**
145   * Decodes an object class set from the provided byte string.
146   *
147   * @param reader
148   *          The byte string reader containing the object class set identifier.
149   * @return The decoded object class set.
150   * @throws DirectoryException
151   *           If the provided byte string reader cannot be decoded as an object
152   *           class set.
153   */
154  public final Map<ObjectClass, String> decodeObjectClasses(
155      final ByteSequenceReader reader) throws DirectoryException
156  {
157    // First decode the encoded object class id.
158    final int length = reader.readBERLength();
159    final byte[] idBytes = new byte[length];
160    reader.readBytes(idBytes);
161    final int id = decodeId(idBytes);
162
163    // Look up the object classes.
164    final Map<ObjectClass, String> ocMap = ocDecodeMap.get(id);
165    if (ocMap != null)
166    {
167      // Before returning the object classes, make sure that none of them are
168      // stale.
169      for (final ObjectClass oc : ocMap.keySet())
170      {
171        if (oc.isDirty())
172        {
173          // Found at least one object class which is dirty so refresh them.
174          return loadObjectClasses(idBytes, ocMap.values());
175        }
176      }
177      return ocMap;
178    }
179    else
180    {
181      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
182          ERR_COMPRESSEDSCHEMA_UNKNOWN_OC_TOKEN.get(id));
183    }
184  }
185
186
187
188  /**
189   * Encodes the information in the provided attribute to a byte array.
190   *
191   * @param builder
192   *          The buffer to encode the attribute to.
193   * @param attribute
194   *          The attribute to be encoded.
195   * @throws DirectoryException
196   *           If a problem occurs while attempting to determine the appropriate
197   *           identifier.
198   */
199  public final void encodeAttribute(final ByteStringBuilder builder,
200      final Attribute attribute) throws DirectoryException
201  {
202    // Re-use or allocate a new ID.
203    final AttributeType type = attribute.getAttributeType();
204    final Set<String> options = attribute.getOptions();
205    final Entry<AttributeType, Set<String>> ad = new SimpleImmutableEntry<>(type, options);
206
207    // Use double checked locking to avoid lazy registration races.
208    Integer id = adEncodeMap.get(ad);
209    if (id == null)
210    {
211      synchronized (adEncodeMap)
212      {
213        id = adEncodeMap.get(ad);
214        if (id == null)
215        {
216          id = adDecodeMap.size();
217          adDecodeMap.add(ad);
218          adEncodeMap.put(ad, id);
219          storeAttribute(encodeId(id), type.getNameOrOID(), options);
220        }
221      }
222    }
223
224    // Encode the attribute.
225    final byte[] idBytes = encodeId(id);
226    builder.appendBERLength(idBytes.length);
227    builder.appendBytes(idBytes);
228    builder.appendBERLength(attribute.size());
229    for (final ByteString v : attribute)
230    {
231      builder.appendBERLength(v.length());
232      builder.appendBytes(v);
233    }
234  }
235
236
237
238  /**
239   * Encodes the provided set of object classes to a byte array. If the same set
240   * had been previously encoded, then the cached value will be used. Otherwise,
241   * a new value will be created.
242   *
243   * @param builder
244   *          The buffer to encode the object classes to.
245   * @param objectClasses
246   *          The set of object classes for which to retrieve the corresponding
247   *          byte array token.
248   * @throws DirectoryException
249   *           If a problem occurs while attempting to determine the appropriate
250   *           identifier.
251   */
252  public final void encodeObjectClasses(final ByteStringBuilder builder,
253      final Map<ObjectClass, String> objectClasses) throws DirectoryException
254  {
255    // Re-use or allocate a new ID.
256    // Use double checked locking to avoid lazy registration races.
257    Integer id = ocEncodeMap.get(objectClasses);
258    if (id == null)
259    {
260      synchronized (ocEncodeMap)
261      {
262        id = ocEncodeMap.get(objectClasses);
263        if (id == null)
264        {
265          id = ocDecodeMap.size();
266          ocDecodeMap.add(objectClasses);
267          ocEncodeMap.put(objectClasses, id);
268          storeObjectClasses(encodeId(id), objectClasses.values());
269        }
270      }
271    }
272
273    // Encode the object classes.
274    final byte[] idBytes = encodeId(id);
275    builder.appendBERLength(idBytes.length);
276    builder.appendBytes(idBytes);
277  }
278
279
280
281  /**
282   * Returns a view of the encoded attributes in this compressed schema which
283   * can be used for saving the entire content to disk. The iterator returned by
284   * this method is not thread safe.
285   *
286   * @return A view of the encoded attributes in this compressed schema.
287   */
288  protected final Iterable<Entry<byte[],
289                                 Entry<String,
290                                       Collection<String>>>> getAllAttributes()
291  {
292    return new Iterable<Entry<byte[], Entry<String, Collection<String>>>>()
293    {
294
295      @Override
296      public Iterator<Entry<byte[],
297                            Entry<String, Collection<String>>>> iterator()
298      {
299        return new Iterator<Entry<byte[], Entry<String, Collection<String>>>>()
300        {
301          private int id = 0;
302
303
304
305          @Override
306          public boolean hasNext()
307          {
308            return id < adDecodeMap.size();
309          }
310
311
312
313          @Override
314          public Entry<byte[], Entry<String, Collection<String>>> next()
315          {
316            final byte[] encodedAttribute = encodeId(id);
317            final Entry<AttributeType, Set<String>> ad = adDecodeMap.get(id++);
318            return new SimpleImmutableEntry<byte[],
319                                            Entry<String, Collection<String>>>(
320                encodedAttribute,
321                new SimpleImmutableEntry<String, Collection<String>>(ad
322                    .getKey().getNameOrOID(), ad.getValue()));
323          }
324
325
326
327          @Override
328          public void remove()
329          {
330            throw new UnsupportedOperationException();
331          }
332        };
333      }
334    };
335  }
336
337
338
339  /**
340   * Returns a view of the encoded object classes in this compressed schema
341   * which can be used for saving the entire content to disk. The iterator
342   * returned by this method is not thread safe.
343   *
344   * @return A view of the encoded object classes in this compressed schema.
345   */
346  protected final Iterable<Entry<byte[],
347                                 Collection<String>>> getAllObjectClasses()
348  {
349    return new Iterable<Entry<byte[], Collection<String>>>()
350    {
351
352      @Override
353      public Iterator<Entry<byte[], Collection<String>>> iterator()
354      {
355        return new Iterator<Map.Entry<byte[], Collection<String>>>()
356        {
357          private int id = 0;
358
359          @Override
360          public boolean hasNext()
361          {
362            return id < ocDecodeMap.size();
363          }
364
365          @Override
366          public Entry<byte[], Collection<String>> next()
367          {
368            final byte[] encodedObjectClasses = encodeId(id);
369            final Map<ObjectClass, String> ocMap = ocDecodeMap.get(id++);
370            return new SimpleImmutableEntry<>(encodedObjectClasses, ocMap.values());
371          }
372
373          @Override
374          public void remove()
375          {
376            throw new UnsupportedOperationException();
377          }
378        };
379      }
380    };
381  }
382
383  /**
384   * Loads an encoded attribute into this compressed schema. This method may
385   * called by implementations during initialization when loading content from
386   * disk.
387   *
388   * @param encodedAttribute
389   *          The encoded attribute description.
390   * @param attributeName
391   *          The user provided attribute type name.
392   * @param attributeOptions
393   *          The non-null but possibly empty set of attribute options.
394   * @return The attribute type description.
395   */
396  protected final Entry<AttributeType, Set<String>> loadAttribute(
397      final byte[] encodedAttribute, final String attributeName,
398      final Collection<String> attributeOptions)
399  {
400    final AttributeType type = DirectoryServer.getAttributeTypeOrDefault(toLowerCase(attributeName));
401    final Set<String> options = getOptions(attributeOptions);
402    final Entry<AttributeType, Set<String>> ad = new SimpleImmutableEntry<>(type, options);
403    final int id = decodeId(encodedAttribute);
404    synchronized (adEncodeMap)
405    {
406      adEncodeMap.put(ad, id);
407      if (id < adDecodeMap.size())
408      {
409        adDecodeMap.set(id, ad);
410      }
411      else
412      {
413        // Grow the decode array.
414        while (id > adDecodeMap.size())
415        {
416          adDecodeMap.add(null);
417        }
418        adDecodeMap.add(ad);
419      }
420    }
421    return ad;
422  }
423
424  private Set<String> getOptions(final Collection<String> attributeOptions)
425  {
426    switch (attributeOptions.size())
427    {
428    case 0:
429      return Collections.emptySet();
430    case 1:
431      return Collections.singleton(attributeOptions.iterator().next());
432    default:
433      return new LinkedHashSet<>(attributeOptions);
434    }
435  }
436
437  /**
438   * Loads an encoded object class into this compressed schema. This method may
439   * called by implementations during initialization when loading content from
440   * disk.
441   *
442   * @param encodedObjectClasses
443   *          The encoded object classes.
444   * @param objectClassNames
445   *          The user provided set of object class names.
446   * @return The object class set.
447   */
448  protected final Map<ObjectClass, String> loadObjectClasses(
449      final byte[] encodedObjectClasses,
450      final Collection<String> objectClassNames)
451  {
452    final LinkedHashMap<ObjectClass, String> ocMap = new LinkedHashMap<>(objectClassNames.size());
453    for (final String name : objectClassNames)
454    {
455      final String lowerName = toLowerCase(name);
456      final ObjectClass oc = DirectoryServer.getObjectClass(lowerName, true);
457      ocMap.put(oc, name);
458    }
459    final int id = decodeId(encodedObjectClasses);
460    synchronized (ocEncodeMap)
461    {
462      ocEncodeMap.put(ocMap, id);
463      if (id < ocDecodeMap.size())
464      {
465        ocDecodeMap.set(id, ocMap);
466      }
467      else
468      {
469        // Grow the decode array.
470        while (id > ocDecodeMap.size())
471        {
472          ocDecodeMap.add(null);
473        }
474        ocDecodeMap.add(ocMap);
475      }
476    }
477    return ocMap;
478  }
479
480
481
482  /**
483   * Persists the provided encoded attribute. The default implementation is to
484   * do nothing. Calls to this method are synchronized, so implementations can
485   * assume that this method is not being called by other threads. Note that
486   * this method is not thread-safe with respect to
487   * {@link #storeObjectClasses(byte[], Collection)}.
488   *
489   * @param encodedAttribute
490   *          The encoded attribute description.
491   * @param attributeName
492   *          The user provided attribute type name.
493   * @param attributeOptions
494   *          The non-null but possibly empty set of attribute options.
495   * @throws DirectoryException
496   *           If an error occurred while persisting the encoded attribute.
497   */
498  protected void storeAttribute(final byte[] encodedAttribute,
499      final String attributeName, final Collection<String> attributeOptions)
500      throws DirectoryException
501  {
502    // Do nothing by default.
503  }
504
505
506
507  /**
508   * Persists the provided encoded object classes. The default implementation is
509   * to do nothing. Calls to this method are synchronized, so implementations
510   * can assume that this method is not being called by other threads. Note that
511   * this method is not thread-safe with respect to
512   * {@link #storeAttribute(byte[], String, Collection)}.
513   *
514   * @param encodedObjectClasses
515   *          The encoded object classes.
516   * @param objectClassNames
517   *          The user provided set of object class names.
518   * @throws DirectoryException
519   *           If an error occurred while persisting the encoded object classes.
520   */
521  protected void storeObjectClasses(final byte[] encodedObjectClasses,
522      final Collection<String> objectClassNames) throws DirectoryException
523  {
524    // Do nothing by default.
525  }
526
527
528
529  /**
530   * Decodes the provided encoded schema element ID.
531   *
532   * @param idBytes
533   *          The encoded schema element ID.
534   * @return The schema element ID.
535   */
536  private int decodeId(final byte[] idBytes)
537  {
538    int id = 0;
539    for (final byte b : idBytes)
540    {
541      id <<= 8;
542      id |= b & 0xFF;
543    }
544    return id - 1; // Subtract 1 to compensate for old behavior.
545  }
546
547
548
549  /**
550   * Encodes the provided schema element ID.
551   *
552   * @param id
553   *          The schema element ID.
554   * @return The encoded schema element ID.
555   */
556  private byte[] encodeId(final int id)
557  {
558    final int value = id + 1; // Add 1 to compensate for old behavior.
559    final byte[] idBytes;
560    if (value <= 0xFF)
561    {
562      idBytes = new byte[1];
563      idBytes[0] = (byte) (value & 0xFF);
564    }
565    else if (value <= 0xFFFF)
566    {
567      idBytes = new byte[2];
568      idBytes[0] = (byte) ((value >> 8) & 0xFF);
569      idBytes[1] = (byte) (value & 0xFF);
570    }
571    else if (value <= 0xFFFFFF)
572    {
573      idBytes = new byte[3];
574      idBytes[0] = (byte) ((value >> 16) & 0xFF);
575      idBytes[1] = (byte) ((value >> 8) & 0xFF);
576      idBytes[2] = (byte) (value & 0xFF);
577    }
578    else
579    {
580      idBytes = new byte[4];
581      idBytes[0] = (byte) ((value >> 24) & 0xFF);
582      idBytes[1] = (byte) ((value >> 16) & 0xFF);
583      idBytes[2] = (byte) ((value >> 8) & 0xFF);
584      idBytes[3] = (byte) (value & 0xFF);
585    }
586    return idBytes;
587  }
588}