View Javadoc
1   /*
2    * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package javax.naming.ldap;
27  
28  import java.util.Iterator;
29  import java.util.NoSuchElementException;
30  import java.util.ArrayList;
31  import java.util.Locale;
32  import java.util.Collections;
33  
34  import javax.naming.InvalidNameException;
35  import javax.naming.directory.BasicAttributes;
36  import javax.naming.directory.Attributes;
37  import javax.naming.directory.Attribute;
38  import javax.naming.NamingEnumeration;
39  import javax.naming.NamingException;
40  
41  import java.io.Serializable;
42  import java.io.ObjectOutputStream;
43  import java.io.ObjectInputStream;
44  import java.io.IOException;
45  
46  /**
47   * This class represents a relative distinguished name, or RDN, which is a
48   * component of a distinguished name as specified by
49   * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.
50   * An example of an RDN is "OU=Sales+CN=J.Smith". In this example,
51   * the RDN consist of multiple attribute type/value pairs. The
52   * RDN is parsed as described in the class description for
53   * {@link javax.naming.ldap.LdapName <tt>LdapName</tt>}.
54   * <p>
55   * The Rdn class represents an RDN as attribute type/value mappings,
56   * which can be viewed using
57   * {@link javax.naming.directory.Attributes Attributes}.
58   * In addition, it contains convenience methods that allow easy retrieval
59   * of type and value when the Rdn consist of a single type/value pair,
60   * which is how it appears in a typical usage.
61   * It also contains helper methods that allow escaping of the unformatted
62   * attribute value and unescaping of the value formatted according to the
63   * escaping syntax defined in RFC2253. For methods that take or return
64   * attribute value as an Object, the value is either a String
65   * (in unescaped form) or a byte array.
66   * <p>
67   * <code>Rdn</code> will properly parse all valid RDNs, but
68   * does not attempt to detect all possible violations when parsing
69   * invalid RDNs. It is "generous" in accepting invalid RDNs.
70   * The "validity" of a name is determined ultimately when it
71   * is supplied to an LDAP server, which may accept or
72   * reject the name based on factors such as its schema information
73   * and interoperability considerations.
74   *
75   * <p>
76   * The following code example shows how to construct an Rdn using the
77   * constructor that takes type and value as arguments:
78   * <pre>
79   *      Rdn rdn = new Rdn("cn", "Juicy, Fruit");
80   *      System.out.println(rdn.toString());
81   * </pre>
82   * The last line will print <tt>cn=Juicy\, Fruit</tt>. The
83   * {@link #unescapeValue(String) <tt>unescapeValue()</tt>} method can be
84   * used to unescape the escaped comma resulting in the original
85   * value <tt>"Juicy, Fruit"</tt>. The {@link #escapeValue(Object)
86   * <tt>escapeValue()</tt>} method adds the escape back preceding the comma.
87   * <p>
88   * This class can be instantiated by a string representation
89   * of the RDN defined in RFC 2253 as shown in the following code example:
90   * <pre>
91   *      Rdn rdn = new Rdn("cn=Juicy\\, Fruit");
92   *      System.out.println(rdn.toString());
93   * </pre>
94   * The last line will print <tt>cn=Juicy\, Fruit</tt>.
95   * <p>
96   * Concurrent multithreaded read-only access of an instance of
97   * <tt>Rdn</tt> need not be synchronized.
98   * <p>
99   * Unless otherwise noted, the behavior of passing a null argument
100  * to a constructor or method in this class will cause NullPointerException
101  * to be thrown.
102  *
103  * @since 1.5
104  */
105 
106 public class Rdn implements Serializable, Comparable<Object> {
107 
108     private transient ArrayList<RdnEntry> entries;
109 
110     // The common case.
111     private static final int DEFAULT_SIZE = 1;
112 
113     private static final long serialVersionUID = -5994465067210009656L;
114 
115     /**
116      * Constructs an Rdn from the given attribute set. See
117      * {@link javax.naming.directory.Attributes Attributes}.
118      * <p>
119      * The string attribute values are not interpreted as
120      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
121      * formatted RDN strings. That is, the values are used
122      * literally (not parsed) and assumed to be unescaped.
123      *
124      * @param attrSet The non-null and non-empty attributes containing
125      * type/value mappings.
126      * @throws InvalidNameException If contents of <tt>attrSet</tt> cannot
127      *          be used to construct a valid RDN.
128      */
129     public Rdn(Attributes attrSet) throws InvalidNameException {
130         if (attrSet.size() == 0) {
131             throw new InvalidNameException("Attributes cannot be empty");
132         }
133         entries = new ArrayList<>(attrSet.size());
134         NamingEnumeration<? extends Attribute> attrs = attrSet.getAll();
135         try {
136             for (int nEntries = 0; attrs.hasMore(); nEntries++) {
137                 RdnEntry entry = new RdnEntry();
138                 Attribute attr = attrs.next();
139                 entry.type = attr.getID();
140                 entry.value = attr.get();
141                 entries.add(nEntries, entry);
142             }
143         } catch (NamingException e) {
144             InvalidNameException e2 = new InvalidNameException(
145                                         e.getMessage());
146             e2.initCause(e);
147             throw e2;
148         }
149         sort(); // arrange entries for comparison
150     }
151 
152     /**
153      * Constructs an Rdn from the given string.
154      * This constructor takes a string formatted according to the rules
155      * defined in <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
156      * and described in the class description for
157      * {@link javax.naming.ldap.LdapName}.
158      *
159      * @param rdnString The non-null and non-empty RFC2253 formatted string.
160      * @throws InvalidNameException If a syntax error occurs during
161      *                  parsing of the rdnString.
162      */
163     public Rdn(String rdnString) throws InvalidNameException {
164         entries = new ArrayList<>(DEFAULT_SIZE);
165         (new Rfc2253Parser(rdnString)).parseRdn(this);
166     }
167 
168     /**
169      * Constructs an Rdn from the given <tt>rdn</tt>.
170      * The contents of the <tt>rdn</tt> are simply copied into the newly
171      * created Rdn.
172      * @param rdn The non-null Rdn to be copied.
173      */
174     public Rdn(Rdn rdn) {
175         entries = new ArrayList<>(rdn.entries.size());
176         entries.addAll(rdn.entries);
177     }
178 
179     /**
180      * Constructs an Rdn from the given attribute type and
181      * value.
182      * The string attribute values are not interpreted as
183      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
184      * formatted RDN strings. That is, the values are used
185      * literally (not parsed) and assumed to be unescaped.
186      *
187      * @param type The non-null and non-empty string attribute type.
188      * @param value The non-null and non-empty attribute value.
189      * @throws InvalidNameException If type/value cannot be used to
190      *                  construct a valid RDN.
191      * @see #toString()
192      */
193     public Rdn(String type, Object value) throws InvalidNameException {
194         if (value == null) {
195             throw new NullPointerException("Cannot set value to null");
196         }
197         if (type.equals("") || isEmptyValue(value)) {
198             throw new InvalidNameException(
199                 "type or value cannot be empty, type:" + type +
200                 " value:" + value);
201         }
202         entries = new ArrayList<>(DEFAULT_SIZE);
203         put(type, value);
204     }
205 
206     private boolean isEmptyValue(Object val) {
207         return ((val instanceof String) && val.equals("")) ||
208         ((val instanceof byte[]) && (((byte[]) val).length == 0));
209     }
210 
211     // An empty constructor used by the parser
212     Rdn() {
213         entries = new ArrayList<>(DEFAULT_SIZE);
214     }
215 
216     /*
217      * Adds the given attribute type and value to this Rdn.
218      * The string attribute values are not interpreted as
219      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
220      * formatted RDN strings. That is the values are used
221      * literally (not parsed) and assumed to be unescaped.
222      *
223      * @param type The non-null and non-empty string attribute type.
224      * @param value The non-null and non-empty attribute value.
225      * @return The updated Rdn, not a new one. Cannot be null.
226      * @see #toString()
227      */
228     Rdn put(String type, Object value) {
229 
230         // create new Entry
231         RdnEntry newEntry = new RdnEntry();
232         newEntry.type =  type;
233         if (value instanceof byte[]) {  // clone the byte array
234             newEntry.value = ((byte[]) value).clone();
235         } else {
236             newEntry.value = value;
237         }
238         entries.add(newEntry);
239         return this;
240     }
241 
242     void sort() {
243         if (entries.size() > 1) {
244             Collections.sort(entries);
245         }
246     }
247 
248     /**
249      * Retrieves one of this Rdn's value.
250      * This is a convenience method for obtaining the value,
251      * when the RDN contains a single type and value mapping,
252      * which is the common RDN usage.
253      * <p>
254      * For a multi-valued RDN, this method returns value corresponding
255      * to the type returned by {@link #getType() getType()} method.
256      *
257      * @return The non-null attribute value.
258      */
259     public Object getValue() {
260         return entries.get(0).getValue();
261     }
262 
263     /**
264      * Retrieves one of this Rdn's type.
265      * This is a convenience method for obtaining the type,
266      * when the RDN contains a single type and value mapping,
267      * which is the common RDN usage.
268      * <p>
269      * For a multi-valued RDN, the type/value pairs have
270      * no specific order defined on them. In that case, this method
271      * returns type of one of the type/value pairs.
272      * The {@link #getValue() getValue()} method returns the
273      * value corresponding to the type returned by this method.
274      *
275      * @return The non-null attribute type.
276      */
277     public String getType() {
278         return entries.get(0).getType();
279     }
280 
281     /**
282      * Returns this Rdn as a string represented in a format defined by
283      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a> and described
284      * in the class description for {@link javax.naming.ldap.LdapName LdapName}.
285      *
286      * @return The string representation of the Rdn.
287      */
288     public String toString() {
289         StringBuilder builder = new StringBuilder();
290         int size = entries.size();
291         if (size > 0) {
292             builder.append(entries.get(0));
293         }
294         for (int next = 1; next < size; next++) {
295             builder.append('+');
296             builder.append(entries.get(next));
297         }
298         return builder.toString();
299     }
300 
301     /**
302      * Compares this Rdn with the specified Object for order.
303      * Returns a negative integer, zero, or a positive integer as this
304      * Rdn is less than, equal to, or greater than the given Object.
305      * <p>
306      * If obj is null or not an instance of Rdn, ClassCastException
307      * is thrown.
308      * <p>
309      * The attribute type and value pairs of the RDNs are lined up
310      * against each other and compared lexicographically. The order of
311      * components in multi-valued Rdns (such as "ou=Sales+cn=Bob") is not
312      * significant.
313      *
314      * @param obj The non-null object to compare against.
315      * @return  A negative integer, zero, or a positive integer as this Rdn
316      *          is less than, equal to, or greater than the given Object.
317      * @exception ClassCastException if obj is null or not a Rdn.
318      */
319     public int compareTo(Object obj) {
320         if (!(obj instanceof Rdn)) {
321             throw new ClassCastException("The obj is not a Rdn");
322         }
323         if (obj == this) {
324             return 0;
325         }
326         Rdn that = (Rdn) obj;
327         int minSize = Math.min(entries.size(), that.entries.size());
328         for (int i = 0; i < minSize; i++) {
329 
330             // Compare a single pair of type/value pairs.
331             int diff = entries.get(i).compareTo(that.entries.get(i));
332             if (diff != 0) {
333                 return diff;
334             }
335         }
336         return (entries.size() - that.entries.size());  // longer RDN wins
337     }
338 
339     /**
340      * Compares the specified Object with this Rdn for equality.
341      * Returns true if the given object is also a Rdn and the two Rdns
342      * represent the same attribute type and value mappings. The order of
343      * components in multi-valued Rdns (such as "ou=Sales+cn=Bob") is not
344      * significant.
345      * <p>
346      * Type and value equality matching is done as below:
347      * <ul>
348      * <li> The types are compared for equality with their case ignored.
349      * <li> String values with different but equivalent usage of quoting,
350      * escaping, or UTF8-hex-encoding are considered equal.
351      * The case of the values is ignored during the comparison.
352      * </ul>
353      * <p>
354      * If obj is null or not an instance of Rdn, false is returned.
355      * <p>
356      * @param obj object to be compared for equality with this Rdn.
357      * @return true if the specified object is equal to this Rdn.
358      * @see #hashCode()
359      */
360     public boolean equals(Object obj) {
361         if (obj == this) {
362             return true;
363         }
364         if (!(obj instanceof Rdn)) {
365             return false;
366         }
367         Rdn that = (Rdn) obj;
368         if (entries.size() != that.size()) {
369             return false;
370         }
371         for (int i = 0; i < entries.size(); i++) {
372             if (!entries.get(i).equals(that.entries.get(i))) {
373                 return false;
374             }
375         }
376         return true;
377     }
378 
379     /**
380      * Returns the hash code of this RDN. Two RDNs that are
381      * equal (according to the equals method) will have the same
382      * hash code.
383      *
384      * @return An int representing the hash code of this Rdn.
385      * @see #equals
386      */
387     public int hashCode() {
388 
389         // Sum up the hash codes of the components.
390         int hash = 0;
391 
392         // For each type/value pair...
393         for (int i = 0; i < entries.size(); i++) {
394             hash += entries.get(i).hashCode();
395         }
396         return hash;
397     }
398 
399     /**
400      * Retrieves the {@link javax.naming.directory.Attributes Attributes}
401      * view of the type/value mappings contained in this Rdn.
402      *
403      * @return  The non-null attributes containing the type/value
404      *          mappings of this Rdn.
405      */
406     public Attributes toAttributes() {
407         Attributes attrs = new BasicAttributes(true);
408         for (int i = 0; i < entries.size(); i++) {
409             RdnEntry entry = entries.get(i);
410             Attribute attr = attrs.put(entry.getType(), entry.getValue());
411             if (attr != null) {
412                 attr.add(entry.getValue());
413                 attrs.put(attr);
414             }
415         }
416         return attrs;
417     }
418 
419 
420     private static class RdnEntry implements Comparable<RdnEntry> {
421         private String type;
422         private Object value;
423 
424         // If non-null, a cannonical representation of the value suitable
425         // for comparison using String.compareTo()
426         private String comparable = null;
427 
428         String getType() {
429             return type;
430         }
431 
432         Object getValue() {
433             return value;
434         }
435 
436         public int compareTo(RdnEntry that) {
437             int diff = type.compareToIgnoreCase(that.type);
438             if (diff != 0) {
439                 return diff;
440             }
441             if (value.equals(that.value)) {     // try shortcut
442                 return 0;
443             }
444             return getValueComparable().compareTo(
445                         that.getValueComparable());
446         }
447 
448         public boolean equals(Object obj) {
449             if (obj == this) {
450                 return true;
451             }
452             if (!(obj instanceof RdnEntry)) {
453                 return false;
454             }
455 
456             // Any change here must be reflected in hashCode()
457             RdnEntry that = (RdnEntry) obj;
458             return (type.equalsIgnoreCase(that.type)) &&
459                         (getValueComparable().equals(
460                         that.getValueComparable()));
461         }
462 
463         public int hashCode() {
464             return (type.toUpperCase(Locale.ENGLISH).hashCode() +
465                 getValueComparable().hashCode());
466         }
467 
468         public String toString() {
469             return type + "=" + escapeValue(value);
470         }
471 
472         private String getValueComparable() {
473             if (comparable != null) {
474                 return comparable;              // return cached result
475             }
476 
477             // cache result
478             if (value instanceof byte[]) {
479                 comparable = escapeBinaryValue((byte[]) value);
480             } else {
481                 comparable = ((String) value).toUpperCase(Locale.ENGLISH);
482             }
483             return comparable;
484         }
485     }
486 
487     /**
488      * Retrieves the number of attribute type/value pairs in this Rdn.
489      * @return The non-negative number of type/value pairs in this Rdn.
490      */
491     public int size() {
492         return entries.size();
493     }
494 
495     /**
496      * Given the value of an attribute, returns a string escaped according
497      * to the rules specified in
498      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.
499      * <p>
500      * For example, if the val is "Sue, Grabbit and Runn", the escaped
501      * value returned by this method is "Sue\, Grabbit and Runn".
502      * <p>
503      * A string value is represented as a String and binary value
504      * as a byte array.
505      *
506      * @param val The non-null object to be escaped.
507      * @return Escaped string value.
508      * @throws ClassCastException if val is is not a String or byte array.
509      */
510     public static String escapeValue(Object val) {
511         return (val instanceof byte[])
512                 ? escapeBinaryValue((byte[])val)
513                 : escapeStringValue((String)val);
514     }
515 
516     /*
517      * Given the value of a string-valued attribute, returns a
518      * string suitable for inclusion in a DN.  This is accomplished by
519      * using backslash (\) to escape the following characters:
520      *  leading and trailing whitespace
521      *  , = + < > # ; " \
522      */
523     private static final String escapees = ",=+<>#;\"\\";
524 
525     private static String escapeStringValue(String val) {
526 
527             char[] chars = val.toCharArray();
528             StringBuilder builder = new StringBuilder(2 * val.length());
529 
530             // Find leading and trailing whitespace.
531             int lead;   // index of first char that is not leading whitespace
532             for (lead = 0; lead < chars.length; lead++) {
533                 if (!isWhitespace(chars[lead])) {
534                     break;
535                 }
536             }
537             int trail;  // index of last char that is not trailing whitespace
538             for (trail = chars.length - 1; trail >= 0; trail--) {
539                 if (!isWhitespace(chars[trail])) {
540                     break;
541                 }
542             }
543 
544             for (int i = 0; i < chars.length; i++) {
545                 char c = chars[i];
546                 if ((i < lead) || (i > trail) || (escapees.indexOf(c) >= 0)) {
547                     builder.append('\\');
548                 }
549                 builder.append(c);
550             }
551             return builder.toString();
552     }
553 
554     /*
555      * Given the value of a binary attribute, returns a string
556      * suitable for inclusion in a DN (such as "#CEB1DF80").
557      * TBD: This method should actually generate the ber encoding
558      * of the binary value
559      */
560     private static String escapeBinaryValue(byte[] val) {
561 
562         StringBuilder builder = new StringBuilder(1 + 2 * val.length);
563         builder.append("#");
564 
565         for (int i = 0; i < val.length; i++) {
566             byte b = val[i];
567             builder.append(Character.forDigit(0xF & (b >>> 4), 16));
568             builder.append(Character.forDigit(0xF & b, 16));
569         }
570         return builder.toString();
571     }
572 
573     /**
574      * Given an attribute value string formated according to the rules
575      * specified in
576      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>,
577      * returns the unformated value.  Escapes and quotes are
578      * stripped away, and hex-encoded UTF-8 is converted to equivalent
579      * UTF-16 characters. Returns a string value as a String, and a
580      * binary value as a byte array.
581      * <p>
582      * Legal and illegal values are defined in RFC 2253.
583      * This method is generous in accepting the values and does not
584      * catch all illegal values.
585      * Therefore, passing in an illegal value might not necessarily
586      * trigger an <tt>IllegalArgumentException</tt>.
587      *
588      * @param   val     The non-null string to be unescaped.
589      * @return          Unescaped value.
590      * @throws          IllegalArgumentException When an Illegal value
591      *                  is provided.
592      */
593     public static Object unescapeValue(String val) {
594 
595             char[] chars = val.toCharArray();
596             int beg = 0;
597             int end = chars.length;
598 
599             // Trim off leading and trailing whitespace.
600             while ((beg < end) && isWhitespace(chars[beg])) {
601                 ++beg;
602             }
603 
604             while ((beg < end) && isWhitespace(chars[end - 1])) {
605                 --end;
606             }
607 
608             // Add back the trailing whitespace with a preceding '\'
609             // (escaped or unescaped) that was taken off in the above
610             // loop. Whether or not to retain this whitespace is decided below.
611             if (end != chars.length &&
612                     (beg < end) &&
613                     chars[end - 1] == '\\') {
614                 end++;
615             }
616             if (beg >= end) {
617                 return "";
618             }
619 
620             if (chars[beg] == '#') {
621                 // Value is binary (eg: "#CEB1DF80").
622                 return decodeHexPairs(chars, ++beg, end);
623             }
624 
625             // Trim off quotes.
626             if ((chars[beg] == '\"') && (chars[end - 1] == '\"')) {
627                 ++beg;
628                 --end;
629             }
630 
631             StringBuilder builder = new StringBuilder(end - beg);
632             int esc = -1; // index of the last escaped character
633 
634             for (int i = beg; i < end; i++) {
635                 if ((chars[i] == '\\') && (i + 1 < end)) {
636                     if (!Character.isLetterOrDigit(chars[i + 1])) {
637                         ++i;                            // skip backslash
638                         builder.append(chars[i]);       // snarf escaped char
639                         esc = i;
640                     } else {
641 
642                         // Convert hex-encoded UTF-8 to 16-bit chars.
643                         byte[] utf8 = getUtf8Octets(chars, i, end);
644                         if (utf8.length > 0) {
645                             try {
646                                 builder.append(new String(utf8, "UTF8"));
647                             } catch (java.io.UnsupportedEncodingException e) {
648                                 // shouldn't happen
649                             }
650                             i += utf8.length * 3 - 1;
651                         } else { // no utf8 bytes available, invalid DN
652 
653                             // '/' has no meaning, throw exception
654                             throw new IllegalArgumentException(
655                                 "Not a valid attribute string value:" +
656                                 val + ",improper usage of backslash");
657                         }
658                     }
659                 } else {
660                     builder.append(chars[i]);   // snarf unescaped char
661                 }
662             }
663 
664             // Get rid of the unescaped trailing whitespace with the
665             // preceding '\' character that was previously added back.
666             int len = builder.length();
667             if (isWhitespace(builder.charAt(len - 1)) && esc != (end - 1)) {
668                 builder.setLength(len - 1);
669             }
670             return builder.toString();
671         }
672 
673 
674         /*
675          * Given an array of chars (with starting and ending indexes into it)
676          * representing bytes encoded as hex-pairs (such as "CEB1DF80"),
677          * returns a byte array containing the decoded bytes.
678          */
679         private static byte[] decodeHexPairs(char[] chars, int beg, int end) {
680             byte[] bytes = new byte[(end - beg) / 2];
681             for (int i = 0; beg + 1 < end; i++) {
682                 int hi = Character.digit(chars[beg], 16);
683                 int lo = Character.digit(chars[beg + 1], 16);
684                 if (hi < 0 || lo < 0) {
685                     break;
686                 }
687                 bytes[i] = (byte)((hi<<4) + lo);
688                 beg += 2;
689             }
690             if (beg != end) {
691                 throw new IllegalArgumentException(
692                         "Illegal attribute value: " + new String(chars));
693             }
694             return bytes;
695         }
696 
697         /*
698          * Given an array of chars (with starting and ending indexes into it),
699          * finds the largest prefix consisting of hex-encoded UTF-8 octets,
700          * and returns a byte array containing the corresponding UTF-8 octets.
701          *
702          * Hex-encoded UTF-8 octets look like this:
703          *      \03\B1\DF\80
704          */
705         private static byte[] getUtf8Octets(char[] chars, int beg, int end) {
706             byte[] utf8 = new byte[(end - beg) / 3];    // allow enough room
707             int len = 0;        // index of first unused byte in utf8
708 
709             while ((beg + 2 < end) &&
710                    (chars[beg++] == '\\')) {
711                 int hi = Character.digit(chars[beg++], 16);
712                 int lo = Character.digit(chars[beg++], 16);
713                 if (hi < 0 || lo < 0) {
714                    break;
715                 }
716                 utf8[len++] = (byte)((hi<<4) + lo);
717             }
718             if (len == utf8.length) {
719                 return utf8;
720             } else {
721                 byte[] res = new byte[len];
722                 System.arraycopy(utf8, 0, res, 0, len);
723                 return res;
724             }
725         }
726 
727     /*
728      * Best guess as to what RFC 2253 means by "whitespace".
729      */
730     private static boolean isWhitespace(char c) {
731         return (c == ' ' || c == '\r');
732     }
733 
734     /**
735      * Serializes only the unparsed RDN, for compactness and to avoid
736      * any implementation dependency.
737      *
738      * @serialData      The RDN string
739      */
740     private void writeObject(ObjectOutputStream s)
741             throws java.io.IOException {
742         s.defaultWriteObject();
743         s.writeObject(toString());
744     }
745 
746     private void readObject(ObjectInputStream s)
747             throws IOException, ClassNotFoundException {
748         s.defaultReadObject();
749         entries = new ArrayList<>(DEFAULT_SIZE);
750         String unparsed = (String) s.readObject();
751         try {
752             (new Rfc2253Parser(unparsed)).parseRdn(this);
753         } catch (InvalidNameException e) {
754             // shouldn't happen
755             throw new java.io.StreamCorruptedException(
756                     "Invalid name: " + unparsed);
757         }
758     }
759 }