001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.io.Serializable;
026import java.nio.ByteBuffer;
027import java.util.ArrayList;
028import java.util.Comparator;
029import java.util.Map;
030import java.util.TreeMap;
031
032import com.unboundid.asn1.ASN1OctetString;
033import com.unboundid.ldap.matchingrules.MatchingRule;
034import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
035import com.unboundid.ldap.sdk.schema.Schema;
036import com.unboundid.util.Debug;
037import com.unboundid.util.NotMutable;
038import com.unboundid.util.StaticUtils;
039import com.unboundid.util.ThreadSafety;
040import com.unboundid.util.ThreadSafetyLevel;
041import com.unboundid.util.Validator;
042
043import static com.unboundid.ldap.sdk.LDAPMessages.*;
044
045
046
047/**
048 * This class provides a data structure for holding information about an LDAP
049 * relative distinguished name (RDN).  An RDN consists of one or more
050 * attribute name-value pairs.  See
051 * <A HREF="http://www.ietf.org/rfc/rfc4514.txt">RFC 4514</A> for more
052 * information about representing DNs and RDNs as strings.  See the
053 * documentation in the {@link DN} class for more information about DNs and
054 * RDNs.
055 */
056@NotMutable()
057@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
058public final class RDN
059       implements Comparable<RDN>, Comparator<RDN>, Serializable
060{
061  /**
062   * The serial version UID for this serializable class.
063   */
064  private static final long serialVersionUID = 2923419812807188487L;
065
066
067
068  // The set of attribute values for this RDN.
069  private final ASN1OctetString[] attributeValues;
070
071  // The schema to use to generate the normalized string representation of this
072  // RDN, if any.
073  private final Schema schema;
074
075  // The normalized string representation for this RDN.
076  private volatile String normalizedString;
077
078  // The user-defined string representation for this RDN.
079  private volatile String rdnString;
080
081  // The set of attribute names for this RDN.
082  private final String[] attributeNames;
083
084
085
086  /**
087   * Creates a new single-valued RDN with the provided information.
088   *
089   * @param  attributeName   The attribute name for this RDN.  It must not be
090   *                         {@code null}.
091   * @param  attributeValue  The attribute value for this RDN.  It must not be
092   *                         {@code null}.
093   */
094  public RDN(final String attributeName, final String attributeValue)
095  {
096    this(attributeName, attributeValue, null);
097  }
098
099
100
101  /**
102   * Creates a new single-valued RDN with the provided information.
103   *
104   * @param  attributeName   The attribute name for this RDN.  It must not be
105   *                         {@code null}.
106   * @param  attributeValue  The attribute value for this RDN.  It must not be
107   *                         {@code null}.
108   * @param  schema          The schema to use to generate the normalized string
109   *                         representation of this RDN.  It may be {@code null}
110   *                         if no schema is available.
111   */
112  public RDN(final String attributeName, final String attributeValue,
113             final Schema schema)
114  {
115    Validator.ensureNotNull(attributeName, attributeValue);
116
117    this.schema = schema;
118
119    attributeNames  = new String[] { attributeName };
120    attributeValues =
121         new ASN1OctetString[] { new ASN1OctetString(attributeValue) };
122  }
123
124
125
126  /**
127   * Creates a new single-valued RDN with the provided information.
128   *
129   * @param  attributeName   The attribute name for this RDN.  It must not be
130   *                         {@code null}.
131   * @param  attributeValue  The attribute value for this RDN.  It must not be
132   *                         {@code null}.
133   */
134  public RDN(final String attributeName, final byte[] attributeValue)
135  {
136    this(attributeName, attributeValue, null);
137  }
138
139
140
141  /**
142   * Creates a new single-valued RDN with the provided information.
143   *
144   * @param  attributeName   The attribute name for this RDN.  It must not be
145   *                         {@code null}.
146   * @param  attributeValue  The attribute value for this RDN.  It must not be
147   *                         {@code null}.
148   * @param  schema          The schema to use to generate the normalized string
149   *                         representation of this RDN.  It may be {@code null}
150   *                         if no schema is available.
151   */
152  public RDN(final String attributeName, final byte[] attributeValue,
153             final Schema schema)
154  {
155    Validator.ensureNotNull(attributeName, attributeValue);
156
157    this.schema = schema;
158
159    attributeNames  = new String[] { attributeName };
160    attributeValues =
161         new ASN1OctetString[] { new ASN1OctetString(attributeValue) };
162  }
163
164
165
166  /**
167   * Creates a new (potentially multivalued) RDN.  The set of names must have
168   * the same number of elements as the set of values, and there must be at
169   * least one element in each array.
170   *
171   * @param  attributeNames   The set of attribute names for this RDN.  It must
172   *                          not be {@code null} or empty.
173   * @param  attributeValues  The set of attribute values for this RDN.  It must
174   *                          not be {@code null} or empty.
175   */
176  public RDN(final String[] attributeNames, final String[] attributeValues)
177  {
178    this(attributeNames, attributeValues, null);
179  }
180
181
182
183  /**
184   * Creates a new (potentially multivalued) RDN.  The set of names must have
185   * the same number of elements as the set of values, and there must be at
186   * least one element in each array.
187   *
188   * @param  attributeNames   The set of attribute names for this RDN.  It must
189   *                          not be {@code null} or empty.
190   * @param  attributeValues  The set of attribute values for this RDN.  It must
191   *                          not be {@code null} or empty.
192   * @param  schema           The schema to use to generate the normalized
193   *                          string representation of this RDN.  It may be
194   *                          {@code null} if no schema is available.
195   */
196  public RDN(final String[] attributeNames, final String[] attributeValues,
197             final Schema schema)
198  {
199    Validator.ensureNotNull(attributeNames, attributeValues);
200    Validator.ensureTrue(attributeNames.length == attributeValues.length,
201         "RDN.attributeNames and attributeValues must be the same size.");
202    Validator.ensureTrue(attributeNames.length > 0,
203         "RDN.attributeNames must not be empty.");
204
205    this.attributeNames = attributeNames;
206    this.schema         = schema;
207
208    this.attributeValues = new ASN1OctetString[attributeValues.length];
209    for (int i=0; i < attributeValues.length; i++)
210    {
211      this.attributeValues[i] = new ASN1OctetString(attributeValues[i]);
212    }
213  }
214
215
216
217  /**
218   * Creates a new (potentially multivalued) RDN.  The set of names must have
219   * the same number of elements as the set of values, and there must be at
220   * least one element in each array.
221   *
222   * @param  attributeNames   The set of attribute names for this RDN.  It must
223   *                          not be {@code null} or empty.
224   * @param  attributeValues  The set of attribute values for this RDN.  It must
225   *                          not be {@code null} or empty.
226   */
227  public RDN(final String[] attributeNames, final byte[][] attributeValues)
228  {
229    this(attributeNames, attributeValues, null);
230  }
231
232
233
234  /**
235   * Creates a new (potentially multivalued) RDN.  The set of names must have
236   * the same number of elements as the set of values, and there must be at
237   * least one element in each array.
238   *
239   * @param  attributeNames   The set of attribute names for this RDN.  It must
240   *                          not be {@code null} or empty.
241   * @param  attributeValues  The set of attribute values for this RDN.  It must
242   *                          not be {@code null} or empty.
243   * @param  schema           The schema to use to generate the normalized
244   *                          string representation of this RDN.  It may be
245   *                          {@code null} if no schema is available.
246   */
247  public RDN(final String[] attributeNames, final byte[][] attributeValues,
248             final Schema schema)
249  {
250    Validator.ensureNotNull(attributeNames, attributeValues);
251    Validator.ensureTrue(attributeNames.length == attributeValues.length,
252         "RDN.attributeNames and attributeValues must be the same size.");
253    Validator.ensureTrue(attributeNames.length > 0,
254         "RDN.attributeNames must not be empty.");
255
256    this.attributeNames = attributeNames;
257    this.schema         = schema;
258
259    this.attributeValues = new ASN1OctetString[attributeValues.length];
260    for (int i=0; i < attributeValues.length; i++)
261    {
262      this.attributeValues[i] = new ASN1OctetString(attributeValues[i]);
263    }
264  }
265
266
267
268  /**
269   * Creates a new single-valued RDN with the provided information.
270   *
271   * @param  attributeName   The name to use for this RDN.
272   * @param  attributeValue  The value to use for this RDN.
273   * @param  schema          The schema to use to generate the normalized string
274   *                         representation of this RDN.  It may be {@code null}
275   *                         if no schema is available.
276   * @param  rdnString       The string representation for this RDN.
277   */
278  RDN(final String attributeName, final ASN1OctetString attributeValue,
279      final Schema schema, final String rdnString)
280  {
281    this.rdnString = rdnString;
282    this.schema    = schema;
283
284    attributeNames  = new String[] { attributeName };
285    attributeValues = new ASN1OctetString[] { attributeValue };
286  }
287
288
289
290  /**
291   * Creates a new potentially multivalued RDN with the provided information.
292   *
293   * @param  attributeNames   The set of names to use for this RDN.
294   * @param  attributeValues  The set of values to use for this RDN.
295   * @param  rdnString        The string representation for this RDN.
296   * @param  schema           The schema to use to generate the normalized
297   *                          string representation of this RDN.  It may be
298   *                          {@code null} if no schema is available.
299   */
300  RDN(final String[] attributeNames, final ASN1OctetString[] attributeValues,
301      final Schema schema, final String rdnString)
302  {
303    this.rdnString = rdnString;
304    this.schema    = schema;
305
306    this.attributeNames  = attributeNames;
307    this.attributeValues = attributeValues;
308  }
309
310
311
312  /**
313   * Creates a new RDN from the provided string representation.
314   *
315   * @param  rdnString  The string representation to use for this RDN.  It must
316   *                    not be empty or {@code null}.
317   *
318   * @throws  LDAPException  If the provided string cannot be parsed as a valid
319   *                         RDN.
320   */
321  public RDN(final String rdnString)
322         throws LDAPException
323  {
324    this(rdnString, (Schema) null);
325  }
326
327
328
329  /**
330   * Creates a new RDN from the provided string representation.
331   *
332   * @param  rdnString  The string representation to use for this RDN.  It must
333   *                    not be empty or {@code null}.
334   * @param  schema     The schema to use to generate the normalized string
335   *                    representation of this RDN.  It may be {@code null} if
336   *                    no schema is available.
337   *
338   * @throws  LDAPException  If the provided string cannot be parsed as a valid
339   *                         RDN.
340   */
341  public RDN(final String rdnString, final Schema schema)
342         throws LDAPException
343  {
344    Validator.ensureNotNull(rdnString);
345
346    this.rdnString = rdnString;
347    this.schema    = schema;
348
349    int pos = 0;
350    final int length = rdnString.length();
351
352    // First, skip over any leading spaces.
353    while ((pos < length) && (rdnString.charAt(pos) == ' '))
354    {
355      pos++;
356    }
357
358    // Read until we find a space or an equal sign.  Technically, we should
359    // ensure that all characters before that point are ASCII letters, numeric
360    // digits, or dashes, or that it is a valid numeric OID, but since some
361    // directories allow technically invalid characters in attribute names,
362    // we'll just blindly take whatever is provided.
363    int attrStartPos = pos;
364    while (pos < length)
365    {
366      final char c = rdnString.charAt(pos);
367      if ((c == ' ') || (c == '='))
368      {
369        break;
370      }
371
372      pos++;
373    }
374
375    // Extract the attribute name, then skip over any spaces between the
376    // attribute name and the equal sign.
377    String attrName = rdnString.substring(attrStartPos, pos);
378    if (attrName.isEmpty())
379    {
380      throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
381           ERR_RDN_NO_ATTR_NAME.get(rdnString));
382    }
383
384    while ((pos < length) && (rdnString.charAt(pos) == ' '))
385    {
386      pos++;
387    }
388
389    if ((pos >= length) || (rdnString.charAt(pos) != '='))
390    {
391      // We didn't find an equal sign.
392      throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
393           ERR_RDN_NO_EQUAL_SIGN.get(rdnString, attrName));
394    }
395
396
397    // The next character is the equal sign.  Skip it, and then skip over any
398    // spaces between it and the attribute value.
399    pos++;
400    while ((pos < length) && (rdnString.charAt(pos) == ' '))
401    {
402      pos++;
403    }
404
405
406    // Look at the next character.  If it is an octothorpe (#), then the value
407    // must be a hex-encoded BER element, which we'll need to parse and take the
408    // value of that element.  Otherwise, it's a regular string (although
409    // possibly containing escaped or quoted characters).
410    ASN1OctetString value;
411    if (pos >= length)
412    {
413      value = new ASN1OctetString();
414    }
415    else if (rdnString.charAt(pos) == '#')
416    {
417      // It is a hex-encoded value, so we'll read until we find the end of the
418      // string or the first non-hex character, which must be either a space or
419      // a plus sign.
420      final byte[] valueArray = readHexString(rdnString, ++pos);
421
422      try
423      {
424        value = ASN1OctetString.decodeAsOctetString(valueArray);
425      }
426      catch (final Exception e)
427      {
428        Debug.debugException(e);
429        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
430             ERR_RDN_HEX_STRING_NOT_BER_ENCODED.get(rdnString, attrName), e);
431      }
432
433      pos += (valueArray.length * 2);
434    }
435    else
436    {
437      // It is a string value, which potentially includes escaped characters.
438      final StringBuilder buffer = new StringBuilder();
439      pos = readValueString(rdnString, pos, buffer);
440      value = new ASN1OctetString(buffer.toString());
441    }
442
443
444    // Skip over any spaces until we find a plus sign or the end of the value.
445    while ((pos < length) && (rdnString.charAt(pos) == ' '))
446    {
447      pos++;
448    }
449
450    if (pos >= length)
451    {
452      // It's a single-valued RDN, so we have everything that we need.
453      attributeNames  = new String[] { attrName };
454      attributeValues = new ASN1OctetString[] { value };
455      return;
456    }
457
458    // It's a multivalued RDN, so create temporary lists to hold the names and
459    // values.
460    final ArrayList<String> nameList = new ArrayList<>(5);
461    final ArrayList<ASN1OctetString> valueList = new ArrayList<>(5);
462    nameList.add(attrName);
463    valueList.add(value);
464
465    if (rdnString.charAt(pos) == '+')
466    {
467      pos++;
468    }
469    else
470    {
471      throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
472           ERR_RDN_VALUE_NOT_FOLLOWED_BY_PLUS.get(rdnString));
473    }
474
475    if (pos >= length)
476    {
477      throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
478           ERR_RDN_PLUS_NOT_FOLLOWED_BY_AVP.get(rdnString));
479    }
480
481    int numValues = 1;
482    while (pos < length)
483    {
484      // Skip over any spaces between the plus sign and the attribute name.
485      while ((pos < length) && (rdnString.charAt(pos) == ' '))
486      {
487        pos++;
488      }
489
490      attrStartPos = pos;
491      while (pos < length)
492      {
493        final char c = rdnString.charAt(pos);
494        if ((c == ' ') || (c == '='))
495        {
496          break;
497        }
498
499        pos++;
500      }
501
502      // Skip over any spaces between the attribute name and the equal sign.
503      attrName = rdnString.substring(attrStartPos, pos);
504      if (attrName.isEmpty())
505      {
506        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
507             ERR_RDN_NO_ATTR_NAME.get(rdnString));
508      }
509
510      while ((pos < length) && (rdnString.charAt(pos) == ' '))
511      {
512        pos++;
513      }
514
515      if ((pos >= length) || (rdnString.charAt(pos) != '='))
516      {
517        // We didn't find an equal sign.
518        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
519             ERR_RDN_NO_EQUAL_SIGN.get(rdnString, attrName));
520      }
521
522      // The next character is the equal sign.  Skip it, and then skip over any
523      // spaces between it and the attribute value.
524      pos++;
525      while ((pos < length) && (rdnString.charAt(pos) == ' '))
526      {
527        pos++;
528      }
529
530      // Look at the next character.  If it is an octothorpe (#), then the value
531      // must be a hex-encoded BER element, which we'll need to parse and take
532      // the value of that element.  Otherwise, it's a regular string (although
533      // possibly containing escaped or quoted characters).
534      if (pos >= length)
535      {
536        value = new ASN1OctetString();
537      }
538      else if (rdnString.charAt(pos) == '#')
539      {
540        // It is a hex-encoded value, so we'll read until we find the end of the
541        // string or the first non-hex character, which must be either a space
542        // or a plus sign.
543        final byte[] valueArray = readHexString(rdnString, ++pos);
544
545        try
546        {
547          value = ASN1OctetString.decodeAsOctetString(valueArray);
548        }
549        catch (final Exception e)
550        {
551          Debug.debugException(e);
552          throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
553               ERR_RDN_HEX_STRING_NOT_BER_ENCODED.get(rdnString, attrName), e);
554        }
555
556        pos += (valueArray.length * 2);
557      }
558      else
559      {
560        // It is a string value, which potentially includes escaped characters.
561        final StringBuilder buffer = new StringBuilder();
562        pos = readValueString(rdnString, pos, buffer);
563        value = new ASN1OctetString(buffer.toString());
564      }
565
566
567      // Skip over any spaces until we find a plus sign or the end of the value.
568      while ((pos < length) && (rdnString.charAt(pos) == ' '))
569      {
570        pos++;
571      }
572
573      nameList.add(attrName);
574      valueList.add(value);
575      numValues++;
576
577      if (pos >= length)
578      {
579        // We're at the end of the value, so break out of the loop.
580        break;
581      }
582      else
583      {
584        // Skip over the plus sign and loop again to read another name-value
585        // pair.
586        if (rdnString.charAt(pos) == '+')
587        {
588          pos++;
589        }
590        else
591        {
592          throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
593               ERR_RDN_VALUE_NOT_FOLLOWED_BY_PLUS.get(rdnString));
594        }
595      }
596
597      if (pos >= length)
598      {
599        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
600             ERR_RDN_PLUS_NOT_FOLLOWED_BY_AVP.get(rdnString));
601      }
602    }
603
604    attributeNames  = new String[numValues];
605    attributeValues = new ASN1OctetString[numValues];
606    for (int i=0; i < numValues; i++)
607    {
608      attributeNames[i]  = nameList.get(i);
609      attributeValues[i] = valueList.get(i);
610    }
611  }
612
613
614
615  /**
616   * Parses a hex-encoded RDN value from the provided string.  Reading will
617   * continue until the end of the string is reached or a non-escaped plus sign
618   * is encountered.  After returning, the caller should increment its position
619   * by two times the length of the value array.
620   *
621   * @param  rdnString  The string to be parsed.  It should be the position
622   *                    immediately after the octothorpe at the start of the
623   *                    hex-encoded value.
624   * @param  startPos   The position at which to start reading the value.
625   *
626   * @return  A byte array containing the parsed value.
627   *
628   * @throws  LDAPException  If an error occurs while reading the value (e.g.,
629   *                         if it contains non-hex characters, or has an odd
630   *                         number of characters.
631   */
632  static byte[] readHexString(final String rdnString, final int startPos)
633         throws LDAPException
634  {
635    final int length = rdnString.length();
636    int pos = startPos;
637
638    final ByteBuffer buffer = ByteBuffer.allocate(length-pos);
639hexLoop:
640    while (pos < length)
641    {
642      final byte hexByte;
643      switch (rdnString.charAt(pos++))
644      {
645        case '0':
646          hexByte = 0x00;
647          break;
648        case '1':
649          hexByte = 0x10;
650          break;
651        case '2':
652          hexByte = 0x20;
653          break;
654        case '3':
655          hexByte = 0x30;
656          break;
657        case '4':
658          hexByte = 0x40;
659          break;
660        case '5':
661          hexByte = 0x50;
662          break;
663        case '6':
664          hexByte = 0x60;
665          break;
666        case '7':
667          hexByte = 0x70;
668          break;
669        case '8':
670          hexByte = (byte) 0x80;
671          break;
672        case '9':
673          hexByte = (byte) 0x90;
674          break;
675        case 'a':
676        case 'A':
677          hexByte = (byte) 0xA0;
678          break;
679        case 'b':
680        case 'B':
681          hexByte = (byte) 0xB0;
682          break;
683        case 'c':
684        case 'C':
685          hexByte = (byte) 0xC0;
686          break;
687        case 'd':
688        case 'D':
689          hexByte = (byte) 0xD0;
690          break;
691        case 'e':
692        case 'E':
693          hexByte = (byte) 0xE0;
694          break;
695        case 'f':
696        case 'F':
697          hexByte = (byte) 0xF0;
698          break;
699        case ' ':
700        case '+':
701        case ',':
702        case ';':
703          // This indicates that we've reached the end of the hex string.
704          break hexLoop;
705        default:
706          throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
707               ERR_RDN_INVALID_HEX_CHAR.get(rdnString, rdnString.charAt(pos-1),
708                    (pos-1)));
709      }
710
711      if (pos >= length)
712      {
713        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
714             ERR_RDN_MISSING_HEX_CHAR.get(rdnString));
715      }
716
717      switch (rdnString.charAt(pos++))
718      {
719        case '0':
720          buffer.put(hexByte);
721          break;
722        case '1':
723          buffer.put((byte) (hexByte | 0x01));
724          break;
725        case '2':
726          buffer.put((byte) (hexByte | 0x02));
727          break;
728        case '3':
729          buffer.put((byte) (hexByte | 0x03));
730          break;
731        case '4':
732          buffer.put((byte) (hexByte | 0x04));
733          break;
734        case '5':
735          buffer.put((byte) (hexByte | 0x05));
736          break;
737        case '6':
738          buffer.put((byte) (hexByte | 0x06));
739          break;
740        case '7':
741          buffer.put((byte) (hexByte | 0x07));
742          break;
743        case '8':
744          buffer.put((byte) (hexByte | 0x08));
745          break;
746        case '9':
747          buffer.put((byte) (hexByte | 0x09));
748          break;
749        case 'a':
750        case 'A':
751          buffer.put((byte) (hexByte | 0x0A));
752          break;
753        case 'b':
754        case 'B':
755          buffer.put((byte) (hexByte | 0x0B));
756          break;
757        case 'c':
758        case 'C':
759          buffer.put((byte) (hexByte | 0x0C));
760          break;
761        case 'd':
762        case 'D':
763          buffer.put((byte) (hexByte | 0x0D));
764          break;
765        case 'e':
766        case 'E':
767          buffer.put((byte) (hexByte | 0x0E));
768          break;
769        case 'f':
770        case 'F':
771          buffer.put((byte) (hexByte | 0x0F));
772          break;
773        default:
774          throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
775               ERR_RDN_INVALID_HEX_CHAR.get(rdnString, rdnString.charAt(pos-1),
776                    (pos-1)));
777      }
778    }
779
780    buffer.flip();
781    final byte[] valueArray = new byte[buffer.limit()];
782    buffer.get(valueArray);
783    return valueArray;
784  }
785
786
787
788  /**
789   * Reads a string value from the provided RDN string.  Reading will continue
790   * until the end of the string is reached or until a non-escaped plus sign is
791   * encountered.
792   *
793   * @param  rdnString  The string from which to read the value.
794   * @param  startPos   The position in the RDN string at which to start reading
795   *                    the value.
796   * @param  buffer     The buffer into which the parsed value should be
797   *                    placed.
798   *
799   * @return  The position at which the caller should continue reading when
800   *          parsing the RDN.
801   *
802   * @throws  LDAPException  If a problem occurs while reading the value.
803   */
804  static int readValueString(final String rdnString, final int startPos,
805                             final StringBuilder buffer)
806          throws LDAPException
807  {
808    final int length = rdnString.length();
809    int pos = startPos;
810
811    boolean inQuotes = false;
812valueLoop:
813    while (pos < length)
814    {
815      char c = rdnString.charAt(pos);
816      switch (c)
817      {
818        case '\\':
819          // It's an escaped value.  It can either be followed by a single
820          // character (e.g., backslash, space, octothorpe, equals, double
821          // quote, plus sign, comma, semicolon, less than, or greater-than), or
822          // two hex digits.  If it is followed by hex digits, then continue
823          // reading to see if there are more of them.
824          if ((pos+1) >= length)
825          {
826            throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
827                 ERR_RDN_ENDS_WITH_BACKSLASH.get(rdnString));
828          }
829          else
830          {
831            pos++;
832            c = rdnString.charAt(pos);
833            if (StaticUtils.isHex(c))
834            {
835              // We need to subtract one from the resulting position because
836              // it will be incremented later.
837              pos = readEscapedHexString(rdnString, pos, buffer) - 1;
838            }
839            else
840            {
841              buffer.append(c);
842            }
843          }
844          break;
845
846        case '"':
847          if (inQuotes)
848          {
849            // This should be the end of the value.  If it's not, then fail.
850            pos++;
851            while (pos < length)
852            {
853              c = rdnString.charAt(pos);
854              if ((c == '+') || (c == ',') || (c == ';'))
855              {
856                break;
857              }
858              else if (c != ' ')
859              {
860                throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
861                     ERR_RDN_CHAR_OUTSIDE_QUOTES.get(rdnString, c, (pos-1)));
862              }
863
864              pos++;
865            }
866
867            inQuotes = false;
868            break valueLoop;
869          }
870          else
871          {
872            // This should be the first character of the value.
873            if (pos == startPos)
874            {
875              inQuotes = true;
876            }
877            else
878            {
879              throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
880                   ERR_RDN_UNEXPECTED_DOUBLE_QUOTE.get(rdnString, pos));
881            }
882          }
883          break;
884
885        case ',':
886        case ';':
887        case '+':
888          // This denotes the end of the value, if it's not in quotes.
889          if (inQuotes)
890          {
891            buffer.append(c);
892          }
893          else
894          {
895            break valueLoop;
896          }
897          break;
898
899        default:
900          // This is a normal character that should be added to the buffer.
901          buffer.append(c);
902          break;
903      }
904
905      pos++;
906    }
907
908
909    // If the value started with a quotation mark, then make sure it was closed.
910    if (inQuotes)
911    {
912      throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
913           ERR_RDN_UNCLOSED_DOUBLE_QUOTE.get(rdnString));
914    }
915
916
917    // If the value ends with any unescaped trailing spaces, then trim them off.
918    int bufferPos = buffer.length() - 1;
919    int rdnStrPos = pos - 2;
920    while ((bufferPos > 0) && (buffer.charAt(bufferPos) == ' '))
921    {
922      if (rdnString.charAt(rdnStrPos) == '\\')
923      {
924        break;
925      }
926      else
927      {
928        buffer.deleteCharAt(bufferPos--);
929        rdnStrPos--;
930      }
931    }
932
933    return pos;
934  }
935
936
937
938  /**
939   * Reads one or more hex-encoded bytes from the specified portion of the RDN
940   * string.
941   *
942   * @param  rdnString  The string from which the data is to be read.
943   * @param  startPos   The position at which to start reading.  This should be
944   *                    the first hex character immediately after the initial
945   *                    backslash.
946   * @param  buffer     The buffer to which the decoded string portion should be
947   *                    appended.
948   *
949   * @return  The position at which the caller may resume parsing.
950   *
951   * @throws  LDAPException  If a problem occurs while reading hex-encoded
952   *                         bytes.
953   */
954  private static int readEscapedHexString(final String rdnString,
955                                          final int startPos,
956                                          final StringBuilder buffer)
957          throws LDAPException
958  {
959    final int length = rdnString.length();
960    int pos = startPos;
961
962    final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
963    while (pos < length)
964    {
965      final byte b;
966      switch (rdnString.charAt(pos++))
967      {
968        case '0':
969          b = 0x00;
970          break;
971        case '1':
972          b = 0x10;
973          break;
974        case '2':
975          b = 0x20;
976          break;
977        case '3':
978          b = 0x30;
979          break;
980        case '4':
981          b = 0x40;
982          break;
983        case '5':
984          b = 0x50;
985          break;
986        case '6':
987          b = 0x60;
988          break;
989        case '7':
990          b = 0x70;
991          break;
992        case '8':
993          b = (byte) 0x80;
994          break;
995        case '9':
996          b = (byte) 0x90;
997          break;
998        case 'a':
999        case 'A':
1000          b = (byte) 0xA0;
1001          break;
1002        case 'b':
1003        case 'B':
1004          b = (byte) 0xB0;
1005          break;
1006        case 'c':
1007        case 'C':
1008          b = (byte) 0xC0;
1009          break;
1010        case 'd':
1011        case 'D':
1012          b = (byte) 0xD0;
1013          break;
1014        case 'e':
1015        case 'E':
1016          b = (byte) 0xE0;
1017          break;
1018        case 'f':
1019        case 'F':
1020          b = (byte) 0xF0;
1021          break;
1022        default:
1023          throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
1024               ERR_RDN_INVALID_HEX_CHAR.get(rdnString, rdnString.charAt(pos-1),
1025                    (pos-1)));
1026      }
1027
1028      if (pos >= length)
1029      {
1030        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
1031             ERR_RDN_MISSING_HEX_CHAR.get(rdnString));
1032      }
1033
1034      switch (rdnString.charAt(pos++))
1035      {
1036        case '0':
1037          byteBuffer.put(b);
1038          break;
1039        case '1':
1040          byteBuffer.put((byte) (b | 0x01));
1041          break;
1042        case '2':
1043          byteBuffer.put((byte) (b | 0x02));
1044          break;
1045        case '3':
1046          byteBuffer.put((byte) (b | 0x03));
1047          break;
1048        case '4':
1049          byteBuffer.put((byte) (b | 0x04));
1050          break;
1051        case '5':
1052          byteBuffer.put((byte) (b | 0x05));
1053          break;
1054        case '6':
1055          byteBuffer.put((byte) (b | 0x06));
1056          break;
1057        case '7':
1058          byteBuffer.put((byte) (b | 0x07));
1059          break;
1060        case '8':
1061          byteBuffer.put((byte) (b | 0x08));
1062          break;
1063        case '9':
1064          byteBuffer.put((byte) (b | 0x09));
1065          break;
1066        case 'a':
1067        case 'A':
1068          byteBuffer.put((byte) (b | 0x0A));
1069          break;
1070        case 'b':
1071        case 'B':
1072          byteBuffer.put((byte) (b | 0x0B));
1073          break;
1074        case 'c':
1075        case 'C':
1076          byteBuffer.put((byte) (b | 0x0C));
1077          break;
1078        case 'd':
1079        case 'D':
1080          byteBuffer.put((byte) (b | 0x0D));
1081          break;
1082        case 'e':
1083        case 'E':
1084          byteBuffer.put((byte) (b | 0x0E));
1085          break;
1086        case 'f':
1087        case 'F':
1088          byteBuffer.put((byte) (b | 0x0F));
1089          break;
1090        default:
1091          throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
1092               ERR_RDN_INVALID_HEX_CHAR.get(rdnString, rdnString.charAt(pos-1),
1093                    (pos-1)));
1094      }
1095
1096      if (((pos+1) < length) && (rdnString.charAt(pos) == '\\') &&
1097          StaticUtils.isHex(rdnString.charAt(pos+1)))
1098      {
1099        // It appears that there are more hex-encoded bytes to follow, so keep
1100        // reading.
1101        pos++;
1102        continue;
1103      }
1104      else
1105      {
1106        break;
1107      }
1108    }
1109
1110    byteBuffer.flip();
1111    final byte[] byteArray = new byte[byteBuffer.limit()];
1112    byteBuffer.get(byteArray);
1113
1114    try
1115    {
1116      buffer.append(StaticUtils.toUTF8String(byteArray));
1117    }
1118    catch (final Exception e)
1119    {
1120      Debug.debugException(e);
1121      // This should never happen.
1122      buffer.append(new String(byteArray));
1123    }
1124
1125    return pos;
1126  }
1127
1128
1129
1130  /**
1131   * Indicates whether the provided string represents a valid RDN.
1132   *
1133   * @param  s  The string for which to make the determination.  It must not be
1134   *            {@code null}.
1135   *
1136   * @return  {@code true} if the provided string represents a valid RDN, or
1137   *          {@code false} if not.
1138   */
1139  public static boolean isValidRDN(final String s)
1140  {
1141    try
1142    {
1143      new RDN(s);
1144      return true;
1145    }
1146    catch (final LDAPException le)
1147    {
1148      return false;
1149    }
1150  }
1151
1152
1153
1154  /**
1155   * Indicates whether this RDN contains multiple components.
1156   *
1157   * @return  {@code true} if this RDN contains multiple components, or
1158   *          {@code false} if not.
1159   */
1160  public boolean isMultiValued()
1161  {
1162    return (attributeNames.length != 1);
1163  }
1164
1165
1166
1167  /**
1168   * Retrieves an array of the attributes that comprise this RDN.
1169   *
1170   * @return  An array of the attributes that comprise this RDN.
1171   */
1172  public Attribute[] getAttributes()
1173  {
1174    final Attribute[] attrs = new Attribute[attributeNames.length];
1175    for (int i=0; i < attrs.length; i++)
1176    {
1177      attrs[i] = new Attribute(attributeNames[i], schema,
1178           new ASN1OctetString[] {  attributeValues[i] });
1179    }
1180
1181    return attrs;
1182  }
1183
1184
1185
1186  /**
1187   * Retrieves the set of attribute names for this RDN.
1188   *
1189   * @return  The set of attribute names for this RDN.
1190   */
1191  public String[] getAttributeNames()
1192  {
1193    return attributeNames;
1194  }
1195
1196
1197
1198  /**
1199   * Retrieves the set of attribute values for this RDN.
1200   *
1201   * @return  The set of attribute values for this RDN.
1202   */
1203  public String[] getAttributeValues()
1204  {
1205    final String[] stringValues = new String[attributeValues.length];
1206    for (int i=0; i < stringValues.length; i++)
1207    {
1208      stringValues[i] = attributeValues[i].stringValue();
1209    }
1210
1211    return stringValues;
1212  }
1213
1214
1215
1216  /**
1217   * Retrieves the set of attribute values for this RDN.
1218   *
1219   * @return  The set of attribute values for this RDN.
1220   */
1221  public byte[][] getByteArrayAttributeValues()
1222  {
1223    final byte[][] byteValues = new byte[attributeValues.length][];
1224    for (int i=0; i < byteValues.length; i++)
1225    {
1226      byteValues[i] = attributeValues[i].getValue();
1227    }
1228
1229    return byteValues;
1230  }
1231
1232
1233
1234  /**
1235   * Retrieves the schema that will be used for this RDN, if any.
1236   *
1237   * @return  The schema that will be used for this RDN, or {@code null} if none
1238   *          has been provided.
1239   */
1240  Schema getSchema()
1241  {
1242    return schema;
1243  }
1244
1245
1246
1247  /**
1248   * Indicates whether this RDN contains the specified attribute.
1249   *
1250   * @param  attributeName  The name of the attribute for which to make the
1251   *                        determination.
1252   *
1253   * @return  {@code true} if RDN contains the specified attribute, or
1254   *          {@code false} if not.
1255   */
1256  public boolean hasAttribute(final String attributeName)
1257  {
1258    for (final String name : attributeNames)
1259    {
1260      if (name.equalsIgnoreCase(attributeName))
1261      {
1262        return true;
1263      }
1264    }
1265
1266    return false;
1267  }
1268
1269
1270
1271  /**
1272   * Indicates whether this RDN contains the specified attribute value.
1273   *
1274   * @param  attributeName   The name of the attribute for which to make the
1275   *                         determination.
1276   * @param  attributeValue  The attribute value for which to make the
1277   *                         determination.
1278   *
1279   * @return  {@code true} if RDN contains the specified attribute, or
1280   *          {@code false} if not.
1281   */
1282  public boolean hasAttributeValue(final String attributeName,
1283                                   final String attributeValue)
1284  {
1285    for (int i=0; i < attributeNames.length; i++)
1286    {
1287      if (attributeNames[i].equalsIgnoreCase(attributeName))
1288      {
1289        final Attribute a =
1290             new Attribute(attributeName, schema, attributeValue);
1291        final Attribute b = new Attribute(attributeName, schema,
1292             attributeValues[i].stringValue());
1293
1294        if (a.equals(b))
1295        {
1296          return true;
1297        }
1298      }
1299    }
1300
1301    return false;
1302  }
1303
1304
1305
1306  /**
1307   * Indicates whether this RDN contains the specified attribute value.
1308   *
1309   * @param  attributeName   The name of the attribute for which to make the
1310   *                         determination.
1311   * @param  attributeValue  The attribute value for which to make the
1312   *                         determination.
1313   *
1314   * @return  {@code true} if RDN contains the specified attribute, or
1315   *          {@code false} if not.
1316   */
1317  public boolean hasAttributeValue(final String attributeName,
1318                                   final byte[] attributeValue)
1319  {
1320    for (int i=0; i < attributeNames.length; i++)
1321    {
1322      if (attributeNames[i].equalsIgnoreCase(attributeName))
1323      {
1324        final Attribute a =
1325             new Attribute(attributeName, schema, attributeValue);
1326        final Attribute b = new Attribute(attributeName, schema,
1327             attributeValues[i].getValue());
1328
1329        if (a.equals(b))
1330        {
1331          return true;
1332        }
1333      }
1334    }
1335
1336    return false;
1337  }
1338
1339
1340
1341  /**
1342   * Retrieves a string representation of this RDN.
1343   *
1344   * @return  A string representation of this RDN.
1345   */
1346  @Override()
1347  public String toString()
1348  {
1349    if (rdnString == null)
1350    {
1351      final StringBuilder buffer = new StringBuilder();
1352      toString(buffer, false);
1353      rdnString = buffer.toString();
1354    }
1355
1356    return rdnString;
1357  }
1358
1359
1360
1361  /**
1362   * Retrieves a string representation of this RDN with minimal encoding for
1363   * special characters.  Only those characters specified in RFC 4514 section
1364   * 2.4 will be escaped.  No escaping will be used for non-ASCII characters or
1365   * non-printable ASCII characters.
1366   *
1367   * @return  A string representation of this RDN with minimal encoding for
1368   *          special characters.
1369   */
1370  public String toMinimallyEncodedString()
1371  {
1372    final StringBuilder buffer = new StringBuilder();
1373    toString(buffer, true);
1374    return buffer.toString();
1375  }
1376
1377
1378
1379  /**
1380   * Appends a string representation of this RDN to the provided buffer.
1381   *
1382   * @param  buffer  The buffer to which the string representation is to be
1383   *                 appended.
1384   */
1385  public void toString(final StringBuilder buffer)
1386  {
1387    toString(buffer, false);
1388  }
1389
1390
1391
1392  /**
1393   * Appends a string representation of this RDN to the provided buffer.
1394   *
1395   * @param  buffer            The buffer to which the string representation is
1396   *                           to be appended.
1397   * @param  minimizeEncoding  Indicates whether to restrict the encoding of
1398   *                           special characters to the bare minimum required
1399   *                           by LDAP (as per RFC 4514 section 2.4).  If this
1400   *                           is {@code true}, then only leading and trailing
1401   *                           spaces, double quotes, plus signs, commas,
1402   *                           semicolons, greater-than, less-than, and
1403   *                           backslash characters will be encoded.
1404   */
1405  public void toString(final StringBuilder buffer,
1406                       final boolean minimizeEncoding)
1407  {
1408    if ((rdnString != null) && (! minimizeEncoding))
1409    {
1410      buffer.append(rdnString);
1411      return;
1412    }
1413
1414    for (int i=0; i < attributeNames.length; i++)
1415    {
1416      if (i > 0)
1417      {
1418        buffer.append('+');
1419      }
1420
1421      buffer.append(attributeNames[i]);
1422      buffer.append('=');
1423
1424      // Iterate through the value character-by-character and do any escaping
1425      // that may be necessary.
1426      final String valueString = attributeValues[i].stringValue();
1427      final int length = valueString.length();
1428      for (int j=0; j < length; j++)
1429      {
1430        final char c = valueString.charAt(j);
1431        switch (c)
1432        {
1433          case '\\':
1434          case '=':
1435          case '"':
1436          case '+':
1437          case ',':
1438          case ';':
1439          case '<':
1440          case '>':
1441            buffer.append('\\');
1442            buffer.append(c);
1443            break;
1444
1445          case '#':
1446            // Escape the octothorpe only if it's the first character.
1447            if (j == 0)
1448            {
1449              buffer.append("\\#");
1450            }
1451            else
1452            {
1453              buffer.append('#');
1454            }
1455            break;
1456
1457          case ' ':
1458            // Escape this space only if it's the first or last character.
1459            if ((j == 0) || ((j+1) == length))
1460            {
1461              buffer.append("\\ ");
1462            }
1463            else
1464            {
1465              buffer.append(' ');
1466            }
1467            break;
1468
1469          case '\u0000':
1470            buffer.append("\\00");
1471            break;
1472
1473          default:
1474            // If it's not a printable ASCII character, then hex-encode it
1475            // unless we're using minimized encoding.
1476            if ((! minimizeEncoding) && ((c < ' ') || (c > '~')))
1477            {
1478              StaticUtils.hexEncode(c, buffer);
1479            }
1480            else
1481            {
1482              buffer.append(c);
1483            }
1484            break;
1485        }
1486      }
1487    }
1488  }
1489
1490
1491
1492  /**
1493   * Retrieves a normalized string representation of this RDN.
1494   *
1495   * @return  A normalized string representation of this RDN.
1496   */
1497  public String toNormalizedString()
1498  {
1499    if (normalizedString == null)
1500    {
1501      final StringBuilder buffer = new StringBuilder();
1502      toNormalizedString(buffer);
1503      normalizedString = buffer.toString();
1504    }
1505
1506    return normalizedString;
1507  }
1508
1509
1510
1511  /**
1512   * Appends a normalized string representation of this RDN to the provided
1513   * buffer.
1514   *
1515   * @param  buffer  The buffer to which the normalized string representation is
1516   *                 to be appended.
1517   */
1518  public void toNormalizedString(final StringBuilder buffer)
1519  {
1520    if (attributeNames.length == 1)
1521    {
1522      // It's a single-valued RDN, so there is no need to sort anything.
1523      final String name = normalizeAttrName(attributeNames[0]);
1524      buffer.append(name);
1525      buffer.append('=');
1526      buffer.append(normalizeValue(name, attributeValues[0]));
1527    }
1528    else
1529    {
1530      // It's a multivalued RDN, so we need to sort the components.
1531      final TreeMap<String,ASN1OctetString> valueMap = new TreeMap<>();
1532      for (int i=0; i < attributeNames.length; i++)
1533      {
1534        final String name = normalizeAttrName(attributeNames[i]);
1535        valueMap.put(name, attributeValues[i]);
1536      }
1537
1538      int i=0;
1539      for (final Map.Entry<String,ASN1OctetString> entry : valueMap.entrySet())
1540      {
1541        if (i++ > 0)
1542        {
1543          buffer.append('+');
1544        }
1545
1546        buffer.append(entry.getKey());
1547        buffer.append('=');
1548        buffer.append(normalizeValue(entry.getKey(), entry.getValue()));
1549      }
1550    }
1551  }
1552
1553
1554
1555  /**
1556   * Obtains a normalized representation of the provided attribute name.
1557   *
1558   * @param  name  The name of the attribute for which to create the normalized
1559   *               representation.
1560   *
1561   * @return  A normalized representation of the provided attribute name.
1562   */
1563  private String normalizeAttrName(final String name)
1564  {
1565    String n = name;
1566    if (schema != null)
1567    {
1568      final AttributeTypeDefinition at = schema.getAttributeType(name);
1569      if (at != null)
1570      {
1571        n = at.getNameOrOID();
1572      }
1573    }
1574    return StaticUtils.toLowerCase(n);
1575  }
1576
1577
1578
1579  /**
1580   * Retrieves a normalized string representation of the RDN with the provided
1581   * string representation.
1582   *
1583   * @param  s  The string representation of the RDN to normalize.  It must not
1584   *            be {@code null}.
1585   *
1586   * @return  The normalized string representation of the RDN with the provided
1587   *          string representation.
1588   *
1589   * @throws  LDAPException  If the provided string cannot be parsed as an RDN.
1590   */
1591  public static String normalize(final String s)
1592         throws LDAPException
1593  {
1594    return normalize(s, null);
1595  }
1596
1597
1598
1599  /**
1600   * Retrieves a normalized string representation of the RDN with the provided
1601   * string representation.
1602   *
1603   * @param  s       The string representation of the RDN to normalize.  It must
1604   *                 not be {@code null}.
1605   * @param  schema  The schema to use to generate the normalized string
1606   *                 representation of the RDN.  It may be {@code null} if no
1607   *                 schema is available.
1608   *
1609   * @return  The normalized string representation of the RDN with the provided
1610   *          string representation.
1611   *
1612   * @throws  LDAPException  If the provided string cannot be parsed as an RDN.
1613   */
1614  public static String normalize(final String s, final Schema schema)
1615         throws LDAPException
1616  {
1617    return new RDN(s, schema).toNormalizedString();
1618  }
1619
1620
1621
1622  /**
1623   * Normalizes the provided attribute value for use in an RDN.
1624   *
1625   * @param  attributeName  The name of the attribute with which the value is
1626   *                        associated.
1627   * @param  value           The value to be normalized.
1628   *
1629   * @return  A string builder containing a normalized representation of the
1630   *          value in a suitable form for inclusion in an RDN.
1631   */
1632  private StringBuilder normalizeValue(final String attributeName,
1633                                       final ASN1OctetString value)
1634  {
1635    final MatchingRule matchingRule =
1636         MatchingRule.selectEqualityMatchingRule(attributeName, schema);
1637
1638    ASN1OctetString rawNormValue;
1639    try
1640    {
1641      rawNormValue = matchingRule.normalize(value);
1642    }
1643    catch (final Exception e)
1644    {
1645      Debug.debugException(e);
1646      rawNormValue =
1647           new ASN1OctetString(StaticUtils.toLowerCase(value.stringValue()));
1648    }
1649
1650    final String valueString = rawNormValue.stringValue();
1651    final int length = valueString.length();
1652    final StringBuilder buffer = new StringBuilder(length);
1653
1654    for (int i=0; i < length; i++)
1655    {
1656      final char c = valueString.charAt(i);
1657
1658      switch (c)
1659      {
1660        case '\\':
1661        case '=':
1662        case '"':
1663        case '+':
1664        case ',':
1665        case ';':
1666        case '<':
1667        case '>':
1668          buffer.append('\\');
1669          buffer.append(c);
1670          break;
1671
1672        case '#':
1673          // Escape the octothorpe only if it's the first character.
1674          if (i == 0)
1675          {
1676            buffer.append("\\#");
1677          }
1678          else
1679          {
1680            buffer.append('#');
1681          }
1682          break;
1683
1684        case ' ':
1685          // Escape this space only if it's the first or last character.
1686          if ((i == 0) || ((i+1) == length))
1687          {
1688            buffer.append("\\ ");
1689          }
1690          else
1691          {
1692            buffer.append(' ');
1693          }
1694          break;
1695
1696        default:
1697          // If it's a printable ASCII character that isn't covered by one of
1698          // the above options, then just append it to the buffer.  Otherwise,
1699          // hex-encode all bytes that comprise its UTF-8 representation, which
1700          // might require special handling if it requires two Java characters
1701          // to encode the Unicode character.
1702          if ((c >= ' ') && (c <= '~'))
1703          {
1704            buffer.append(c);
1705          }
1706          else if (Character.isHighSurrogate(c))
1707          {
1708            if (((i+1) < length) &&
1709                 Character.isLowSurrogate(valueString.charAt(i+1)))
1710            {
1711              final char c2 = valueString.charAt(++i);
1712              final int codePoint = Character.toCodePoint(c, c2);
1713              StaticUtils.hexEncode(codePoint, buffer);
1714            }
1715            else
1716            {
1717              // This should never happen.
1718              StaticUtils.hexEncode(c, buffer);
1719            }
1720          }
1721          else
1722          {
1723            StaticUtils.hexEncode(c, buffer);
1724          }
1725          break;
1726      }
1727    }
1728
1729    return buffer;
1730  }
1731
1732
1733
1734  /**
1735   * Retrieves a hash code for this RDN.
1736   *
1737   * @return  The hash code for this RDN.
1738   */
1739  @Override()
1740  public int hashCode()
1741  {
1742    return toNormalizedString().hashCode();
1743  }
1744
1745
1746
1747  /**
1748   * Indicates whether this RDN is equal to the provided object.  The given
1749   * object will only be considered equal to this RDN if it is also an RDN with
1750   * the same set of names and values.
1751   *
1752   * @param  o  The object for which to make the determination.
1753   *
1754   * @return  {@code true} if the provided object can be considered equal to
1755   *          this RDN, or {@code false} if not.
1756   */
1757  @Override()
1758  public boolean equals(final Object o)
1759  {
1760    if (o == null)
1761    {
1762      return false;
1763    }
1764
1765    if (o == this)
1766    {
1767      return true;
1768    }
1769
1770    if (! (o instanceof RDN))
1771    {
1772      return false;
1773    }
1774
1775    final RDN rdn = (RDN) o;
1776    return (toNormalizedString().equals(rdn.toNormalizedString()));
1777  }
1778
1779
1780
1781  /**
1782   * Indicates whether the RDN with the provided string representation is equal
1783   * to this RDN.
1784   *
1785   * @param  s  The string representation of the DN to compare with this RDN.
1786   *
1787   * @return  {@code true} if the DN with the provided string representation is
1788   *          equal to this RDN, or {@code false} if not.
1789   *
1790   * @throws  LDAPException  If the provided string cannot be parsed as an RDN.
1791   */
1792  public boolean equals(final String s)
1793         throws LDAPException
1794  {
1795    if (s == null)
1796    {
1797      return false;
1798    }
1799
1800    return equals(new RDN(s, schema));
1801  }
1802
1803
1804
1805  /**
1806   * Indicates whether the two provided strings represent the same RDN.
1807   *
1808   * @param  s1  The string representation of the first RDN for which to make
1809   *             the determination.  It must not be {@code null}.
1810   * @param  s2  The string representation of the second RDN for which to make
1811   *             the determination.  It must not be {@code null}.
1812   *
1813   * @return  {@code true} if the provided strings represent the same RDN, or
1814   *          {@code false} if not.
1815   *
1816   * @throws  LDAPException  If either of the provided strings cannot be parsed
1817   *                         as an RDN.
1818   */
1819  public static boolean equals(final String s1, final String s2)
1820         throws LDAPException
1821  {
1822    return new RDN(s1).equals(new RDN(s2));
1823  }
1824
1825
1826
1827  /**
1828   * Compares the provided RDN to this RDN to determine their relative order in
1829   * a sorted list.
1830   *
1831   * @param  rdn  The RDN to compare against this RDN.  It must not be
1832   *              {@code null}.
1833   *
1834   * @return  A negative integer if this RDN should come before the provided RDN
1835   *          in a sorted list, a positive integer if this RDN should come after
1836   *          the provided RDN in a sorted list, or zero if the provided RDN
1837   *          can be considered equal to this RDN.
1838   */
1839  @Override()
1840  public int compareTo(final RDN rdn)
1841  {
1842    return compare(this, rdn);
1843  }
1844
1845
1846
1847  /**
1848   * Compares the provided RDN values to determine their relative order in a
1849   * sorted list.
1850   *
1851   * @param  rdn1  The first RDN to be compared.  It must not be {@code null}.
1852   * @param  rdn2  The second RDN to be compared.  It must not be {@code null}.
1853   *
1854   * @return  A negative integer if the first RDN should come before the second
1855   *          RDN in a sorted list, a positive integer if the first RDN should
1856   *          come after the second RDN in a sorted list, or zero if the two RDN
1857   *          values can be considered equal.
1858   */
1859  @Override()
1860  public int compare(final RDN rdn1, final RDN rdn2)
1861  {
1862    Validator.ensureNotNull(rdn1, rdn2);
1863
1864    return(rdn1.toNormalizedString().compareTo(rdn2.toNormalizedString()));
1865  }
1866
1867
1868
1869  /**
1870   * Compares the RDN values with the provided string representations to
1871   * determine their relative order in a sorted list.
1872   *
1873   * @param  s1  The string representation of the first RDN to be compared.  It
1874   *             must not be {@code null}.
1875   * @param  s2  The string representation of the second RDN to be compared.  It
1876   *             must not be {@code null}.
1877   *
1878   * @return  A negative integer if the first RDN should come before the second
1879   *          RDN in a sorted list, a positive integer if the first RDN should
1880   *          come after the second RDN in a sorted list, or zero if the two RDN
1881   *          values can be considered equal.
1882   *
1883   * @throws  LDAPException  If either of the provided strings cannot be parsed
1884   *                         as an RDN.
1885   */
1886  public static int compare(final String s1, final String s2)
1887         throws LDAPException
1888  {
1889    return compare(s1, s2, null);
1890  }
1891
1892
1893
1894  /**
1895   * Compares the RDN values with the provided string representations to
1896   * determine their relative order in a sorted list.
1897   *
1898   * @param  s1      The string representation of the first RDN to be compared.
1899   *                 It must not be {@code null}.
1900   * @param  s2      The string representation of the second RDN to be compared.
1901   *                 It must not be {@code null}.
1902   * @param  schema  The schema to use to generate the normalized string
1903   *                 representations of the RDNs.  It may be {@code null} if no
1904   *                 schema is available.
1905   *
1906   * @return  A negative integer if the first RDN should come before the second
1907   *          RDN in a sorted list, a positive integer if the first RDN should
1908   *          come after the second RDN in a sorted list, or zero if the two RDN
1909   *          values can be considered equal.
1910   *
1911   * @throws  LDAPException  If either of the provided strings cannot be parsed
1912   *                         as an RDN.
1913   */
1914  public static int compare(final String s1, final String s2,
1915                            final Schema schema)
1916         throws LDAPException
1917  {
1918    return new RDN(s1, schema).compareTo(new RDN(s2, schema));
1919  }
1920}