/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2009 Sun Microsystems, Inc. * Portions Copyright 2013-2015 ForgeRock AS. */ package org.opends.server.api; import static org.opends.messages.CoreMessages.*; import static com.forgerock.opendj.util.StaticUtils.toLowerCase; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import org.opends.server.core.DirectoryServer; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeBuilder; import org.opends.server.types.AttributeType; import org.forgerock.opendj.ldap.ByteString; import org.opends.server.types.Attributes; import org.forgerock.opendj.ldap.ByteSequence; import org.forgerock.opendj.ldap.ByteSequenceReader; import org.forgerock.opendj.ldap.ByteStringBuilder; import org.opends.server.types.DirectoryException; import org.opends.server.types.ObjectClass; /** * This class provides a utility for interacting with compressed representations * of schema elements. The default implementation does not persist encoded * attributes and object classes. */ @org.opends.server.types.PublicAPI( stability = org.opends.server.types.StabilityLevel.UNCOMMITTED, mayInstantiate = false, mayExtend = true, mayInvoke = false) public class CompressedSchema { /** Maps attribute description to ID. */ private final List>> adDecodeMap = new CopyOnWriteArrayList<>(); /** Maps ID to attribute description. */ private final Map>, Integer> adEncodeMap = new ConcurrentHashMap<>(); /** The map between encoded representations and object class sets. */ private final List> ocDecodeMap = new CopyOnWriteArrayList<>(); /** The map between object class sets and encoded representations. */ private final Map, Integer> ocEncodeMap = new ConcurrentHashMap<>(); /** * Decodes the contents of the provided array as an attribute at the current * position. * * @param reader * The byte string reader containing the encoded entry. * @return The decoded attribute. * @throws DirectoryException * If the attribute could not be decoded properly for some reason. */ public final Attribute decodeAttribute(final ByteSequenceReader reader) throws DirectoryException { // First decode the encoded attribute description id. final int length = reader.readBERLength(); final byte[] idBytes = new byte[length]; reader.readBytes(idBytes); final int id = decodeId(idBytes); // Look up the attribute description. Entry> ad = adDecodeMap.get(id); if (ad == null) { throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_COMPRESSEDSCHEMA_UNRECOGNIZED_AD_TOKEN.get(id)); } // Before returning the attribute, make sure that the attribute type is not // stale. AttributeType attrType = ad.getKey(); Set options = ad.getValue(); if (attrType.isDirty()) { ad = loadAttribute(idBytes, attrType.getNameOrOID(), options); attrType = ad.getKey(); options = ad.getValue(); } // Determine the number of values for the attribute. final int numValues = reader.readBERLength(); // For the common case of a single value with no options, generate // less garbage. if (numValues == 1 && options.isEmpty()) { final int valueLength = reader.readBERLength(); final ByteSequence valueBytes = reader.readByteSequence(valueLength); return Attributes.create(attrType, valueBytes.toByteString()); } else { // Read the appropriate number of values. final AttributeBuilder builder = new AttributeBuilder(attrType); builder.setOptions(options); for (int i = 0; i < numValues; i++) { final int valueLength = reader.readBERLength(); final ByteSequence valueBytes = reader.readByteSequence(valueLength); builder.add(valueBytes.toByteString()); } return builder.toAttribute(); } } /** * Decodes an object class set from the provided byte string. * * @param reader * The byte string reader containing the object class set identifier. * @return The decoded object class set. * @throws DirectoryException * If the provided byte string reader cannot be decoded as an object * class set. */ public final Map decodeObjectClasses( final ByteSequenceReader reader) throws DirectoryException { // First decode the encoded object class id. final int length = reader.readBERLength(); final byte[] idBytes = new byte[length]; reader.readBytes(idBytes); final int id = decodeId(idBytes); // Look up the object classes. final Map ocMap = ocDecodeMap.get(id); if (ocMap != null) { // Before returning the object classes, make sure that none of them are // stale. for (final ObjectClass oc : ocMap.keySet()) { if (oc.isDirty()) { // Found at least one object class which is dirty so refresh them. return loadObjectClasses(idBytes, ocMap.values()); } } return ocMap; } else { throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_COMPRESSEDSCHEMA_UNKNOWN_OC_TOKEN.get(id)); } } /** * Encodes the information in the provided attribute to a byte array. * * @param builder * The buffer to encode the attribute to. * @param attribute * The attribute to be encoded. * @throws DirectoryException * If a problem occurs while attempting to determine the appropriate * identifier. */ public final void encodeAttribute(final ByteStringBuilder builder, final Attribute attribute) throws DirectoryException { // Re-use or allocate a new ID. final AttributeType type = attribute.getAttributeType(); final Set options = attribute.getOptions(); final Entry> ad = new SimpleImmutableEntry<>(type, options); // Use double checked locking to avoid lazy registration races. Integer id = adEncodeMap.get(ad); if (id == null) { synchronized (adEncodeMap) { id = adEncodeMap.get(ad); if (id == null) { id = adDecodeMap.size(); adDecodeMap.add(ad); adEncodeMap.put(ad, id); storeAttribute(encodeId(id), type.getNameOrOID(), options); } } } // Encode the attribute. final byte[] idBytes = encodeId(id); builder.appendBERLength(idBytes.length); builder.appendBytes(idBytes); builder.appendBERLength(attribute.size()); for (final ByteString v : attribute) { builder.appendBERLength(v.length()); builder.appendBytes(v); } } /** * Encodes the provided set of object classes to a byte array. If the same set * had been previously encoded, then the cached value will be used. Otherwise, * a new value will be created. * * @param builder * The buffer to encode the object classes to. * @param objectClasses * The set of object classes for which to retrieve the corresponding * byte array token. * @throws DirectoryException * If a problem occurs while attempting to determine the appropriate * identifier. */ public final void encodeObjectClasses(final ByteStringBuilder builder, final Map objectClasses) throws DirectoryException { // Re-use or allocate a new ID. // Use double checked locking to avoid lazy registration races. Integer id = ocEncodeMap.get(objectClasses); if (id == null) { synchronized (ocEncodeMap) { id = ocEncodeMap.get(objectClasses); if (id == null) { id = ocDecodeMap.size(); ocDecodeMap.add(objectClasses); ocEncodeMap.put(objectClasses, id); storeObjectClasses(encodeId(id), objectClasses.values()); } } } // Encode the object classes. final byte[] idBytes = encodeId(id); builder.appendBERLength(idBytes.length); builder.appendBytes(idBytes); } /** * Returns a view of the encoded attributes in this compressed schema which * can be used for saving the entire content to disk. The iterator returned by * this method is not thread safe. * * @return A view of the encoded attributes in this compressed schema. */ protected final Iterable>>> getAllAttributes() { return new Iterable>>>() { @Override public Iterator>>> iterator() { return new Iterator>>>() { private int id = 0; @Override public boolean hasNext() { return id < adDecodeMap.size(); } @Override public Entry>> next() { final byte[] encodedAttribute = encodeId(id); final Entry> ad = adDecodeMap.get(id++); return new SimpleImmutableEntry>>( encodedAttribute, new SimpleImmutableEntry>(ad .getKey().getNameOrOID(), ad.getValue())); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Returns a view of the encoded object classes in this compressed schema * which can be used for saving the entire content to disk. The iterator * returned by this method is not thread safe. * * @return A view of the encoded object classes in this compressed schema. */ protected final Iterable>> getAllObjectClasses() { return new Iterable>>() { @Override public Iterator>> iterator() { return new Iterator>>() { private int id = 0; @Override public boolean hasNext() { return id < ocDecodeMap.size(); } @Override public Entry> next() { final byte[] encodedObjectClasses = encodeId(id); final Map ocMap = ocDecodeMap.get(id++); return new SimpleImmutableEntry<>(encodedObjectClasses, ocMap.values()); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Loads an encoded attribute into this compressed schema. This method may * called by implementations during initialization when loading content from * disk. * * @param encodedAttribute * The encoded attribute description. * @param attributeName * The user provided attribute type name. * @param attributeOptions * The non-null but possibly empty set of attribute options. * @return The attribute type description. */ protected final Entry> loadAttribute( final byte[] encodedAttribute, final String attributeName, final Collection attributeOptions) { final AttributeType type = DirectoryServer.getAttributeTypeOrDefault(toLowerCase(attributeName)); final Set options = getOptions(attributeOptions); final Entry> ad = new SimpleImmutableEntry<>(type, options); final int id = decodeId(encodedAttribute); synchronized (adEncodeMap) { adEncodeMap.put(ad, id); if (id < adDecodeMap.size()) { adDecodeMap.set(id, ad); } else { // Grow the decode array. while (id > adDecodeMap.size()) { adDecodeMap.add(null); } adDecodeMap.add(ad); } } return ad; } private Set getOptions(final Collection attributeOptions) { switch (attributeOptions.size()) { case 0: return Collections.emptySet(); case 1: return Collections.singleton(attributeOptions.iterator().next()); default: return new LinkedHashSet<>(attributeOptions); } } /** * Loads an encoded object class into this compressed schema. This method may * called by implementations during initialization when loading content from * disk. * * @param encodedObjectClasses * The encoded object classes. * @param objectClassNames * The user provided set of object class names. * @return The object class set. */ protected final Map loadObjectClasses( final byte[] encodedObjectClasses, final Collection objectClassNames) { final LinkedHashMap ocMap = new LinkedHashMap<>(objectClassNames.size()); for (final String name : objectClassNames) { final String lowerName = toLowerCase(name); final ObjectClass oc = DirectoryServer.getObjectClass(lowerName, true); ocMap.put(oc, name); } final int id = decodeId(encodedObjectClasses); synchronized (ocEncodeMap) { ocEncodeMap.put(ocMap, id); if (id < ocDecodeMap.size()) { ocDecodeMap.set(id, ocMap); } else { // Grow the decode array. while (id > ocDecodeMap.size()) { ocDecodeMap.add(null); } ocDecodeMap.add(ocMap); } } return ocMap; } /** * Persists the provided encoded attribute. The default implementation is to * do nothing. Calls to this method are synchronized, so implementations can * assume that this method is not being called by other threads. Note that * this method is not thread-safe with respect to * {@link #storeObjectClasses(byte[], Collection)}. * * @param encodedAttribute * The encoded attribute description. * @param attributeName * The user provided attribute type name. * @param attributeOptions * The non-null but possibly empty set of attribute options. * @throws DirectoryException * If an error occurred while persisting the encoded attribute. */ protected void storeAttribute(final byte[] encodedAttribute, final String attributeName, final Collection attributeOptions) throws DirectoryException { // Do nothing by default. } /** * Persists the provided encoded object classes. The default implementation is * to do nothing. Calls to this method are synchronized, so implementations * can assume that this method is not being called by other threads. Note that * this method is not thread-safe with respect to * {@link #storeAttribute(byte[], String, Collection)}. * * @param encodedObjectClasses * The encoded object classes. * @param objectClassNames * The user provided set of object class names. * @throws DirectoryException * If an error occurred while persisting the encoded object classes. */ protected void storeObjectClasses(final byte[] encodedObjectClasses, final Collection objectClassNames) throws DirectoryException { // Do nothing by default. } /** * Decodes the provided encoded schema element ID. * * @param idBytes * The encoded schema element ID. * @return The schema element ID. */ private int decodeId(final byte[] idBytes) { int id = 0; for (final byte b : idBytes) { id <<= 8; id |= b & 0xFF; } return id - 1; // Subtract 1 to compensate for old behavior. } /** * Encodes the provided schema element ID. * * @param id * The schema element ID. * @return The encoded schema element ID. */ private byte[] encodeId(final int id) { final int value = id + 1; // Add 1 to compensate for old behavior. final byte[] idBytes; if (value <= 0xFF) { idBytes = new byte[1]; idBytes[0] = (byte) (value & 0xFF); } else if (value <= 0xFFFF) { idBytes = new byte[2]; idBytes[0] = (byte) ((value >> 8) & 0xFF); idBytes[1] = (byte) (value & 0xFF); } else if (value <= 0xFFFFFF) { idBytes = new byte[3]; idBytes[0] = (byte) ((value >> 16) & 0xFF); idBytes[1] = (byte) ((value >> 8) & 0xFF); idBytes[2] = (byte) (value & 0xFF); } else { idBytes = new byte[4]; idBytes[0] = (byte) ((value >> 24) & 0xFF); idBytes[1] = (byte) ((value >> 16) & 0xFF); idBytes[2] = (byte) ((value >> 8) & 0xFF); idBytes[3] = (byte) (value & 0xFF); } return idBytes; } }