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 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2013-2015 ForgeRock AS
026 */
027package org.opends.server.types;
028
029import java.io.UnsupportedEncodingException;
030import java.net.URLEncoder;
031import java.nio.CharBuffer;
032import java.nio.charset.Charset;
033import java.nio.charset.CharsetDecoder;
034import java.nio.charset.CodingErrorAction;
035import java.util.*;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.i18n.slf4j.LocalizedLogger;
039import org.forgerock.opendj.ldap.ByteString;
040import org.forgerock.opendj.ldap.ByteStringBuilder;
041import org.forgerock.opendj.ldap.DecodeException;
042import org.forgerock.opendj.ldap.ResultCode;
043import org.forgerock.opendj.ldap.schema.MatchingRule;
044import org.forgerock.util.Reject;
045import org.opends.server.core.DirectoryServer;
046
047import static org.opends.messages.CoreMessages.*;
048import static org.opends.server.util.StaticUtils.*;
049
050/**
051 * This class defines a data structure for storing and interacting
052 * with the relative distinguished names associated with entries in
053 * the Directory Server.
054 */
055@org.opends.server.types.PublicAPI(
056     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
057     mayInstantiate=true,
058     mayExtend=false,
059     mayInvoke=true)
060public final class RDN
061       implements Comparable<RDN>
062{
063  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
064
065  /** The set of attribute types for the elements in this RDN. */
066  private AttributeType[] attributeTypes;
067
068  /** The set of values for the elements in this RDN. */
069  private ByteString[] attributeValues;
070
071  /** The set of user-provided names for the attributes in this RDN. */
072  private String[] attributeNames;
073
074  /** Representation of the normalized form of this RDN. */
075  private ByteString normalizedRDN;
076
077
078  /**
079   * Creates a new RDN with the provided information.
080   *
081   * @param  attributeType   The attribute type for this RDN.  It must
082   *                         not be {@code null}.
083   * @param  attributeValue  The value for this RDN.  It must not be
084   *                         {@code null}.
085   */
086  @SuppressWarnings("unchecked")
087  public RDN(AttributeType attributeType, ByteString attributeValue)
088  {
089    Reject.ifNull(attributeType, attributeValue);
090    attributeTypes  = new AttributeType[] { attributeType };
091    attributeNames  = new String[] { attributeType.getPrimaryName() };
092    attributeValues = new ByteString[] { attributeValue };
093  }
094
095
096
097  /**
098   * Creates a new RDN with the provided information.
099   *
100   * @param  attributeType   The attribute type for this RDN.  It must
101   *                         not be {@code null}.
102   * @param  attributeName   The user-provided name for this RDN.  It
103   *                         must not be {@code null}.
104   * @param  attributeValue  The value for this RDN.  It must not be
105   *                         {@code null}.
106   */
107  @SuppressWarnings("unchecked")
108  public RDN(AttributeType attributeType, String attributeName, ByteString attributeValue)
109  {
110    Reject.ifNull(attributeType, attributeName, attributeValue);
111    attributeTypes  = new AttributeType[] { attributeType };
112    attributeNames  = new String[] { attributeName };
113    attributeValues = new ByteString[] { attributeValue };
114  }
115
116
117
118  /**
119   * Creates a new RDN with the provided information.  The number of
120   * type, name, and value elements must be nonzero and equal.
121   *
122   * @param  attributeTypes   The set of attribute types for this RDN.
123   *                          It must not be empty or {@code null}.
124   * @param  attributeNames   The set of user-provided names for this
125   *                          RDN.  It must have the same number of
126   *                          elements as the {@code attributeTypes}
127   *                          argument.
128   * @param  attributeValues  The set of values for this RDN.  It must
129   *                          have the same number of elements as the
130   *                          {@code attributeTypes} argument.
131   */
132  @SuppressWarnings("unchecked")
133  public RDN(List<AttributeType> attributeTypes,
134             List<String> attributeNames,
135             List<ByteString> attributeValues)
136  {
137    Reject.ifNull(attributeTypes, attributeNames, attributeValues);
138    Reject.ifTrue(attributeTypes.isEmpty(), "attributeTypes must not be empty");
139    Reject.ifFalse(attributeNames.size() == attributeTypes.size(),
140            "attributeNames must have the same number of elements than attributeTypes");
141    Reject.ifFalse(attributeValues.size() == attributeTypes.size(),
142            "attributeValues must have the same number of elements than attributeTypes");
143
144    this.attributeTypes  = new AttributeType[attributeTypes.size()];
145    this.attributeNames  = new String[attributeNames.size()];
146    this.attributeValues = new ByteString[attributeValues.size()];
147
148    attributeTypes.toArray(this.attributeTypes);
149    attributeNames.toArray(this.attributeNames);
150    attributeValues.toArray(this.attributeValues);
151  }
152
153
154
155  /**
156   * Creates a new RDN with the provided information.  The number of
157   * type, name, and value elements must be nonzero and equal.
158   *
159   * @param  attributeTypes   The set of attribute types for this RDN.
160   *                          It must not be empty or {@code null}.
161   * @param  attributeNames   The set of user-provided names for this
162   *                          RDN.  It must have the same number of
163   *                          elements as the {@code attributeTypes}
164   *                          argument.
165   * @param  attributeValues  The set of values for this RDN.  It must
166   *                          have the same number of elements as the
167   *                          {@code attributeTypes} argument.
168   */
169  @SuppressWarnings("unchecked")
170  public RDN(AttributeType[] attributeTypes, String[] attributeNames, ByteString[] attributeValues)
171  {
172    Reject.ifNull(attributeTypes, attributeNames, attributeValues);
173    Reject.ifFalse(attributeTypes.length > 0, "attributeTypes must not be empty");
174    Reject.ifFalse(attributeNames.length == attributeTypes.length,
175        "attributeNames must have the same number of elements than attributeTypes");
176    Reject.ifFalse(attributeValues.length == attributeTypes.length,
177        "attributeValues must have the same number of elements than attributeTypes");
178
179    this.attributeTypes = attributeTypes;
180    this.attributeNames = attributeNames;
181    this.attributeValues = attributeValues;
182  }
183
184
185
186  /**
187   * Creates a new RDN with the provided information.
188   *
189   * @param  attributeType   The attribute type for this RDN.  It must
190   *                         not be {@code null}.
191   * @param  attributeValue  The value for this RDN.  It must not be
192   *                         {@code null}.
193   *
194   * @return  The RDN created with the provided information.
195   */
196  public static RDN create(AttributeType attributeType, ByteString attributeValue)
197  {
198    return new RDN(attributeType, attributeValue);
199  }
200
201
202
203  /**
204   * Retrieves the number of attribute-value pairs contained in this
205   * RDN.
206   *
207   * @return  The number of attribute-value pairs contained in this
208   *          RDN.
209   */
210  public int getNumValues()
211  {
212    return attributeTypes.length;
213  }
214
215
216
217  /**
218   * Indicates whether this RDN includes the specified attribute type.
219   *
220   * @param  attributeType  The attribute type for which to make the
221   *                        determination.
222   *
223   * @return  <CODE>true</CODE> if the RDN includes the specified
224   *          attribute type, or <CODE>false</CODE> if not.
225   */
226  public boolean hasAttributeType(AttributeType attributeType)
227  {
228    for (AttributeType t : attributeTypes)
229    {
230      if (t.equals(attributeType))
231      {
232        return true;
233      }
234    }
235
236    return false;
237  }
238
239
240
241  /**
242   * Indicates whether this RDN includes the specified attribute type.
243   *
244   * @param  lowerName  The name or OID for the attribute type for
245   *                    which to make the determination, formatted in
246   *                    all lowercase characters.
247   *
248   * @return  <CODE>true</CODE> if the RDN includes the specified
249   *          attribute type, or <CODE>false</CODE> if not.
250   */
251  public boolean hasAttributeType(String lowerName)
252  {
253    for (AttributeType t : attributeTypes)
254    {
255      if (t.hasNameOrOID(lowerName))
256      {
257        return true;
258      }
259    }
260
261    for (String s : attributeNames)
262    {
263      if (s.equalsIgnoreCase(lowerName))
264      {
265        return true;
266      }
267    }
268
269    return false;
270  }
271
272
273
274  /**
275   * Retrieves the attribute type at the specified position in the set
276   * of attribute types for this RDN.
277   *
278   * @param  pos  The position of the attribute type to retrieve.
279   *
280   * @return  The attribute type at the specified position in the set
281   *          of attribute types for this RDN.
282   */
283  public AttributeType getAttributeType(int pos)
284  {
285    return attributeTypes[pos];
286  }
287
288
289
290  /**
291   * Retrieves the name for the attribute type at the specified
292   * position in the set of attribute types for this RDN.
293   *
294   * @param  pos  The position of the attribute type for which to
295   *              retrieve the name.
296   *
297   * @return  The name for the attribute type at the specified
298   *          position in the set of attribute types for this RDN.
299   */
300  public String getAttributeName(int pos)
301  {
302    return attributeNames[pos];
303  }
304
305
306
307  /**
308   * Retrieves the attribute value that is associated with the
309   * specified attribute type.
310   *
311   * @param  attributeType  The attribute type for which to retrieve
312   *                        the corresponding value.
313   *
314   * @return  The value for the requested attribute type, or
315   *          <CODE>null</CODE> if the specified attribute type is not
316   *          present in the RDN.
317   */
318  public ByteString getAttributeValue(AttributeType attributeType)
319  {
320    for (int i=0; i < attributeTypes.length; i++)
321    {
322      if (attributeTypes[i].equals(attributeType))
323      {
324        return attributeValues[i];
325      }
326    }
327
328    return null;
329  }
330
331
332
333  /**
334   * Retrieves the value for the attribute type at the specified
335   * position in the set of attribute types for this RDN.
336   *
337   * @param  pos  The position of the attribute type for which to
338   *              retrieve the value.
339   *
340   * @return  The value for the attribute type at the specified
341   *          position in the set of attribute types for this RDN.
342   */
343  public ByteString getAttributeValue(int pos)
344  {
345    return attributeValues[pos];
346  }
347
348
349
350  /**
351   * Indicates whether this RDN is multivalued.
352   *
353   * @return  <CODE>true</CODE> if this RDN is multivalued, or
354   *          <CODE>false</CODE> if not.
355   */
356  public boolean isMultiValued()
357  {
358    return attributeTypes.length > 1;
359  }
360
361
362
363  /**
364   * Indicates whether this RDN contains the specified type-value
365   * pair.
366   *
367   * @param  type   The attribute type for which to make the
368   *                determination.
369   * @param  value  The value for which to make the determination.
370   *
371   * @return  <CODE>true</CODE> if this RDN contains the specified
372   *          attribute value, or <CODE>false</CODE> if not.
373   */
374  public boolean hasValue(AttributeType type, ByteString value)
375  {
376    for (int i=0; i < attributeTypes.length; i++)
377    {
378      if (attributeTypes[i].equals(type) &&
379          attributeValues[i].equals(value))
380      {
381        return true;
382      }
383    }
384
385    return false;
386  }
387
388
389
390  /**
391   * Adds the provided type-value pair from this RDN.  Note that this
392   * is intended only for internal use when constructing DN values.
393   *
394   * @param  type   The attribute type of the pair to add.
395   * @param  name   The user-provided name of the pair to add.
396   * @param  value  The attribute value of the pair to add.
397   *
398   * @return  <CODE>true</CODE> if the type-value pair was added to
399   *          this RDN, or <CODE>false</CODE> if it was not (e.g., it
400   *          was already present).
401   */
402  boolean addValue(AttributeType type, String name, ByteString value)
403  {
404    int numValues = attributeTypes.length;
405    for (int i=0; i < numValues; i++)
406    {
407      if (attributeTypes[i].equals(type) &&
408          attributeValues[i].equals(value))
409      {
410        return false;
411      }
412    }
413    numValues++;
414    AttributeType[] newTypes = new AttributeType[numValues];
415    System.arraycopy(attributeTypes, 0, newTypes, 0, attributeTypes.length);
416    newTypes[attributeTypes.length] = type;
417    attributeTypes = newTypes;
418
419    String[] newNames = new String[numValues];
420    System.arraycopy(attributeNames, 0, newNames, 0, attributeNames.length);
421    newNames[attributeNames.length] = name;
422    attributeNames = newNames;
423
424    ByteString[] newValues = new ByteString[numValues];
425    System.arraycopy(attributeValues, 0, newValues, 0, attributeValues.length);
426    newValues[attributeValues.length] = value;
427    attributeValues = newValues;
428
429    return true;
430  }
431
432
433
434  /**
435   * Retrieves a version of the provided value in a form that is
436   * properly escaped for use in a DN or RDN.
437   *
438   * @param  valueBS  The value to be represented in a DN-safe form.
439   *
440   * @return  A version of the provided value in a form that is
441   *          properly escaped for use in a DN or RDN.
442   */
443  private static String getDNValue(ByteString valueBS) {
444    final String value = valueBS.toString();
445    if (value == null || value.length() == 0) {
446      return "";
447    }
448
449    // Only copy the string value if required.
450    boolean needsEscaping = false;
451    int length = value.length();
452
453    needsEscaping: {
454      char c = value.charAt(0);
455      if (c == ' ' || c == '#') {
456        needsEscaping = true;
457        break needsEscaping;
458      }
459
460      if (value.charAt(length - 1) == ' ') {
461        needsEscaping = true;
462        break needsEscaping;
463      }
464
465      for (int i = 0; i < length; i++) {
466        c = value.charAt(i);
467        if (c < ' ') {
468          needsEscaping = true;
469          break needsEscaping;
470        } else {
471          switch (c) {
472          case ',':
473          case '+':
474          case '"':
475          case '\\':
476          case '<':
477          case '>':
478          case ';':
479            needsEscaping = true;
480            break needsEscaping;
481          }
482        }
483      }
484    }
485
486    if (!needsEscaping) {
487      return value;
488    }
489
490    // We need to copy and escape the string (allow for at least one
491    // escaped character).
492    StringBuilder buffer = new StringBuilder(length + 3);
493
494    // If the lead character is a space or a # it must be escaped.
495    int start = 0;
496    char c = value.charAt(0);
497    if (c == ' ' || c == '#') {
498      buffer.append('\\');
499      buffer.append(c);
500      start = 1;
501    }
502
503    // Escape remaining characters as necessary.
504    for (int i = start; i < length; i++) {
505      c = value.charAt(i);
506      if (c < ' ') {
507        for (byte b : getBytes(String.valueOf(c))) {
508          buffer.append('\\');
509          buffer.append(byteToLowerHex(b));
510        }
511      } else {
512        switch (value.charAt(i)) {
513        case ',':
514        case '+':
515        case '"':
516        case '\\':
517        case '<':
518        case '>':
519        case ';':
520          buffer.append('\\');
521          buffer.append(c);
522          break;
523        default:
524          buffer.append(c);
525          break;
526        }
527      }
528    }
529
530    // If the last character is a space it must be escaped.
531    if (value.charAt(length - 1) == ' ') {
532      length = buffer.length();
533      buffer.insert(length - 1, '\\');
534    }
535
536    return buffer.toString();
537  }
538
539
540
541  /**
542   * Decodes the provided string as an RDN.
543   *
544   * @param rdnString
545   *          The string to decode as an RDN.
546   * @return The decoded RDN.
547   * @throws DirectoryException
548   *           If a problem occurs while trying to decode the provided
549   *           string as a RDN.
550   */
551  public static RDN decode(String rdnString) throws DirectoryException
552  {
553    // A null or empty RDN is not acceptable.
554    if (rdnString == null)
555    {
556      LocalizableMessage message = ERR_RDN_DECODE_NULL.get();
557      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
558    }
559
560    int length = rdnString.length();
561    if (length == 0)
562    {
563      LocalizableMessage message = ERR_RDN_DECODE_NULL.get();
564      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
565    }
566
567
568    // Iterate through the RDN string.  The first thing to do is to
569    // get rid of any leading spaces.
570    int pos = 0;
571    char c = rdnString.charAt(pos);
572    while (c == ' ')
573    {
574      pos++;
575      if (pos == length)
576      {
577        // This means that the RDN was completely comprised of spaces,
578        // which is not valid.
579        LocalizableMessage message = ERR_RDN_DECODE_NULL.get();
580        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
581      }
582      else
583      {
584        c = rdnString.charAt(pos);
585      }
586    }
587
588
589    // We know that it's not an empty RDN, so we can do the real processing.
590    // First, parse the attribute name. We can borrow the DN code for this.
591    boolean allowExceptions = DirectoryServer.allowAttributeNameExceptions();
592    StringBuilder attributeName = new StringBuilder();
593    pos = DN.parseAttributeName(rdnString, pos, attributeName, allowExceptions);
594
595
596    // Make sure that we're not at the end of the RDN string because
597    // that would be invalid.
598    if (pos >= length)
599    {
600      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
601          ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName));
602    }
603
604
605    // Skip over any spaces between the attribute name and its value.
606    c = rdnString.charAt(pos);
607    while (c == ' ')
608    {
609      pos++;
610      if (pos >= length)
611      {
612        // This means that we hit the end of the string before finding a '='.
613        // This is illegal because there is no attribute-value separator.
614        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
615            ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName));
616      }
617      else
618      {
619        c = rdnString.charAt(pos);
620      }
621    }
622
623
624    // The next character must be an equal sign.  If it is not, then
625    // that's an error.
626    if (c == '=')
627    {
628      pos++;
629    }
630    else
631    {
632      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
633          ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c));
634    }
635
636
637    // Skip over any spaces between the equal sign and the value.
638    while (pos < length && ((c = rdnString.charAt(pos)) == ' '))
639    {
640      pos++;
641    }
642
643
644    // If we are at the end of the RDN string, then that must mean
645    // that the attribute value was empty.
646    if (pos >= length)
647    {
648      String        name      = attributeName.toString();
649      String        lowerName = toLowerCase(name);
650      LocalizableMessage message = ERR_RDN_MISSING_ATTRIBUTE_VALUE.get(rdnString,
651             lowerName);
652      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
653    }
654
655
656    // Parse the value for this RDN component.  This can be done using
657    // the DN code.
658    ByteStringBuilder parsedValue = new ByteStringBuilder(0);
659    pos = DN.parseAttributeValue(rdnString, pos, parsedValue);
660
661
662    // Create the new RDN with the provided information.  However,
663    // don't return it yet because this could be a multi-valued RDN.
664    String name            = attributeName.toString();
665    String lowerName       = toLowerCase(name);
666    AttributeType attrType = DirectoryServer.getAttributeType(lowerName);
667    if (attrType == null)
668    {
669      // This must be an attribute type that we don't know about.
670      // In that case, we'll create a new attribute using the default
671      // syntax.  If this is a problem, it will be caught later either
672      // by not finding the target entry or by not allowing the entry
673      // to be added.
674      attrType = DirectoryServer.getDefaultAttributeType(name);
675    }
676
677    RDN rdn = new RDN(attrType, name, parsedValue.toByteString());
678
679
680    // Skip over any spaces that might be after the attribute value.
681    while (pos < length && ((c = rdnString.charAt(pos)) == ' '))
682    {
683      pos++;
684    }
685
686
687    // Most likely, this is the end of the RDN.  If so, then return it.
688    if (pos >= length)
689    {
690      return rdn;
691    }
692
693
694    // If the next character is a comma or semicolon, then that is not
695    // allowed.  It would be legal for a DN but not an RDN.
696    if (c == ',' || c == ';')
697    {
698      LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos);
699      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
700    }
701
702
703    // If the next character is anything but a plus sign, then it is illegal.
704    if (c != '+')
705    {
706      LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos);
707      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
708    }
709
710
711    // If we have gotten here, then it is a multi-valued RDN.  Parse
712    // the remaining attribute/value pairs and add them to the RDN
713    // that we've already created.
714    while (true)
715    {
716      // Skip over the plus sign and any spaces that may follow it
717      // before the next attribute name.
718      pos++;
719      while (pos < length && ((c = rdnString.charAt(pos)) == ' '))
720      {
721        pos++;
722      }
723
724
725      // Parse the attribute name.
726      attributeName = new StringBuilder();
727      pos = DN.parseAttributeName(rdnString, pos, attributeName,
728                                  allowExceptions);
729
730
731      // Make sure we're not at the end of the RDN.
732      if (pos >= length)
733      {
734        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
735            ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName));
736      }
737
738
739      // Skip over any spaces between the attribute name and the equal sign.
740      c = rdnString.charAt(pos);
741      while (c == ' ')
742      {
743        pos++;
744        if (pos >= length)
745        {
746          // This means that we hit the end of the string before finding a '='.
747          // This is illegal because there is no attribute-value separator.
748          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
749              ERR_RDN_END_WITH_ATTR_NAME.get(rdnString, attributeName));
750        }
751        else
752        {
753          c = rdnString.charAt(pos);
754        }
755      }
756
757
758      // The next character must be an equal sign.
759      if (c == '=')
760      {
761        pos++;
762      }
763      else
764      {
765        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
766            ERR_RDN_NO_EQUAL.get(rdnString, attributeName, c));
767      }
768
769
770      // Skip over any spaces after the equal sign.
771      while (pos < length && ((c = rdnString.charAt(pos)) == ' '))
772      {
773        pos++;
774      }
775
776
777      // If we are at the end of the RDN string, then that must mean
778      // that the attribute value was empty.  This will probably never
779      // happen in a real-world environment, but technically isn't
780      // illegal.  If it does happen, then go ahead and return the RDN.
781      if (pos >= length)
782      {
783        name      = attributeName.toString();
784        lowerName = toLowerCase(name);
785        attrType  = DirectoryServer.getAttributeType(lowerName);
786
787        if (attrType == null)
788        {
789          // This must be an attribute type that we don't know about.
790          // In that case, we'll create a new attribute using the
791          // default syntax.  If this is a problem, it will be caught
792          // later either by not finding the target entry or by not
793          // allowing the entry to be added.
794          attrType = DirectoryServer.getDefaultAttributeType(name);
795        }
796
797        rdn.addValue(attrType, name, ByteString.empty());
798        return rdn;
799      }
800
801
802      // Parse the value for this RDN component.
803      parsedValue.clear();
804      pos = DN.parseAttributeValue(rdnString, pos, parsedValue);
805
806
807      // Update the RDN to include the new attribute/value.
808      name            = attributeName.toString();
809      lowerName       = toLowerCase(name);
810      attrType = DirectoryServer.getAttributeType(lowerName);
811      if (attrType == null)
812      {
813        // This must be an attribute type that we don't know about.
814        // In that case, we'll create a new attribute using the
815        // default syntax.  If this is a problem, it will be caught
816        // later either by not finding the target entry or by not
817        // allowing the entry to be added.
818        attrType = DirectoryServer.getDefaultAttributeType(name);
819      }
820
821      rdn.addValue(attrType, name, parsedValue.toByteString());
822
823
824      // Skip over any spaces that might be after the attribute value.
825      while (pos < length && ((c = rdnString.charAt(pos)) == ' '))
826      {
827        pos++;
828      }
829
830
831      // If we're at the end of the string, then return the RDN.
832      if (pos >= length)
833      {
834        return rdn;
835      }
836
837
838      // If the next character is a comma or semicolon, then that is
839      // not allowed.  It would be legal for a DN but not an RDN.
840      if (c == ',' || c == ';')
841      {
842        LocalizableMessage message = ERR_RDN_UNEXPECTED_COMMA.get(rdnString, pos);
843        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
844      }
845
846
847      // If the next character is anything but a plus sign, then it is illegal.
848      if (c != '+')
849      {
850        LocalizableMessage message = ERR_RDN_ILLEGAL_CHARACTER.get(rdnString, c, pos);
851        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
852      }
853    }
854  }
855
856
857
858  /**
859   * Creates a duplicate of this RDN that can be modified without
860   * impacting this RDN.
861   *
862   * @return  A duplicate of this RDN that can be modified without
863   *          impacting this RDN.
864   */
865  public RDN duplicate()
866  {
867    int numValues = attributeTypes.length;
868    AttributeType[] newTypes = new AttributeType[numValues];
869    System.arraycopy(attributeTypes, 0, newTypes, 0, numValues);
870
871    String[] newNames = new String[numValues];
872    System.arraycopy(attributeNames, 0, newNames, 0, numValues);
873
874    ByteString[] newValues = new ByteString[numValues];
875    System.arraycopy(attributeValues, 0, newValues, 0, numValues);
876
877    return new RDN(newTypes, newNames, newValues);
878  }
879
880
881
882  /**
883   * Indicates whether the provided object is equal to this RDN.  It
884   * will only be considered equal if it is an RDN object that
885   * contains the same number of elements in the same order with the
886   * same types and normalized values.
887   *
888   * @param  o  The object for which to make the determination.
889   *
890   * @return  <CODE>true</CODE> if it is determined that the provided
891   *          object is equal to this RDN, or <CODE>false</CODE> if
892   *          not.
893   */
894  @Override
895  public boolean equals(Object o)
896  {
897    if (this == o)
898    {
899      return true;
900    }
901    if (o instanceof RDN)
902    {
903      RDN otherRDN = (RDN) o;
904      return toNormalizedByteString().equals(otherRDN.toNormalizedByteString());
905    }
906    return false;
907  }
908
909  /**
910   * Retrieves the hash code for this RDN.  It will be calculated as
911   * the sum of the hash codes of the types and values.
912   *
913   * @return  The hash code for this RDN.
914   */
915  @Override
916  public int hashCode()
917  {
918    return toNormalizedByteString().hashCode();
919  }
920
921  /** Returns normalized value for attribute at provided position. */
922  private ByteString getEqualityNormalizedValue(int position)
923  {
924    final MatchingRule matchingRule = attributeTypes[position].getEqualityMatchingRule();
925    ByteString attributeValue = attributeValues[position];
926    if (matchingRule != null)
927    {
928      try
929      {
930        attributeValue = matchingRule.normalizeAttributeValue(attributeValue);
931      }
932      catch (final DecodeException de)
933      {
934        // Unable to normalize, use default
935        attributeValue = attributeValues[position];
936      }
937    }
938    return attributeValue;
939  }
940
941
942
943  /**
944   * Retrieves a string representation of this RDN.
945   *
946   * @return  A string representation of this RDN.
947   */
948  @Override
949  public String toString()
950  {
951        StringBuilder buffer = new StringBuilder();
952        buffer.append(attributeNames[0]);
953        buffer.append("=");
954        buffer.append(getDNValue(attributeValues[0]));
955        for (int i = 1; i < attributeTypes.length; i++) {
956            buffer.append("+");
957            buffer.append(attributeNames[i]);
958            buffer.append("=");
959            buffer.append(getDNValue(attributeValues[i]));
960        }
961        return buffer.toString();
962  }
963
964  /**
965   * Appends a string representation of this RDN to the provided
966   * buffer.
967   *
968   * @param  buffer  The buffer to which the string representation
969   *                 should be appended.
970   */
971  public void toString(StringBuilder buffer)
972  {
973    buffer.append(this);
974  }
975
976 /**
977  * Retrieves a normalized string representation of this RDN.
978  * <p>
979  *
980  * This representation is safe to use in an URL or in a file name.
981  * However, it is not a valid RDN and can't be reverted to a valid RDN.
982  *
983  * @return  The normalized string representation of this RDN.
984  */
985  String toNormalizedUrlSafeString()
986  {
987    final StringBuilder buffer = new StringBuilder();
988    if (attributeNames.length == 1)
989    {
990      normalizeAVAToUrlSafeString(0, buffer);
991    }
992    else
993    {
994      // Normalization sorts RDNs alphabetically
995      SortedSet<String> avaStrings = new TreeSet<>();
996      for (int i=0; i < attributeNames.length; i++)
997      {
998        StringBuilder builder = new StringBuilder();
999        normalizeAVAToUrlSafeString(i, builder);
1000        avaStrings.add(builder.toString());
1001      }
1002
1003      Iterator<String> iterator = avaStrings.iterator();
1004      buffer.append(iterator.next());
1005      while (iterator.hasNext())
1006      {
1007        buffer.append('+');
1008        buffer.append(iterator.next());
1009      }
1010    }
1011    return buffer.toString();
1012  }
1013
1014  private ByteString toNormalizedByteString()
1015  {
1016    if (normalizedRDN == null)
1017    {
1018      computeNormalizedByteString(new ByteStringBuilder());
1019    }
1020    return normalizedRDN;
1021  }
1022
1023  /**
1024   * Adds a normalized byte string representation of this RDN to the provided builder.
1025   * <p>
1026   * The representation is suitable for equality and comparisons, and for providing a
1027   * natural hierarchical ordering.
1028   * However, it is not a valid RDN and can't be reverted to a valid RDN.
1029   *
1030   * @param builder
1031   *           Builder to add this representation to.
1032   * @return the builder
1033   */
1034  public ByteStringBuilder toNormalizedByteString(ByteStringBuilder builder)
1035  {
1036    if (normalizedRDN != null)
1037    {
1038      return builder.append(normalizedRDN);
1039    }
1040    return computeNormalizedByteString(builder);
1041  }
1042
1043  private ByteStringBuilder computeNormalizedByteString(ByteStringBuilder builder)
1044  {
1045    final int startPos = builder.length();
1046
1047    if (attributeNames.length == 1)
1048    {
1049      normalizeAVAToByteString(0, builder);
1050    }
1051    else
1052    {
1053      // Normalization sorts RDNs
1054      SortedSet<ByteString> avaStrings = new TreeSet<>();
1055      for (int i = 0; i < attributeNames.length; i++)
1056      {
1057        ByteStringBuilder b = new ByteStringBuilder();
1058        normalizeAVAToByteString(i, b);
1059        avaStrings.add(b.toByteString());
1060      }
1061
1062      Iterator<ByteString> iterator = avaStrings.iterator();
1063      builder.append(iterator.next());
1064      while (iterator.hasNext())
1065      {
1066        builder.append(DN.NORMALIZED_AVA_SEPARATOR);
1067        builder.append(iterator.next());
1068      }
1069    }
1070
1071    if (normalizedRDN == null)
1072    {
1073      normalizedRDN = builder.subSequence(startPos, builder.length()).toByteString();
1074    }
1075
1076    return builder;
1077  }
1078
1079  /**
1080   * Adds a normalized byte string representation of the AVA corresponding
1081   * to provided position in this RDN to the provided builder.
1082   *
1083   * @param position
1084   *           Position of AVA in this RDN.
1085   * @param builder
1086   *           Builder to add the representation to.
1087   * @return the builder
1088   */
1089  private ByteStringBuilder normalizeAVAToByteString(int position, final ByteStringBuilder builder)
1090  {
1091    builder.append(attributeTypes[position].getNormalizedPrimaryNameOrOID());
1092    builder.append("=");
1093    final ByteString value = getEqualityNormalizedValue(position);
1094    if (value.length() > 0)
1095    {
1096      builder.append(escapeBytes(value));
1097    }
1098    return builder;
1099  }
1100
1101  /**
1102   * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped.
1103   * <p>
1104   * These bytes are reserved to represent respectively the RDN separator, the
1105   * AVA separator and the escape byte in a normalized byte string.
1106   */
1107  private ByteString escapeBytes(final ByteString value)
1108  {
1109    if (!needEscaping(value))
1110    {
1111      return value;
1112    }
1113
1114    final ByteStringBuilder builder = new ByteStringBuilder();
1115    for (int i = 0; i < value.length(); i++)
1116    {
1117      final byte b = value.byteAt(i);
1118      if (isByteToEscape(b))
1119      {
1120        builder.append(DN.NORMALIZED_ESC_BYTE);
1121      }
1122      builder.append(b);
1123    }
1124    return builder.toByteString();
1125  }
1126
1127  private boolean needEscaping(final ByteString value)
1128  {
1129    for (int i = 0; i < value.length(); i++)
1130    {
1131      if (isByteToEscape(value.byteAt(i)))
1132      {
1133        return true;
1134      }
1135    }
1136    return false;
1137  }
1138
1139  private boolean isByteToEscape(final byte b)
1140  {
1141    return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE;
1142  }
1143
1144
1145  /**
1146   * Appends a normalized string representation of this RDN to the
1147   * provided buffer.
1148   *
1149   * @param  position  The position of the attribute type and value to
1150   *              retrieve.
1151   * @param  builder  The buffer to which to append the information.
1152   * @return the builder
1153   */
1154  private StringBuilder normalizeAVAToUrlSafeString(int position, StringBuilder builder)
1155  {
1156      builder.append(attributeTypes[position].getNormalizedPrimaryNameOrOID());
1157      builder.append('=');
1158
1159      ByteString value = getEqualityNormalizedValue(position);
1160      if (value.length() == 0)
1161      {
1162        return builder;
1163      }
1164      final boolean hasAttributeName = attributeTypes[position].getPrimaryName() != null;
1165      final boolean isHumanReadable = attributeTypes[position].getSyntax().isHumanReadable();
1166      if (!hasAttributeName || !isHumanReadable)
1167      {
1168        builder.append(value.toPercentHexString());
1169      }
1170      else
1171      {
1172        // try to decode value as UTF-8 string
1173        final CharBuffer buffer = CharBuffer.allocate(value.length());
1174        final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder()
1175            .onMalformedInput(CodingErrorAction.REPORT)
1176            .onUnmappableCharacter(CodingErrorAction.REPORT);
1177        if (value.copyTo(buffer, decoder))
1178        {
1179          try
1180          {
1181            // URL encoding encodes space char as '+' instead of using hex code
1182            final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20");
1183            builder.append(val);
1184          }
1185          catch (UnsupportedEncodingException e)
1186          {
1187            // should never happen
1188            builder.append(value.toPercentHexString());
1189          }
1190        }
1191        else
1192        {
1193          builder.append(value.toPercentHexString());
1194        }
1195      }
1196      return builder;
1197  }
1198
1199  /**
1200   * Compares this RDN with the provided RDN based on natural ordering defined
1201   * by the toNormalizedByteString() method.
1202   *
1203   * @param  rdn  The RDN against which to compare this RDN.
1204   *
1205   * @return  A negative integer if this RDN should come before the
1206   *          provided RDN, a positive integer if this RDN should come
1207   *          after the provided RDN, or zero if there is no
1208   *          difference with regard to ordering.
1209   */
1210  @Override
1211  public int compareTo(RDN rdn)
1212  {
1213    return toNormalizedByteString().compareTo(rdn.toNormalizedByteString());
1214  }
1215
1216}