/* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 2010 Sun Microsystems, Inc. */ package org.opends.sdk; import static com.sun.opends.sdk.messages.Messages.*; import static com.sun.opends.sdk.util.StaticUtils.*; import org.opends.sdk.schema.*; import com.sun.opends.sdk.util.StaticUtils; import com.sun.opends.sdk.util.SubstringReader; import com.sun.opends.sdk.util.Validator; /** * An attribute value assertion (AVA) as defined in RFC 4512 section 2.3 * consists of an attribute description with zero options and an attribute * value. *

* The following are examples of string representations of AVAs: * *

 * uid=12345
 * ou=Engineering
 * cn=Kurt Zeilenga
 * 
* * @see RFC 4512 - * Lightweight Directory Access Protocol (LDAP): Directory Information * Models */ public final class AVA implements Comparable { /** * Parses the provided LDAP string representation of an AVA using the default * schema. * * @param ava * The LDAP string representation of an AVA. * @return The parsed RDN. * @throws LocalizedIllegalArgumentException * If {@code ava} is not a valid LDAP string representation of a * AVA. * @throws NullPointerException * If {@code ava} was {@code null}. */ public static AVA valueOf(final String ava) throws LocalizedIllegalArgumentException, NullPointerException { return valueOf(ava, Schema.getDefaultSchema()); } /** * Parses the provided LDAP string representation of an AVA using the provided * schema. * * @param ava * The LDAP string representation of a AVA. * @param schema * The schema to use when parsing the AVA. * @return The parsed AVA. * @throws LocalizedIllegalArgumentException * If {@code ava} is not a valid LDAP string representation of a * AVA. * @throws NullPointerException * If {@code ava} or {@code schema} was {@code null}. */ public static AVA valueOf(final String ava, final Schema schema) throws LocalizedIllegalArgumentException, NullPointerException { final SubstringReader reader = new SubstringReader(ava); try { return decode(reader, schema); } catch (final UnknownSchemaElementException e) { final LocalizableMessage message = ERR_RDN_TYPE_NOT_FOUND.get(ava, e .getMessageObject()); throw new LocalizedIllegalArgumentException(message); } } static AVA decode(final SubstringReader reader, final Schema schema) throws LocalizedIllegalArgumentException, UnknownSchemaElementException { // Skip over any spaces at the beginning. reader.skipWhitespaces(); final AttributeType attribute = readAttributeName(reader, schema); // Make sure that we're not at the end of the DN string because // that would be invalid. if (reader.remaining() == 0) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME .get(reader.getString(), attribute.getNameOrOID()); throw new LocalizedIllegalArgumentException(message); } // Skip over any spaces if we have. reader.skipWhitespaces(); // The next character must be an equal sign. If it is not, then // that's an error. char c; if ((c = reader.read()) != '=') { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_NO_EQUAL.get(reader .getString(), attribute.getNameOrOID(), c); throw new LocalizedIllegalArgumentException(message); } // Skip over any spaces after the equal sign. reader.skipWhitespaces(); // Parse the value for this RDN component. final ByteString value = readAttributeValue(reader); return new AVA(attribute, value); } private static void appendHexChars(final SubstringReader reader, final StringBuilder valueBuffer, final StringBuilder hexBuffer) throws DecodeException { final int length = hexBuffer.length(); if (length == 0) { return; } if ((length % 2) != 0) { final LocalizableMessage message = ERR_HEX_DECODE_INVALID_LENGTH .get(hexBuffer); DecodeException.error(message); } int pos = 0; final int arrayLength = (length / 2); final byte[] hexArray = new byte[arrayLength]; for (int i = 0; i < arrayLength; i++) { switch (hexBuffer.charAt(pos++)) { case '0': hexArray[i] = 0x00; break; case '1': hexArray[i] = 0x10; break; case '2': hexArray[i] = 0x20; break; case '3': hexArray[i] = 0x30; break; case '4': hexArray[i] = 0x40; break; case '5': hexArray[i] = 0x50; break; case '6': hexArray[i] = 0x60; break; case '7': hexArray[i] = 0x70; break; case '8': hexArray[i] = (byte) 0x80; break; case '9': hexArray[i] = (byte) 0x90; break; case 'A': case 'a': hexArray[i] = (byte) 0xA0; break; case 'B': case 'b': hexArray[i] = (byte) 0xB0; break; case 'C': case 'c': hexArray[i] = (byte) 0xC0; break; case 'D': case 'd': hexArray[i] = (byte) 0xD0; break; case 'E': case 'e': hexArray[i] = (byte) 0xE0; break; case 'F': case 'f': hexArray[i] = (byte) 0xF0; break; default: final LocalizableMessage message = ERR_HEX_DECODE_INVALID_CHARACTER .get(hexBuffer, hexBuffer.charAt(pos - 1)); throw DecodeException.error(message); } switch (hexBuffer.charAt(pos++)) { case '0': // No action required. break; case '1': hexArray[i] |= 0x01; break; case '2': hexArray[i] |= 0x02; break; case '3': hexArray[i] |= 0x03; break; case '4': hexArray[i] |= 0x04; break; case '5': hexArray[i] |= 0x05; break; case '6': hexArray[i] |= 0x06; break; case '7': hexArray[i] |= 0x07; break; case '8': hexArray[i] |= 0x08; break; case '9': hexArray[i] |= 0x09; break; case 'A': case 'a': hexArray[i] |= 0x0A; break; case 'B': case 'b': hexArray[i] |= 0x0B; break; case 'C': case 'c': hexArray[i] |= 0x0C; break; case 'D': case 'd': hexArray[i] |= 0x0D; break; case 'E': case 'e': hexArray[i] |= 0x0E; break; case 'F': case 'f': hexArray[i] |= 0x0F; break; default: final LocalizableMessage message = ERR_HEX_DECODE_INVALID_CHARACTER .get(hexBuffer, hexBuffer.charAt(pos - 1)); throw DecodeException.error(message); } } try { valueBuffer.append(new String(hexArray, "UTF-8")); } catch (final Exception e) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE .get(reader.getString(), String.valueOf(e)); throw DecodeException.error(message); } // Clean up the hex buffer. hexBuffer.setLength(0); } private static ByteString delimitAndEvaluateEscape( final SubstringReader reader) throws DecodeException { char c = '\u0000'; final StringBuilder valueBuffer = new StringBuilder(); final StringBuilder hexBuffer = new StringBuilder(); reader.skipWhitespaces(); boolean escaped = false; while (reader.remaining() > 0) { c = reader.read(); if (escaped) { // This character is escaped. if (isHexDigit(c)) { // Unicode characters. if (!(reader.remaining() > 0)) { final LocalizableMessage msg = ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID .get(reader.getString()); DecodeException.error(msg); } // Check the next byte for hex. final char c2 = reader.read(); if (isHexDigit(c2)) { hexBuffer.append(c); hexBuffer.append(c2); // We may be at the end. if (reader.remaining() == 0) { appendHexChars(reader, valueBuffer, hexBuffer); } } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID .get(reader.getString()); DecodeException.error(message); } } else { appendHexChars(reader, valueBuffer, hexBuffer); valueBuffer.append(c); } escaped = false; } else if (c == 0x5C) // The backslash character { // We found an escape. escaped = true; } else { // Check for delimited chars. if (c == '+' || c == ',' || c == ';') { reader.reset(); // Return what we have got here so far. appendHexChars(reader, valueBuffer, hexBuffer); return ByteString.valueOf(valueBuffer.toString()); } // It is definitely not a delimiter at this point. appendHexChars(reader, valueBuffer, hexBuffer); valueBuffer.append(c); // reader.mark(); } reader.mark(); } reader.reset(); return ByteString.valueOf(valueBuffer.toString()); } private static AttributeType readAttributeName(final SubstringReader reader, final Schema schema) throws LocalizedIllegalArgumentException, UnknownSchemaElementException { int length = 1; reader.mark(); // The next character must be either numeric (for an OID) or // alphabetic (for an attribute description). char c = reader.read(); if (isDigit(c)) { boolean lastWasPeriod = false; while (reader.remaining() > 0 && (c = reader.read()) != '=') { if (c == '.') { if (lastWasPeriod) { final LocalizableMessage message = ERR_ATTR_SYNTAX_OID_CONSECUTIVE_PERIODS .get(reader.getString(), reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } else { lastWasPeriod = true; } } else if (!isDigit(c)) { // This must have been an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_OID_ILLEGAL_CHARACTER .get(reader.getString(), reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } else { lastWasPeriod = false; } length++; } } else if (isAlpha(c)) { // This must be an attribute description. In this case, we will // only accept alphabetic characters, numeric digits, and the hyphen. while (reader.remaining() > 0) { c = reader.read(); if (length == 0 && !isAlpha(c)) { // This is an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR .get(reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } if (c == '=') { // End of the attribute. break; } else if (c == ' ') { // Got a whitespace.It MUST be the end of the attribute // Make sure that the next non-whitespace character is '='. reader.skipWhitespaces(); // Read back the next char. c = reader.read(); if (c == '=') { break; } else { // This is an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR .get(reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } } else if (!isAlpha(c) && !isDigit(c) && c != '-') { // This is an illegal character. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR .get(reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } length++; } } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR .get(reader.getString(), c, reader.pos() - 1); throw new LocalizedIllegalArgumentException(message); } reader.reset(); // Return the position of the first non-space character after the // token. return schema.getAttributeType(reader.read(length)); } private static ByteString readAttributeValue(final SubstringReader reader) throws LocalizedIllegalArgumentException { // All leading spaces have already been stripped so we can start // reading the value. However, it may be empty so check for that. if (reader.remaining() == 0) { return ByteString.empty(); } reader.mark(); // Look at the first character. If it is an octothorpe (#), then // that means that the value should be a hex string. char c = reader.read(); int length = 0; if (c == '#') { // The first two characters must be hex characters. reader.mark(); if (reader.remaining() < 2) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT .get(reader.getString()); throw new LocalizedIllegalArgumentException(message); } for (int i = 0; i < 2; i++) { c = reader.read(); if (isHexDigit(c)) { length++; } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT .get(reader.getString(), c); throw new LocalizedIllegalArgumentException(message); } } // The rest of the value must be a multiple of two hex // characters. The end of the value may be designated by the // end of the DN, a comma or semicolon, or a space. while (reader.remaining() > 0) { c = reader.read(); if (isHexDigit(c)) { length++; if (reader.remaining() > 0) { c = reader.read(); if (isHexDigit(c)) { length++; } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT .get(reader.getString(), c); throw new LocalizedIllegalArgumentException(message); } } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT .get(reader.getString()); throw new LocalizedIllegalArgumentException(message); } } else if ((c == ' ') || (c == ',') || (c == ';')) { // This denotes the end of the value. break; } else { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT .get(reader.getString(), c); throw new LocalizedIllegalArgumentException(message); } } // At this point, we should have a valid hex string. Convert it // to a byte array and set that as the value of the provided // octet string. try { reader.reset(); return ByteString.wrap(hexStringToByteArray(reader.read(length))); } catch (final Exception e) { final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE .get(reader.getString(), String.valueOf(e)); throw new LocalizedIllegalArgumentException(message); } } // If the first character is a quotation mark, then the value // should continue until the corresponding closing quotation mark. else if (c == '"') { reader.mark(); while (true) { if (reader.remaining() <= 0) { // We hit the end of the AVA before the closing quote. // That's an error. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE .get(reader.getString()); throw new LocalizedIllegalArgumentException(message); } if (reader.read() == '"') { // This is the end of the value. break; } length++; } reader.reset(); final ByteString retString = ByteString.valueOf(reader.read(length)); reader.read(); return retString; } // Otherwise, use general parsing to find the end of the value. else { reader.reset(); ByteString bytes; try { bytes = delimitAndEvaluateEscape(reader); } catch (final DecodeException e) { throw new LocalizedIllegalArgumentException(e.getMessageObject()); } if (bytes.length() == 0) { // We don't allow an empty attribute value. final LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR .get(reader.getString(), reader.pos()); throw new LocalizedIllegalArgumentException(message); } return bytes; } } private final AttributeType attributeType; private final ByteString attributeValue; /** * Creates a new attribute value assertion (AVA) using the provided attribute * type and value. * * @param attributeType * The attribute type. * @param attributeValue * The attribute value. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was {@code * null}. */ public AVA(final AttributeType attributeType, final ByteString attributeValue) throws NullPointerException { Validator.ensureNotNull(attributeType, attributeValue); this.attributeType = attributeType; this.attributeValue = attributeValue; } /** * Creates a new attribute value assertion (AVA) using the provided attribute * type and value decoded using the default schema. *

* If {@code attributeValue} is not an instance of {@code ByteString} then it * will be converted using the {@link ByteString#valueOf(Object)} method. * * @param attributeType * The attribute type. * @param attributeValue * The attribute value. * @throws UnknownSchemaElementException * If {@code attributeType} was not found in the default schema. * @throws NullPointerException * If {@code attributeType} or {@code attributeValue} was {@code * null}. */ public AVA(final String attributeType, final Object attributeValue) throws UnknownSchemaElementException, NullPointerException { Validator.ensureNotNull(attributeType, attributeValue); this.attributeType = Schema.getDefaultSchema().getAttributeType( attributeType); this.attributeValue = ByteString.valueOf(attributeValue); } /** * {@inheritDoc} */ public int compareTo(final AVA ava) { int result = attributeType.compareTo(ava.attributeType); if (result != 0) { return result > 0 ? 1 : -1; } final ByteString normalizedValue = getNormalizeValue(); final MatchingRule rule = attributeType.getOrderingMatchingRule(); try { if (rule != null) { // Check equality assertion first. final Assertion lteAssertion = rule .getLessOrEqualAssertion(ava.attributeValue); final ConditionResult lteResult = lteAssertion.matches(normalizedValue); final Assertion gteAssertion = rule .getGreaterOrEqualAssertion(ava.attributeValue); final ConditionResult gteResult = gteAssertion.matches(normalizedValue); if (lteResult.equals(gteResult)) { // it is equal to the assertion value. return 0; } else if (lteResult == ConditionResult.TRUE) { return -1; } else { return 1; } } } catch (final DecodeException de) { // use the bytestring comparison as default. } if (result == 0) { final ByteString nv1 = normalizedValue; final ByteString nv2 = ava.getNormalizeValue(); result = nv1.compareTo(nv2); } return result; } /** * {@inheritDoc} */ @Override public boolean equals(final Object obj) { if (this == obj) { return true; } else if (obj instanceof AVA) { return compareTo((AVA) obj) == 0; } else { return false; } } /** * Returns the attribute type associated with this AVA. * * @return The attribute type associated with this AVA. */ public AttributeType getAttributeType() { return attributeType; } /** * Returns the attribute value associated with this AVA. * * @return The attribute value associated with this AVA. */ public ByteString getAttributeValue() { return attributeValue; } /** * {@inheritDoc} */ @Override public int hashCode() { return attributeType.hashCode() * 31 + getNormalizeValue().hashCode(); } /** * {@inheritDoc} */ @Override public String toString() { final StringBuilder builder = new StringBuilder(); return toString(builder).toString(); } private ByteString getNormalizeValue() { final MatchingRule matchingRule = attributeType.getEqualityMatchingRule(); if (matchingRule != null) { try { return matchingRule.normalizeAttributeValue(attributeValue); } catch (final DecodeException de) { // Ignore - we'll drop back to the user provided value. } } return attributeValue; } StringBuilder toString(final StringBuilder builder) { if (!attributeType.getNames().iterator().hasNext()) { builder.append(attributeType.getOID()); builder.append("=#"); StaticUtils.toHex(attributeValue, builder); } else { final String name = attributeType.getNameOrOID(); builder.append(name); builder.append("="); final Syntax syntax = attributeType.getSyntax(); if (!syntax.isHumanReadable()) { builder.append("#"); StaticUtils.toHex(attributeValue, builder); } else { final String str = attributeValue.toString(); if (str.length() == 0) { return builder; } char c = str.charAt(0); int startPos = 0; if ((c == ' ') || (c == '#')) { builder.append('\\'); builder.append(c); startPos = 1; } final int length = str.length(); for (int si = startPos; si < length; si++) { c = str.charAt(si); if (c < ' ') { for (final byte b : getBytes(String.valueOf(c))) { builder.append('\\'); builder.append(StaticUtils.byteToLowerHex(b)); } } else { if ((c == ' ' && si == length - 1) || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<' || c == '=' || c == '>' || c == '\\' || c == '\u0000')) { builder.append('\\'); } builder.append(c); } } } } return builder; } }