opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -21,6 +21,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; import java.util.WeakHashMap; import org.forgerock.i18n.LocalizableMessage; @@ -56,6 +57,9 @@ static final byte NORMALIZED_AVA_SEPARATOR = 0x01; static final byte NORMALIZED_ESC_BYTE = 0x02; static final char RDN_CHAR_SEPARATOR = ','; static final char AVA_CHAR_SEPARATOR = '+'; private static final DN ROOT_DN = new DN(CoreSchema.getInstance(), null, null, ""); /** @@ -191,15 +195,13 @@ } /** * Parses the provided LDAP string representation of a DN using the default * schema. * Parses the provided LDAP string representation of a DN using the default schema. * * @param dn * The LDAP string representation of a DN. * @return The parsed DN. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a * DN. * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. * @see #format(String, Object...) @@ -209,8 +211,7 @@ } /** * Parses the provided LDAP string representation of a DN using the provided * schema. * Parses the provided LDAP string representation of a DN using the provided schema. * * @param dn * The LDAP string representation of a DN. @@ -218,8 +219,7 @@ * The schema to use when parsing the DN. * @return The parsed DN. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP string representation of a * DN. * If {@code dn} is not a valid LDAP string representation of a DN. * @throws NullPointerException * If {@code dn} or {@code schema} was {@code null}. * @see #format(String, Schema, Object...) @@ -243,21 +243,18 @@ } /** * Compares the provided DN values to determine their relative order in a * sorted list. The order is the natural order as defined by the * {@code toNormalizedByteString()} method. * Parses the provided LDAP string representation of a DN using the default schema. * * @param dn1 * The first DN to be compared. It must not be {@code null}. * @param dn2 * The second DN to be compared. It must not be {@code null}. * @return A negative integer if the first DN should come before the second * DN in a sorted list, a positive integer if the first DN should * come after the second DN in a sorted list, or zero if the two DN * values can be considered equal. * @param dn * The LDAP byte string representation of a DN. * @return The parsed DN. * @throws LocalizedIllegalArgumentException * If {@code dn} is not a valid LDAP byte string representation of a DN. * @throws NullPointerException * If {@code dn} was {@code null}. */ private static int compareTo(final DN dn1, final DN dn2) { return dn1.toNormalizedByteString().compareTo(dn2.toNormalizedByteString()); public static DN valueOf(ByteString dn) { return DN.valueOf(dn.toString()); } /** Decodes a DN using the provided reader and schema. */ @@ -446,7 +443,7 @@ @Override public int compareTo(final DN dn) { return compareTo(this, dn); return toNormalizedByteString().compareTo(dn.toNormalizedByteString()); } @Override @@ -814,6 +811,29 @@ } /** * Returns the RDN at the specified index for this DN, * or {@code null} if no such RDN exists. * <p> * Here is an example usage: * <pre> * DN.valueOf("ou=people,dc=example,dc=com").rdn(0) => "ou=people" * DN.valueOf("ou=people,dc=example,dc=com").rdn(1) => "dc=example" * DN.valueOf("ou=people,dc=example,dc=com").rdn(2) => "dc=com" * DN.valueOf("ou=people,dc=example,dc=com").rdn(3) => null * </pre> * * @param index * The index of the requested RDN. * @return The RDN at the specified index, or {@code null} if no such RDN exists. * @throws IllegalArgumentException * If {@code index} is less than zero. */ public RDN rdn(int index) { DN parentDN = parent(index); return parentDN != null ? parentDN.rdn : null; } /** * Returns a copy of this DN whose parent DN, {@code fromDN}, has been * renamed to the new parent DN, {@code toDN}. If this DN is not subordinate * or equal to {@code fromDN} then this DN is returned (i.e. the DN is not @@ -863,7 +883,7 @@ final StringBuilder builder = new StringBuilder(); rdn.toString(builder); if (!parent.isRootDN()) { builder.append(','); builder.append(RDN_CHAR_SEPARATOR); builder.append(parent); } stringValue = builder.toString(); @@ -883,20 +903,13 @@ */ public ByteString toNormalizedByteString() { if (normalizedDN == null) { if (rdn() == null) { if (rdn == null) { normalizedDN = ByteString.empty(); } else { final ByteStringBuilder builder = new ByteStringBuilder(); int i = size() - 1; parent(i).rdn().toNormalizedByteString(builder); for (i--; i >= 0; i--) { final RDN rdn = parent(i).rdn(); // Only add a separator if the RDN is not RDN.maxValue(). if (rdn.size() != 0) { builder.appendByte(DN.NORMALIZED_RDN_SEPARATOR); } rdn.toNormalizedByteString(builder); } final ByteString normalizedParent = parent.toNormalizedByteString(); final ByteStringBuilder builder = new ByteStringBuilder(normalizedParent.length() + 16); builder.appendBytes(normalizedParent); rdn.toNormalizedByteString(builder); normalizedDN = builder.toByteString(); } } @@ -916,14 +929,17 @@ return ""; } // This code differs from toNormalizedByteString(), // because we do not care about ordering comparisons. // (so we do not append the RDN_SEPARATOR first) final StringBuilder builder = new StringBuilder(); int i = size() - 1; parent(i).rdn().toNormalizedUrlSafeString(builder); for (i--; i >= 0; i--) { final RDN rdn = parent(i).rdn(); // Only add a separator if the RDN is not RDN.maxValue(). // Only add a separator if the RDN is not RDN.maxValue() or RDN.minValue(). if (rdn.size() != 0) { builder.append(','); builder.append(RDN_CHAR_SEPARATOR); } rdn.toNormalizedUrlSafeString(builder); } @@ -931,6 +947,21 @@ } /** * Returns a UUID whose content is based on the normalized content of this DN. * Two equivalent DNs subject to the same schema will always yield the same UUID. * * @return the UUID representing this DN */ public UUID toUUID() { ByteString normDN = toNormalizedByteString(); if (!normDN.isEmpty()) { // remove leading RDN separator normDN = normDN.subSequence(1, normDN.length()); } return UUID.nameUUIDFromBytes(normDN.toByteArray()); } /** * A compact representation of a DN, suitable for equality and comparisons, and providing a natural hierarchical * ordering. * <p> opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
@@ -16,6 +16,11 @@ */ package org.forgerock.opendj.ldap; import static org.forgerock.opendj.ldap.DN.AVA_CHAR_SEPARATOR; import static org.forgerock.opendj.ldap.DN.RDN_CHAR_SEPARATOR; import static org.forgerock.opendj.ldap.DN.NORMALIZED_AVA_SEPARATOR; import static org.forgerock.opendj.ldap.DN.NORMALIZED_RDN_SEPARATOR; import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TYPE_NOT_FOUND; import java.util.ArrayList; @@ -30,10 +35,10 @@ import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; import org.forgerock.util.Reject; import com.forgerock.opendj.util.Iterators; import com.forgerock.opendj.util.SubstringReader; import org.forgerock.util.Reject; /** * A relative distinguished name (RDN) as defined in RFC 4512 section 2.3 is the @@ -63,18 +68,44 @@ */ public final class RDN implements Iterable<AVA>, Comparable<RDN> { /** Separator for AVAs. */ private static final char AVA_CHAR_SEPARATOR = '+'; /** * A constant holding a special RDN having zero AVAs and which always * compares greater than any other RDN other than itself. * A constant holding a special RDN having zero AVAs * and which sorts before any RDN other than itself. */ private static final RDN MIN_VALUE = new RDN(new AVA[0], ""); /** * A constant holding a special RDN having zero AVAs * and which sorts after any RDN other than itself. */ private static final RDN MAX_VALUE = new RDN(new AVA[0], ""); /** * Returns a constant containing a special RDN which is greater than any * other RDN other than itself. This RDN may be used in order to perform * Returns a constant containing a special RDN which sorts before any * RDN other than itself. This RDN may be used in order to perform * range queries on DN keyed collections such as {@code SortedSet}s and * {@code SortedMap}s. For example, the following code can be used to * construct a range whose contents is a sub-tree of entries, excluding the base entry: * * <pre> * SortedMap<DN, Entry> entries = ...; * DN baseDN = ...; * * // Returns a map containing the baseDN and all of its subordinates. * SortedMap<DN,Entry> subtree = entries.subMap( * baseDN.child(RDN.minValue()), baseDN.child(RDN.maxValue())); * </pre> * * @return A constant containing a special RDN which sorts before any * RDN other than itself. * @see #maxValue() */ public static RDN minValue() { return MIN_VALUE; } /** * Returns a constant containing a special RDN which sorts after any * RDN other than itself. This RDN may be used in order to perform * range queries on DN keyed collections such as {@code SortedSet}s and * {@code SortedMap}s. For example, the following code can be used to * construct a range whose contents is a sub-tree of entries: @@ -84,11 +115,12 @@ * DN baseDN = ...; * * // Returns a map containing the baseDN and all of its subordinates. * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue)); * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue())); * </pre> * * @return A constant containing a special RDN which is greater than any * other RDN other than itself. * @return A constant containing a special RDN which sorts after any * RDN other than itself. * @see #minValue() */ public static RDN maxValue() { return MAX_VALUE; @@ -244,6 +276,9 @@ @Override public int compareTo(final RDN rdn) { // FIXME how about replacing this method body with the following code? // return toNormalizedByteString().compareTo(rdn.toNormalizedByteString()) // Identity. if (this == rdn) { return 0; @@ -253,11 +288,18 @@ if (this == MAX_VALUE) { return 1; } if (rdn == MAX_VALUE) { return -1; } // MIN_VALUE is always less than any other RDN other than itself. if (this == MIN_VALUE) { return -1; } if (rdn == MIN_VALUE) { return 1; } // Compare number of AVAs first as this is quick and easy. final int sz1 = avas.length; final int sz2 = rdn.avas.length; @@ -348,6 +390,22 @@ } /** * Indicates whether this RDN includes the specified attribute type. * * @param attributeType The attribute type for which to make the determination. * @return {@code true} if the RDN includes the specified attribute type, * or {@code false} if not. */ public boolean hasAttributeType(AttributeType attributeType) { for (AVA ava : avas) { if (ava.getAttributeType().equals(attributeType)) { return true; } } return false; } /** * Returns an iterator of the AVAs contained in this RDN. The AVAs will be * returned in the user provided order. * <p> @@ -386,7 +444,7 @@ final StringBuilder builder = new StringBuilder(); avas[0].toString(builder); for (int i = 1; i < avas.length; i++) { builder.append('+'); builder.append(AVA_CHAR_SEPARATOR); avas[i].toString(builder); } stringValue = builder.toString(); @@ -411,17 +469,22 @@ ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) { switch (size()) { case 0: // Handle RDN.maxValue(). builder.appendByte(DN.NORMALIZED_AVA_SEPARATOR); if (this == MIN_VALUE) { builder.appendByte(NORMALIZED_RDN_SEPARATOR); } else { // can only be MAX_VALUE builder.appendByte(NORMALIZED_AVA_SEPARATOR); } break; case 1: builder.appendByte(NORMALIZED_RDN_SEPARATOR); getFirstAVA().toNormalizedByteString(builder); break; default: builder.appendByte(NORMALIZED_RDN_SEPARATOR); Iterator<AVA> it = getSortedAvas(); it.next().toNormalizedByteString(builder); while (it.hasNext()) { builder.appendByte(DN.NORMALIZED_AVA_SEPARATOR); builder.appendByte(NORMALIZED_AVA_SEPARATOR); it.next().toNormalizedByteString(builder); } break; @@ -441,8 +504,13 @@ StringBuilder toNormalizedUrlSafeString(final StringBuilder builder) { switch (size()) { case 0: // Handle RDN.maxValue(). builder.append(RDN.AVA_CHAR_SEPARATOR); // since MIN_VALUE and MAX_VALUE are only used for sorting DNs, // it is strange to call toNormalizedUrlSafeString() on one of them if (this == MIN_VALUE) { builder.append(RDN_CHAR_SEPARATOR); } else { // can only be MAX_VALUE builder.append(AVA_CHAR_SEPARATOR); } break; case 1: getFirstAVA().toNormalizedUrlSafe(builder); @@ -451,7 +519,7 @@ Iterator<AVA> it = getSortedAvas(); it.next().toNormalizedUrlSafe(builder); while (it.hasNext()) { builder.append(RDN.AVA_CHAR_SEPARATOR); builder.append(AVA_CHAR_SEPARATOR); it.next().toNormalizedUrlSafe(builder); } break; opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
@@ -12,17 +12,22 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2010 Sun Microsystems, Inc. * Portions copyright 2011-2015 ForgeRock AS. * Portions copyright 2011-2016 ForgeRock AS. */ package org.forgerock.opendj.ldap; import static java.lang.Integer.*; import static org.fest.assertions.Assertions.*; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.*; import static org.testng.Assert.*; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.TreeMap; import java.util.UUID; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.testng.annotations.DataProvider; @@ -594,18 +599,22 @@ dn.isChildOf((String) null); } /** * Tests the parent method that require iteration. */ /** Tests the parent and rdn method that require iteration. */ @Test public void testIterableParent() { public void testIterableParentAndRdn() { final String str = "ou=people,dc=example,dc=com"; final DN dn = DN.valueOf(str); // Parent at index 0 is self. assertEquals(dn, dn.parent(0)); assertEquals(dn.parent(0), dn); assertEquals(dn.parent(1), DN.valueOf("dc=example,dc=com")); assertEquals(dn.parent(2), DN.valueOf("dc=com")); assertEquals(dn.parent(3), DN.rootDN()); assertEquals(dn.parent(4), null); assertEquals(dn.rdn(0), RDN.valueOf("ou=people")); assertEquals(dn.rdn(1), RDN.valueOf("dc=example")); assertEquals(dn.rdn(2), RDN.valueOf("dc=com")); assertEquals(dn.rdn(3), null); } /** @@ -674,8 +683,6 @@ assertEquals(p.rdn(), RDN.valueOf("dc=bar")); assertEquals(p.rdn(), RDN.valueOf("dc=bar")); assertEquals(p.parent(), DN.valueOf("dc=opendj,dc=org")); assertEquals(p.parent(), e.parent()); @@ -719,15 +726,25 @@ } /** * Tests the root DN. * Tests {@link DN#valueOf(String)}. * * @throws Exception * If the test failed unexpectedly. */ @Test(expectedExceptions = { NullPointerException.class, AssertionError.class }) public void testRootDN2() throws Exception { final DN dn = DN.valueOf(null); assertEquals(dn, DN.rootDN()); public void testValueOfString() throws Exception { DN.valueOf((String) null); } /** * Tests {@link DN#valueOf(ByteString)}. * * @throws Exception * If the test failed unexpectedly. */ @Test(expectedExceptions = { NullPointerException.class, AssertionError.class }) public void testValueOfByteString() throws Exception { DN.valueOf((ByteString) null); } /** @@ -1030,7 +1047,7 @@ } @DataProvider public Object[][] toIrreversibleNormalizedByteStringDataProvider() { public Object[][] toNormalizedByteStringDataProvider() { // @formatter:off return new Object[][] { // first value to normalize, second value to normalize, expected sign of comparison between the two @@ -1038,6 +1055,13 @@ { "dc=example,dc=com", "dc=example,dc=com", 0 }, { "cn=test+dc=example,dc=com", "cn=test+dc=example,dc=com", 0 }, { "dc=example+cn=test,dc=com", "cn=test+dc=example,dc=com", 0 }, // siblings { "cn=test,dc=com", "cn=test+dc=example,dc=com", -1 }, { "cn=test+dc=example,dc=com", "cn=test,dc=com", 1 }, { "dc=example,dc=com", "cn=test+dc=example,dc=com", 1 }, { "cn=test+dc=example,dc=com", "dc=example,dc=com", -1 }, { "dc=example,dc=com", "dc=example+cn=test,dc=com", 1 }, { "dc=example+cn=test,dc=com", "dc=example,dc=com", -1 }, // parent entry is followed by its children, not its siblings { "dc=com", "dc=example,dc=com", -1 }, { "dc=com", "dc=test,dc=example,dc=com", -1}, @@ -1071,7 +1095,7 @@ // @formatter:on } @Test(dataProvider = "toIrreversibleNormalizedByteStringDataProvider") @Test(dataProvider = "toNormalizedByteStringDataProvider") public void testToNormalizedByteString(String first, String second, int expectedCompareResult) { DN actual = DN.valueOf(first); DN expected = DN.valueOf(second); @@ -1092,7 +1116,81 @@ } @DataProvider public Object[][] toIrreversibleReadableStringDataProvider() { private Object[][] minAndMaxRdnsDataProvider() { DN dcCom = DN.valueOf("dc=com"); DN dcExampleDcCom = DN.valueOf("dc=example,dc=com"); DN cnTestDcCom = DN.valueOf("cn=test,dc=com"); return new Object[][] { { dcCom, dcCom.child(RDN.minValue()), -1 }, { dcCom, dcCom.child(RDN.maxValue()), -1 }, { dcExampleDcCom, dcExampleDcCom.child(RDN.minValue()), -1 }, { dcExampleDcCom, dcExampleDcCom.child(RDN.maxValue()), -1 }, { dcExampleDcCom, dcCom.child(RDN.minValue()), 1 }, { dcExampleDcCom, dcCom.child(RDN.maxValue()), -1 }, // siblings { DN.valueOf("cn=test+dc=example,dc=com"), cnTestDcCom.child(RDN.minValue()), 1 }, { DN.valueOf("dc=example+cn=test,dc=com"), cnTestDcCom.child(RDN.minValue()), 1 }, { DN.valueOf("cn=test+dc=example,dc=com"), cnTestDcCom.child(RDN.maxValue()), 1 }, { DN.valueOf("dc=example+cn=test,dc=com"), cnTestDcCom.child(RDN.maxValue()), 1 }, }; } /** Using DN as a Map key depends on this behaviour. In particular MemoryBackend depends on this behaviour. */ @Test(dataProvider = "minAndMaxRdnsDataProvider") public void testToNormalizedByteStringWithMinAndMaxRdns(DN dn1, DN dn2, int expectedCompareResult) { int cmp = dn1.toNormalizedByteString().compareTo(dn2.toNormalizedByteString()); assertThat(signum(cmp)).isEqualTo(expectedCompareResult); } @Test public void testToNormalizedByteStringWithMinAndMaxRdnsInOrderedCollection() { DN dcCom = DN.valueOf("dc=com"); DN cnTestDcCom = DN.valueOf("cn=test,dc=com"); DN cnDeeperCnTestDcCom = DN.valueOf("cn=deeper,cn=test,dc=com"); DN cnTestAndDcExampleDcCom = DN.valueOf("cn=test+dc=example,dc=com"); DN dcExampleDcCom = DN.valueOf("dc=example,dc=com"); TreeMap<ByteString, DN> map = new TreeMap<>(); putAll(map, dcCom, cnTestDcCom, cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom); assertThat(subordinates(map, dcCom)) .containsExactly(cnTestDcCom, cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom); assertThat(subordinates(map, cnTestDcCom)) .containsExactly(cnDeeperCnTestDcCom); assertThat(after(map, cnTestDcCom)) .containsExactly(cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom); assertThat(after(map, cnDeeperCnTestDcCom)) .containsExactly(cnTestAndDcExampleDcCom, dcExampleDcCom); assertThat(before(map, cnTestDcCom)) .containsExactly(dcCom); assertThat(before(map, cnDeeperCnTestDcCom)) .containsExactly(dcCom, cnTestDcCom); } private void putAll(Map<ByteString, DN> map, DN... dns) { for (DN dn : dns) { map.put(dn.toNormalizedByteString(), dn); } } private Collection<DN> subordinates(TreeMap<ByteString, DN> map, DN dn) { return map.subMap( dn.child(RDN.minValue()).toNormalizedByteString(), dn.child(RDN.maxValue()).toNormalizedByteString()).values(); } private Collection<DN> before(TreeMap<ByteString, DN> map, DN dn) { return map.headMap(dn.toNormalizedByteString(), false).values(); } private Collection<DN> after(TreeMap<ByteString, DN> map, DN dn) { return map.tailMap(dn.toNormalizedByteString(), false).values(); } @DataProvider public Object[][] toNormalizedUrlSafeStringDataProvider() { // @formatter:off return new Object[][] { // first value = string used to build DN, second value = expected readable string @@ -1125,7 +1223,7 @@ // @formatter:on } @Test(dataProvider = "toIrreversibleReadableStringDataProvider") @Test(dataProvider = "toNormalizedUrlSafeStringDataProvider") public void testToNormalizedUrlSafeString(String dnAsString, String expectedReadableString) { DN actual = DN.valueOf(dnAsString); assertEquals(actual.toNormalizedUrlSafeString(), expectedReadableString); @@ -1141,4 +1239,11 @@ assertEquals(irreversibleReadableString, dn2.toNormalizedUrlSafeString()); assertEquals(irreversibleReadableString, dn3.toNormalizedUrlSafeString()); } @Test public void toUUID() { UUID uuid1 = DN.valueOf("dc=example+cn=test,dc=com").toUUID(); UUID uuid2 = DN.valueOf("cn=test+dc=example,dc=com").toUUID(); assertEquals(uuid1, uuid2); } } opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
@@ -21,6 +21,7 @@ import static org.testng.Assert.assertEquals; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ByteStringBuilder; import org.forgerock.opendj.ldap.ConditionResult; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -194,8 +195,14 @@ final MatchingRule rule = getRule(); final ByteString normalizedValue1 = rule.normalizeAttributeValue(ByteString.valueOfUtf8(value1)); final ByteString expectedValue = ByteString.valueOfUtf8(value2); final ByteString expectedValue = toExpectedNormalizedByteString(value2); assertEquals(normalizedValue1, expectedValue); } private ByteString toExpectedNormalizedByteString(final String s) { if (s.isEmpty()) { return ByteString.valueOfUtf8(s); } return new ByteStringBuilder().appendByte(0).appendUtf8(s).toByteString(); } }