/* * 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 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2015 ForgeRock AS */ package org.opends.server.types; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.util.*; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ByteStringBuilder; import org.forgerock.opendj.ldap.DecodeException; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.schema.MatchingRule; import org.forgerock.util.Reject; import org.opends.server.core.DirectoryServer; import static org.opends.messages.CoreMessages.*; import static com.forgerock.opendj.util.StaticUtils.*; /** * This class defines a data structure for storing and interacting * with the relative distinguished names associated with entries in * the Directory Server. */ @org.opends.server.types.PublicAPI( stability=org.opends.server.types.StabilityLevel.UNCOMMITTED, mayInstantiate=true, mayExtend=false, mayInvoke=true) public final class RDN implements Comparable<RDN> { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The set of attribute types for the elements in this RDN. */ private AttributeType[] attributeTypes; /** The set of values for the elements in this RDN. */ private ByteString[] attributeValues; /** The set of user-provided names for the attributes in this RDN. */ private String[] attributeNames; /** Representation of the normalized form of this RDN. */ private ByteString normalizedRDN; /** * Creates a new RDN with the provided information. * * @param attributeType The attribute type for this RDN. It must * not be {@code null}. * @param attributeValue The value for this RDN. It must not be * {@code null}. */ @SuppressWarnings("unchecked") public RDN(AttributeType attributeType, ByteString attributeValue) { Reject.ifNull(attributeType, attributeValue); attributeTypes = new AttributeType[] { attributeType }; attributeNames = new String[] { attributeType.getPrimaryName() }; attributeValues = new ByteString[] { attributeValue }; } /** * Creates a new RDN with the provided information. * * @param attributeType The attribute type for this RDN. It must * not be {@code null}. * @param attributeName The user-provided name for this RDN. It * must not be {@code null}. * @param attributeValue The value for this RDN. It must not be * {@code null}. */ @SuppressWarnings("unchecked") public RDN(AttributeType attributeType, String attributeName, ByteString attributeValue) { Reject.ifNull(attributeType, attributeName, attributeValue); attributeTypes = new AttributeType[] { attributeType }; attributeNames = new String[] { attributeName }; attributeValues = new ByteString[] { attributeValue }; } /** * Creates a new RDN with the provided information. The number of * type, name, and value elements must be nonzero and equal. * * @param attributeTypes The set of attribute types for this RDN. * It must not be empty or {@code null}. * @param attributeNames The set of user-provided names for this * RDN. It must have the same number of * elements as the {@code attributeTypes} * argument. * @param attributeValues The set of values for this RDN. It must * have the same number of elements as the * {@code attributeTypes} argument. */ @SuppressWarnings("unchecked") public RDN(List<AttributeType> attributeTypes, List<String> attributeNames, List<ByteString> attributeValues) { Reject.ifNull(attributeTypes, attributeNames, attributeValues); Reject.ifTrue(attributeTypes.isEmpty(), "attributeTypes must not be empty"); Reject.ifFalse(attributeNames.size() == attributeTypes.size(), "attributeNames must have the same number of elements than attributeTypes"); Reject.ifFalse(attributeValues.size() == attributeTypes.size(), "attributeValues must have the same number of elements than attributeTypes"); this.attributeTypes = new AttributeType[attributeTypes.size()]; this.attributeNames = new String[attributeNames.size()]; this.attributeValues = new ByteString[attributeValues.size()]; attributeTypes.toArray(this.attributeTypes); attributeNames.toArray(this.attributeNames); attributeValues.toArray(this.attributeValues); } /** * Creates a new RDN with the provided information. The number of * type, name, and value elements must be nonzero and equal. * * @param attributeTypes The set of attribute types for this RDN. * It must not be empty or {@code null}. * @param attributeNames The set of user-provided names for this * RDN. It must have the same number of * elements as the {@code attributeTypes} * argument. * @param attributeValues The set of values for this RDN. It must * have the same number of elements as the * {@code attributeTypes} argument. */ @SuppressWarnings("unchecked") public RDN(AttributeType[] attributeTypes, String[] attributeNames, ByteString[] attributeValues) { Reject.ifNull(attributeTypes, attributeNames, attributeValues); Reject.ifFalse(attributeTypes.length > 0, "attributeTypes must not be empty"); Reject.ifFalse(attributeNames.length == attributeTypes.length, "attributeNames must have the same number of elements than attributeTypes"); Reject.ifFalse(attributeValues.length == attributeTypes.length, "attributeValues must have the same number of elements than attributeTypes"); this.attributeTypes = attributeTypes; this.attributeNames = attributeNames; this.attributeValues = attributeValues; } /** * Creates a new RDN with the provided information. * * @param attributeType The attribute type for this RDN. It must * not be {@code null}. * @param attributeValue The value for this RDN. It must not be * {@code null}. * * @return The RDN created with the provided information. */ public static RDN create(AttributeType attributeType, ByteString attributeValue) { return new RDN(attributeType, attributeValue); } /** * Retrieves the number of attribute-value pairs contained in this * RDN. * * @return The number of attribute-value pairs contained in this * RDN. */ public int getNumValues() { return attributeTypes.length; } /** * Indicates whether this RDN includes the specified attribute type. * * @param attributeType The attribute type for which to make the * determination. * * @return <CODE>true</CODE> if the RDN includes the specified * attribute type, or <CODE>false</CODE> if not. */ public boolean hasAttributeType(AttributeType attributeType) { for (AttributeType t : attributeTypes) { if (t.equals(attributeType)) { return true; } } return false; } /** * Indicates whether this RDN includes the specified attribute type. * * @param lowerName The name or OID for the attribute type for * which to make the determination, formatted in * all lowercase characters. * * @return <CODE>true</CODE> if the RDN includes the specified * attribute type, or <CODE>false</CODE> if not. */ public boolean hasAttributeType(String lowerName) { for (AttributeType t : attributeTypes) { if (t.hasNameOrOID(lowerName)) { return true; } } for (String s : attributeNames) { if (s.equalsIgnoreCase(lowerName)) { return true; } } return false; } /** * Retrieves the attribute type at the specified position in the set * of attribute types for this RDN. * * @param pos The position of the attribute type to retrieve. * * @return The attribute type at the specified position in the set * of attribute types for this RDN. */ public AttributeType getAttributeType(int pos) { return attributeTypes[pos]; } /** * Retrieves the name for the attribute type at the specified * position in the set of attribute types for this RDN. * * @param pos The position of the attribute type for which to * retrieve the name. * * @return The name for the attribute type at the specified * position in the set of attribute types for this RDN. */ public String getAttributeName(int pos) { return attributeNames[pos]; } /** * Retrieves the attribute value that is associated with the * specified attribute type. * * @param attributeType The attribute type for which to retrieve * the corresponding value. * * @return The value for the requested attribute type, or * <CODE>null</CODE> if the specified attribute type is not * present in the RDN. */ public ByteString getAttributeValue(AttributeType attributeType) { for (int i=0; i < attributeTypes.length; i++) { if (attributeTypes[i].equals(attributeType)) { return attributeValues[i]; } } return null; } /** * Retrieves the value for the attribute type at the specified * position in the set of attribute types for this RDN. * * @param pos The position of the attribute type for which to * retrieve the value. * * @return The value for the attribute type at the specified * position in the set of attribute types for this RDN. */ public ByteString getAttributeValue(int pos) { return attributeValues[pos]; } /** * Indicates whether this RDN is multivalued. * * @return <CODE>true</CODE> if this RDN is multivalued, or * <CODE>false</CODE> if not. */ public boolean isMultiValued() { return attributeTypes.length > 1; } /** * Indicates whether this RDN contains the specified type-value * pair. * * @param type The attribute type for which to make the * determination. * @param value The value for which to make the determination. * * @return <CODE>true</CODE> if this RDN contains the specified * attribute value, or <CODE>false</CODE> if not. */ public boolean hasValue(AttributeType type, ByteString value) { for (int i=0; i < attributeTypes.length; i++) { if (attributeTypes[i].equals(type) && attributeValues[i].equals(value)) { return true; } } return false; } /** * Adds the provided type-value pair from this RDN. Note that this * is intended only for internal use when constructing DN values. * * @param type The attribute type of the pair to add. * @param name The user-provided name of the pair to add. * @param value The attribute value of the pair to add. * * @return <CODE>true</CODE> if the type-value pair was added to * this RDN, or <CODE>false</CODE> if it was not (e.g., it * was already present). */ boolean addValue(AttributeType type, String name, ByteString value) { int numValues = attributeTypes.length; for (int i=0; i < numValues; i++) { if (attributeTypes[i].equals(type) && attributeValues[i].equals(value)) { return false; } } numValues++; AttributeType[] newTypes = new AttributeType[numValues]; System.arraycopy(attributeTypes, 0, newTypes, 0, attributeTypes.length); newTypes[attributeTypes.length] = type; attributeTypes = newTypes; String[] newNames = new String[numValues]; System.arraycopy(attributeNames, 0, newNames, 0, attributeNames.length); newNames[attributeNames.length] = name; attributeNames = newNames; ByteString[] newValues = new ByteString[numValues]; System.arraycopy(attributeValues, 0, newValues, 0, attributeValues.length); newValues[attributeValues.length] = value; attributeValues = newValues; return true; } /** * Retrieves a version of the provided value in a form that is * properly escaped for use in a DN or RDN. * * @param valueBS The value to be represented in a DN-safe form. * * @return A version of the provided value in a form that is * properly escaped for use in a DN or RDN. */ private static String getDNValue(ByteString valueBS) { final String value = valueBS.toString(); if (value == null || value.length() == 0) { return ""; } // Only copy the string value if required. boolean needsEscaping = false; int length = value.length(); needsEscaping: { char c = value.charAt(0); if (c == ' ' || c == '#') { needsEscaping = true; break needsEscaping; } if (value.charAt(length - 1) == ' ') { needsEscaping = true; break needsEscaping; } for (int i = 0; i < length; i++) { c = value.charAt(i); if (c < ' ') { needsEscaping = true; break needsEscaping; } else { switch (c) { case ',': case '+': case '"': case '\\': case '<': case '>': case ';': needsEscaping = true; break needsEscaping; } } } } if (!needsEscaping) { return value; } // We need to copy and escape the string (allow for at least one // escaped character). StringBuilder buffer = new StringBuilder(length + 3); // If the lead character is a space or a # it must be escaped. int start = 0; char c = value.charAt(0); if (c == ' ' || c == '#') { buffer.append('\\'); buffer.append(c); if (length == 1) { return buffer.toString(); } start = 1; } // Escape remaining characters as necessary. for (int i = start; i < length; i++) { c = value.charAt(i); if (c < ' ') { for (byte b : getBytes(String.valueOf(c))) { buffer.append('\\'); buffer.append(byteToLowerHex(b)); } } else { switch (value.charAt(i)) { case ',': case '+': case '"': case '\\': case '<': case '>': case ';': buffer.append('\\'); buffer.append(c); break; default: buffer.append(c); break; } } } // If the last character is a space it must be escaped. if (value.charAt(length - 1) == ' ') { length = buffer.length(); buffer.insert(length - 1, '\\'); } return buffer.toString(); } /** * Decodes the provided string as an RDN. * * @param rdnString * The string to decode as an RDN. * @return The decoded RDN. * @throws DirectoryException * If a problem occurs while trying to decode the provided * string as a RDN. */ public static RDN decode(String rdnString) throws DirectoryException { // A null or empty RDN is not acceptable. if (rdnString == null) { LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } int length = rdnString.length(); if (length == 0) { LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } // Iterate through the RDN string. The first thing to do is to // get rid of any leading spaces. int pos = 0; char c = rdnString.charAt(pos); while (c == ' ') { pos++; if (pos == length) { // This means that the RDN was completely comprised of spaces, // which is not valid. LocalizableMessage message = ERR_RDN_DECODE_NULL.get(); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } else { c = rdnString.charAt(pos); } } // We know that it's not an empty RDN, so we can do the real processing. // First, parse the attribute name. We can borrow the DN code for this. boolean allowExceptions = DirectoryServer.allowAttributeNameExceptions(); StringBuilder attributeName = new StringBuilder(); pos = DN.parseAttributeName(rdnString, pos, attributeName, allowExceptions); // Make sure that we're not at the end of the RDN string because // that would be invalid. if (pos >= length) { throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); } // Skip over any spaces between the attribute name and its value. c = rdnString.charAt(pos); while (c == ' ') { pos++; if (pos >= length) { // This means that we hit the end of the string before finding a '='. // This is illegal because there is no attribute-value separator. throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); } else { c = rdnString.charAt(pos); } } // The next character must be an equal sign. If it is not, then // that's an error. if (c == '=') { pos++; } else { throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c)); } // Skip over any spaces between the equal sign and the value. while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) { pos++; } // If we are at the end of the RDN string, then that must mean // that the attribute value was empty. if (pos >= length) { String name = attributeName.toString(); String lowerName = toLowerCase(name); LocalizableMessage message = ERR_RDN_MISSING_ATTRIBUTE_VALUE.get(rdnString, lowerName); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } // Parse the value for this RDN component. This can be done using // the DN code. ByteStringBuilder parsedValue = new ByteStringBuilder(0); pos = DN.parseAttributeValue(rdnString, pos, parsedValue); // Create the new RDN with the provided information. However, // don't return it yet because this could be a multi-valued RDN. String name = attributeName.toString(); String lowerName = toLowerCase(name); AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(lowerName); if (attrType == null) { // This must be an attribute type that we don't know about. // In that case, we'll create a new attribute using the default // syntax. If this is a problem, it will be caught later either // by not finding the target entry or by not allowing the entry // to be added. attrType = DirectoryServer.getAttributeTypeOrDefault(name); } RDN rdn = new RDN(attrType, name, parsedValue.toByteString()); // Skip over any spaces that might be after the attribute value. while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) { pos++; } // Most likely, this is the end of the RDN. If so, then return it. if (pos >= length) { return rdn; } // If the next character is a comma or semicolon, then that is not // allowed. It would be legal for a DN but not an RDN. if (c == ',' || c == ';') { LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } // If the next character is anything but a plus sign, then it is illegal. if (c != '+') { LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } // If we have gotten here, then it is a multi-valued RDN. Parse // the remaining attribute/value pairs and add them to the RDN // that we've already created. while (true) { // Skip over the plus sign and any spaces that may follow it // before the next attribute name. pos++; while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) { pos++; } // Parse the attribute name. attributeName = new StringBuilder(); pos = DN.parseAttributeName(rdnString, pos, attributeName, allowExceptions); // Make sure we're not at the end of the RDN. if (pos >= length) { throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); } // Skip over any spaces between the attribute name and the equal sign. c = rdnString.charAt(pos); while (c == ' ') { pos++; if (pos >= length) { // This means that we hit the end of the string before finding a '='. // This is illegal because there is no attribute-value separator. throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName)); } else { c = rdnString.charAt(pos); } } // The next character must be an equal sign. if (c == '=') { pos++; } else { throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c)); } // Skip over any spaces after the equal sign. while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) { pos++; } // If we are at the end of the RDN string, then that must mean // that the attribute value was empty. This will probably never // happen in a real-world environment, but technically isn't // illegal. If it does happen, then go ahead and return the RDN. if (pos >= length) { name = attributeName.toString(); lowerName = toLowerCase(name); attrType = DirectoryServer.getAttributeTypeOrNull(lowerName); if (attrType == null) { // This must be an attribute type that we don't know about. // In that case, we'll create a new attribute using the // default syntax. If this is a problem, it will be caught // later either by not finding the target entry or by not // allowing the entry to be added. attrType = DirectoryServer.getAttributeTypeOrDefault(name); } rdn.addValue(attrType, name, ByteString.empty()); return rdn; } // Parse the value for this RDN component. parsedValue.clear(); pos = DN.parseAttributeValue(rdnString, pos, parsedValue); // Update the RDN to include the new attribute/value. name = attributeName.toString(); lowerName = toLowerCase(name); attrType = DirectoryServer.getAttributeTypeOrNull(lowerName); if (attrType == null) { // This must be an attribute type that we don't know about. // In that case, we'll create a new attribute using the // default syntax. If this is a problem, it will be caught // later either by not finding the target entry or by not // allowing the entry to be added. attrType = DirectoryServer.getAttributeTypeOrDefault(name); } rdn.addValue(attrType, name, parsedValue.toByteString()); // Skip over any spaces that might be after the attribute value. while (pos < length && ((c = rdnString.charAt(pos)) == ' ')) { pos++; } // If we're at the end of the string, then return the RDN. if (pos >= length) { return rdn; } // If the next character is a comma or semicolon, then that is // not allowed. It would be legal for a DN but not an RDN. if (c == ',' || c == ';') { LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } // If the next character is anything but a plus sign, then it is illegal. if (c != '+') { LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos); throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); } } } /** * Creates a duplicate of this RDN that can be modified without * impacting this RDN. * * @return A duplicate of this RDN that can be modified without * impacting this RDN. */ public RDN duplicate() { int numValues = attributeTypes.length; AttributeType[] newTypes = new AttributeType[numValues]; System.arraycopy(attributeTypes, 0, newTypes, 0, numValues); String[] newNames = new String[numValues]; System.arraycopy(attributeNames, 0, newNames, 0, numValues); ByteString[] newValues = new ByteString[numValues]; System.arraycopy(attributeValues, 0, newValues, 0, numValues); return new RDN(newTypes, newNames, newValues); } /** * Indicates whether the provided object is equal to this RDN. It * will only be considered equal if it is an RDN object that * contains the same number of elements in the same order with the * same types and normalized values. * * @param o The object for which to make the determination. * * @return <CODE>true</CODE> if it is determined that the provided * object is equal to this RDN, or <CODE>false</CODE> if * not. */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof RDN) { RDN otherRDN = (RDN) o; return toNormalizedByteString().equals(otherRDN.toNormalizedByteString()); } return false; } /** * Retrieves the hash code for this RDN. It will be calculated as * the sum of the hash codes of the types and values. * * @return The hash code for this RDN. */ @Override public int hashCode() { return toNormalizedByteString().hashCode(); } /** Returns normalized value for attribute at provided position. */ private ByteString getEqualityNormalizedValue(int position) { final MatchingRule matchingRule = attributeTypes[position].getEqualityMatchingRule(); ByteString attributeValue = attributeValues[position]; if (matchingRule != null) { try { attributeValue = matchingRule.normalizeAttributeValue(attributeValue); } catch (final DecodeException de) { // Unable to normalize, use default attributeValue = attributeValues[position]; } } return attributeValue; } /** * Retrieves a string representation of this RDN. * * @return A string representation of this RDN. */ @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append(attributeNames[0]); buffer.append("="); buffer.append(getDNValue(attributeValues[0])); for (int i = 1; i < attributeTypes.length; i++) { buffer.append("+"); buffer.append(attributeNames[i]); buffer.append("="); buffer.append(getDNValue(attributeValues[i])); } return buffer.toString(); } /** * Appends a string representation of this RDN to the provided * buffer. * * @param buffer The buffer to which the string representation * should be appended. */ public void toString(StringBuilder buffer) { buffer.append(this); } /** * Retrieves a normalized string representation of this RDN. * <p> * * This representation is safe to use in an URL or in a file name. * However, it is not a valid RDN and can't be reverted to a valid RDN. * * @return The normalized string representation of this RDN. */ String toNormalizedUrlSafeString() { final StringBuilder buffer = new StringBuilder(); if (attributeNames.length == 1) { normalizeAVAToUrlSafeString(0, buffer); } else { // Normalization sorts RDNs alphabetically SortedSet<String> avaStrings = new TreeSet<>(); for (int i=0; i < attributeNames.length; i++) { StringBuilder builder = new StringBuilder(); normalizeAVAToUrlSafeString(i, builder); avaStrings.add(builder.toString()); } Iterator<String> iterator = avaStrings.iterator(); buffer.append(iterator.next()); while (iterator.hasNext()) { buffer.append('+'); buffer.append(iterator.next()); } } return buffer.toString(); } private ByteString toNormalizedByteString() { if (normalizedRDN == null) { computeNormalizedByteString(new ByteStringBuilder()); } return normalizedRDN; } /** * Adds a normalized byte string representation of this RDN to the provided builder. * <p> * The representation is suitable for equality and comparisons, and for providing a * natural hierarchical ordering. * However, it is not a valid RDN and can't be reverted to a valid RDN. * * @param builder * Builder to add this representation to. * @return the builder */ public ByteStringBuilder toNormalizedByteString(ByteStringBuilder builder) { if (normalizedRDN != null) { return builder.appendBytes(normalizedRDN); } return computeNormalizedByteString(builder); } private ByteStringBuilder computeNormalizedByteString(ByteStringBuilder builder) { final int startPos = builder.length(); if (attributeNames.length == 1) { normalizeAVAToByteString(0, builder); } else { // Normalization sorts RDNs SortedSet<ByteString> avaStrings = new TreeSet<>(); for (int i = 0; i < attributeNames.length; i++) { ByteStringBuilder b = new ByteStringBuilder(); normalizeAVAToByteString(i, b); avaStrings.add(b.toByteString()); } Iterator<ByteString> iterator = avaStrings.iterator(); builder.appendBytes(iterator.next()); while (iterator.hasNext()) { builder.appendByte(DN.NORMALIZED_AVA_SEPARATOR); builder.appendBytes(iterator.next()); } } if (normalizedRDN == null) { normalizedRDN = builder.subSequence(startPos, builder.length()).toByteString(); } return builder; } /** * Adds a normalized byte string representation of the AVA corresponding * to provided position in this RDN to the provided builder. * * @param position * Position of AVA in this RDN. * @param builder * Builder to add the representation to. * @return the builder */ private ByteStringBuilder normalizeAVAToByteString(int position, final ByteStringBuilder builder) { builder.appendUtf8(attributeTypes[position].getNormalizedPrimaryNameOrOID()); builder.appendUtf8("="); final ByteString value = getEqualityNormalizedValue(position); if (value.length() > 0) { builder.appendBytes(escapeBytes(value)); } return builder; } /** * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped. * <p> * These bytes are reserved to represent respectively the RDN separator, the * AVA separator and the escape byte in a normalized byte string. */ private ByteString escapeBytes(final ByteString value) { if (!needEscaping(value)) { return value; } final ByteStringBuilder builder = new ByteStringBuilder(); for (int i = 0; i < value.length(); i++) { final byte b = value.byteAt(i); if (isByteToEscape(b)) { builder.appendByte(DN.NORMALIZED_ESC_BYTE); } builder.appendByte(b); } return builder.toByteString(); } private boolean needEscaping(final ByteString value) { for (int i = 0; i < value.length(); i++) { if (isByteToEscape(value.byteAt(i))) { return true; } } return false; } private boolean isByteToEscape(final byte b) { return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE; } /** * Appends a normalized string representation of this RDN to the * provided buffer. * * @param position The position of the attribute type and value to * retrieve. * @param builder The buffer to which to append the information. * @return the builder */ private StringBuilder normalizeAVAToUrlSafeString(int position, StringBuilder builder) { builder.append(attributeTypes[position].getNormalizedPrimaryNameOrOID()); builder.append('='); ByteString value = getEqualityNormalizedValue(position); if (value.length() == 0) { return builder; } final boolean hasAttributeName = attributeTypes[position].getPrimaryName() != null; final boolean isHumanReadable = attributeTypes[position].getSyntax().isHumanReadable(); if (!hasAttributeName || !isHumanReadable) { builder.append(value.toPercentHexString()); } else { // try to decode value as UTF-8 string final CharBuffer buffer = CharBuffer.allocate(value.length()); final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT); if (value.copyTo(buffer, decoder)) { buffer.flip(); try { // URL encoding encodes space char as '+' instead of using hex code final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20"); builder.append(val); } catch (UnsupportedEncodingException e) { // should never happen builder.append(value.toPercentHexString()); } } else { builder.append(value.toPercentHexString()); } } return builder; } /** * Compares this RDN with the provided RDN based on natural ordering defined * by the toNormalizedByteString() method. * * @param rdn The RDN against which to compare this RDN. * * @return A negative integer if this RDN should come before the * provided RDN, a positive integer if this RDN should come * after the provided RDN, or zero if there is no * difference with regard to ordering. */ @Override public int compareTo(RDN rdn) { return toNormalizedByteString().compareTo(rdn.toNormalizedByteString()); } }