001 /*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 UnboundID Corp.
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 */
021 package com.unboundid.ldap.sdk;
022
023
024
025 import java.io.Serializable;
026 import java.util.ArrayList;
027 import java.util.Comparator;
028 import java.util.List;
029
030 import com.unboundid.asn1.ASN1OctetString;
031 import com.unboundid.ldap.sdk.schema.Schema;
032 import com.unboundid.util.NotMutable;
033 import com.unboundid.util.ThreadSafety;
034 import com.unboundid.util.ThreadSafetyLevel;
035
036 import static com.unboundid.ldap.sdk.LDAPMessages.*;
037 import static com.unboundid.util.Validator.*;
038
039
040
041 /**
042 * This class provides a data structure for holding information about an LDAP
043 * distinguished name (DN). A DN consists of a comma-delimited list of zero or
044 * more RDN components. See
045 * <A HREF="http://www.ietf.org/rfc/rfc4514.txt">RFC 4514</A> for more
046 * information about representing DNs and RDNs as strings.
047 * <BR><BR>
048 * Examples of valid DNs (excluding the quotation marks, which are provided for
049 * clarity) include:
050 * <UL>
051 * <LI>"" -- This is the zero-length DN (also called the null DN), which may
052 * be used to refer to the directory server root DSE.</LI>
053 * <LI>"{@code o=example.com}". This is a DN with a single, single-valued
054 * RDN. The RDN attribute is "{@code o}" and the RDN value is
055 * "{@code example.com}".</LI>
056 * <LI>"{@code givenName=John+sn=Doe,ou=People,dc=example,dc=com}". This is a
057 * DN with four different RDNs ("{@code givenName=John+sn=Doe"},
058 * "{@code ou=People}", "{@code dc=example}", and "{@code dc=com}". The
059 * first RDN is multivalued with attribute-value pairs of
060 * "{@code givenName=John}" and "{@code sn=Doe}".</LI>
061 * </UL>
062 * Note that there is some inherent ambiguity in the string representations of
063 * distinguished names. In particular, there may be differences in spacing
064 * (particularly around commas and equal signs, as well as plus signs in
065 * multivalued RDNs), and also differences in capitalization in attribute names
066 * and/or values. For example, the strings
067 * "{@code uid=john.doe,ou=people,dc=example,dc=com}" and
068 * "{@code UID = JOHN.DOE , OU = PEOPLE , DC = EXAMPLE , DC = COM}" actually
069 * refer to the same distinguished name. To deal with these differences, the
070 * normalized representation may be used. The normalized representation is a
071 * standardized way of representing a DN, and it is obtained by eliminating any
072 * unnecessary spaces and converting all non-case-sensitive characters to
073 * lowercase. The normalized representation of a DN may be obtained using the
074 * {@link DN#toNormalizedString} method, and two DNs may be compared to
075 * determine if they are equal using the standard {@link DN#equals} method.
076 * <BR><BR>
077 * Distinguished names are hierarchical. The rightmost RDN refers to the root
078 * of the directory information tree (DIT), and each successive RDN to the left
079 * indicates the addition of another level of hierarchy. For example, in the
080 * DN "{@code uid=john.doe,ou=People,o=example.com}", the entry
081 * "{@code o=example.com}" is at the root of the DIT, the entry
082 * "{@code ou=People,o=example.com}" is an immediate descendant of the
083 * "{@code o=example.com}" entry, and the
084 * "{@code uid=john.doe,ou=People,o=example.com}" entry is an immediate
085 * descendant of the "{@code ou=People,o=example.com}" entry. Similarly, the
086 * entry "{@code uid=jane.doe,ou=People,o=example.com}" would be considered a
087 * peer of the "{@code uid=john.doe,ou=People,o=example.com}" entry because they
088 * have the same parent.
089 * <BR><BR>
090 * Note that in some cases, the root of the DIT may actually contain a DN with
091 * multiple RDNs. For example, in the DN
092 * "{@code uid=john.doe,ou=People,dc=example,dc=com}", the directory server may
093 * or may not actually have a "{@code dc=com}" entry. In many such cases, the
094 * base entry may actually be just "{@code dc=example,dc=com}". The DNs of the
095 * entries that are at the base of the directory information tree are called
096 * "naming contexts" or "suffixes" and they are generally available in the
097 * {@code namingContexts} attribute of the root DSE. See the {@link RootDSE}
098 * class for more information about interacting with the server root DSE.
099 * <BR><BR>
100 * This class provides methods for making determinations based on the
101 * hierarchical relationships of DNs. For example, the
102 * {@link DN#isAncestorOf} and {@link DN#isDescendantOf} methods may be used to
103 * determine whether two DNs have a hierarchical relationship. In addition,
104 * this class implements the {@link Comparable} and {@link Comparator}
105 * interfaces so that it may be used to easily sort DNs (ancestors will always
106 * be sorted before descendants, and peers will always be sorted
107 * lexicographically based on their normalized representations).
108 */
109 @NotMutable()
110 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
111 public final class DN
112 implements Comparable<DN>, Comparator<DN>, Serializable
113 {
114 /**
115 * The RDN array that will be used for the null DN.
116 */
117 private static final RDN[] NO_RDNS = new RDN[0];
118
119
120
121 /**
122 * A pre-allocated DN object equivalent to the null DN.
123 */
124 public static final DN NULL_DN = new DN();
125
126
127
128 /**
129 * The serial version UID for this serializable class.
130 */
131 private static final long serialVersionUID = -5272968942085729346L;
132
133
134
135 // The set of RDN components that make up this DN.
136 private final RDN[] rdns;
137
138 // The schema to use to generate the normalized string representation of this
139 // DN, if any.
140 private final Schema schema;
141
142 // The string representation of this DN.
143 private final String dnString;
144
145 // The normalized string representation of this DN.
146 private volatile String normalizedString;
147
148
149
150 /**
151 * Creates a new DN with the provided set of RDNs.
152 *
153 * @param rdns The RDN components for this DN. It must not be {@code null}.
154 */
155 public DN(final RDN... rdns)
156 {
157 ensureNotNull(rdns);
158
159 this.rdns = rdns;
160 if (rdns.length == 0)
161 {
162 dnString = "";
163 normalizedString = "";
164 schema = null;
165 }
166 else
167 {
168 Schema s = null;
169 final StringBuilder buffer = new StringBuilder();
170 for (final RDN rdn : rdns)
171 {
172 if (buffer.length() > 0)
173 {
174 buffer.append(',');
175 }
176 rdn.toString(buffer, false);
177
178 if (s == null)
179 {
180 s = rdn.getSchema();
181 }
182 }
183
184 dnString = buffer.toString();
185 schema = s;
186 }
187 }
188
189
190
191 /**
192 * Creates a new DN with the provided set of RDNs.
193 *
194 * @param rdns The RDN components for this DN. It must not be {@code null}.
195 */
196 public DN(final List<RDN> rdns)
197 {
198 ensureNotNull(rdns);
199
200 if (rdns.isEmpty())
201 {
202 this.rdns = NO_RDNS;
203 dnString = "";
204 normalizedString = "";
205 schema = null;
206 }
207 else
208 {
209 this.rdns = rdns.toArray(new RDN[rdns.size()]);
210
211 Schema s = null;
212 final StringBuilder buffer = new StringBuilder();
213 for (final RDN rdn : this.rdns)
214 {
215 if (buffer.length() > 0)
216 {
217 buffer.append(',');
218 }
219 rdn.toString(buffer, false);
220
221 if (s == null)
222 {
223 s = rdn.getSchema();
224 }
225 }
226
227 dnString = buffer.toString();
228 schema = s;
229 }
230 }
231
232
233
234 /**
235 * Creates a new DN below the provided parent DN with the given RDN.
236 *
237 * @param rdn The RDN for the new DN. It must not be {@code null}.
238 * @param parentDN The parent DN for the new DN to create. It must not be
239 * {@code null}.
240 */
241 public DN(final RDN rdn, final DN parentDN)
242 {
243 ensureNotNull(rdn, parentDN);
244
245 rdns = new RDN[parentDN.rdns.length + 1];
246 rdns[0] = rdn;
247 System.arraycopy(parentDN.rdns, 0, rdns, 1, parentDN.rdns.length);
248
249 Schema s = null;
250 final StringBuilder buffer = new StringBuilder();
251 for (final RDN r : rdns)
252 {
253 if (buffer.length() > 0)
254 {
255 buffer.append(',');
256 }
257 r.toString(buffer, false);
258
259 if (s == null)
260 {
261 s = r.getSchema();
262 }
263 }
264
265 dnString = buffer.toString();
266 schema = s;
267 }
268
269
270
271 /**
272 * Creates a new DN from the provided string representation.
273 *
274 * @param dnString The string representation to use to create this DN. It
275 * must not be {@code null}.
276 *
277 * @throws LDAPException If the provided string cannot be parsed as a valid
278 * DN.
279 */
280 public DN(final String dnString)
281 throws LDAPException
282 {
283 this(dnString, null);
284 }
285
286
287
288 /**
289 * Creates a new DN from the provided string representation.
290 *
291 * @param dnString The string representation to use to create this DN. It
292 * must not be {@code null}.
293 * @param schema The schema to use to generate the normalized string
294 * representation of this DN. It may be {@code null} if no
295 * schema is available.
296 *
297 * @throws LDAPException If the provided string cannot be parsed as a valid
298 * DN.
299 */
300 public DN(final String dnString, final Schema schema)
301 throws LDAPException
302 {
303 ensureNotNull(dnString);
304
305 this.dnString = dnString;
306 this.schema = schema;
307
308 final ArrayList<RDN> rdnList = new ArrayList<RDN>(5);
309
310 final int length = dnString.length();
311 if (length == 0)
312 {
313 rdns = NO_RDNS;
314 normalizedString = "";
315 return;
316 }
317
318 int pos = 0;
319 boolean expectMore = false;
320 rdnLoop:
321 while (pos < length)
322 {
323 // Skip over any spaces before the attribute name.
324 while ((pos < length) && (dnString.charAt(pos) == ' '))
325 {
326 pos++;
327 }
328
329 if (pos >= length)
330 {
331 // This is only acceptable if we haven't read anything yet.
332 if (rdnList.isEmpty())
333 {
334 break;
335 }
336 else
337 {
338 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
339 ERR_DN_ENDS_WITH_COMMA.get());
340 }
341 }
342
343 // Read the attribute name, until we find a space or equal sign.
344 int rdnEndPos;
345 int rdnStartPos = pos;
346 int attrStartPos = pos;
347 while (pos < length)
348 {
349 final char c = dnString.charAt(pos);
350 if ((c == ' ') || (c == '='))
351 {
352 break;
353 }
354 else if ((c == ',') || (c == ';'))
355 {
356 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
357 ERR_DN_UNEXPECTED_COMMA.get(pos));
358 }
359
360 pos++;
361 }
362
363 String attrName = dnString.substring(attrStartPos, pos);
364 if (attrName.length() == 0)
365 {
366 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
367 ERR_DN_NO_ATTR_IN_RDN.get());
368 }
369
370
371 // Skip over any spaces before the equal sign.
372 while ((pos < length) && (dnString.charAt(pos) == ' '))
373 {
374 pos++;
375 }
376
377 if ((pos >= length) || (dnString.charAt(pos) != '='))
378 {
379 // We didn't find an equal sign.
380 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
381 ERR_DN_NO_EQUAL_SIGN.get(attrName));
382 }
383
384 // Skip over the equal sign, and then any spaces leading up to the
385 // attribute value.
386 pos++;
387 while ((pos < length) && (dnString.charAt(pos) == ' '))
388 {
389 pos++;
390 }
391
392
393 // Read the value for this RDN component.
394 ASN1OctetString value;
395 if (pos >= length)
396 {
397 value = new ASN1OctetString();
398 rdnEndPos = pos;
399 }
400 else if (dnString.charAt(pos) == '#')
401 {
402 // It is a hex-encoded value, so we'll read until we find the end of the
403 // string or the first non-hex character, which must be a space, a
404 // comma, or a plus sign.
405 final byte[] valueArray = RDN.readHexString(dnString, ++pos);
406 value = new ASN1OctetString(valueArray);
407 pos += (valueArray.length * 2);
408 rdnEndPos = pos;
409 }
410 else
411 {
412 // It is a string value, which potentially includes escaped characters.
413 final StringBuilder buffer = new StringBuilder();
414 pos = RDN.readValueString(dnString, pos, buffer);
415 value = new ASN1OctetString(buffer.toString());
416 rdnEndPos = pos;
417 }
418
419
420 // Skip over any spaces until we find a comma, a plus sign, or the end of
421 // the value.
422 while ((pos < length) && (dnString.charAt(pos) == ' '))
423 {
424 pos++;
425 }
426
427 if (pos >= length)
428 {
429 // It's a single-valued RDN, and we're at the end of the DN.
430 rdnList.add(new RDN(attrName, value, schema,
431 getTrimmedRDN(dnString, rdnStartPos,rdnEndPos)));
432 expectMore = false;
433 break;
434 }
435
436 switch (dnString.charAt(pos))
437 {
438 case '+':
439 // It is a multivalued RDN, so we're not done reading either the DN
440 // or the RDN.
441 pos++;
442 break;
443
444 case ',':
445 case ';':
446 // We hit the end of the single-valued RDN, but there's still more of
447 // the DN to be read.
448 rdnList.add(new RDN(attrName, value, schema,
449 getTrimmedRDN(dnString, rdnStartPos,rdnEndPos)));
450 pos++;
451 expectMore = true;
452 continue rdnLoop;
453
454 default:
455 // It's an illegal character. This should never happen.
456 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
457 ERR_DN_UNEXPECTED_CHAR.get(
458 dnString.charAt(pos), pos));
459 }
460
461 if (pos >= length)
462 {
463 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
464 ERR_DN_ENDS_WITH_PLUS.get());
465 }
466
467
468 // If we've gotten here, then we're dealing with a multivalued RDN.
469 // Create lists to hold the names and values, and then loop until we hit
470 // the end of the RDN.
471 final ArrayList<String> nameList = new ArrayList<String>(5);
472 final ArrayList<ASN1OctetString> valueList =
473 new ArrayList<ASN1OctetString>(5);
474 nameList.add(attrName);
475 valueList.add(value);
476
477 while (pos < length)
478 {
479 // Skip over any spaces before the attribute name.
480 while ((pos < length) && (dnString.charAt(pos) == ' '))
481 {
482 pos++;
483 }
484
485 if (pos >= length)
486 {
487 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
488 ERR_DN_ENDS_WITH_PLUS.get());
489 }
490
491 // Read the attribute name, until we find a space or equal sign.
492 attrStartPos = pos;
493 while (pos < length)
494 {
495 final char c = dnString.charAt(pos);
496 if ((c == ' ') || (c == '='))
497 {
498 break;
499 }
500 else if ((c == ',') || (c == ';'))
501 {
502 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
503 ERR_DN_UNEXPECTED_COMMA.get(pos));
504 }
505
506 pos++;
507 }
508
509 attrName = dnString.substring(attrStartPos, pos);
510 if (attrName.length() == 0)
511 {
512 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
513 ERR_DN_NO_ATTR_IN_RDN.get());
514 }
515
516
517 // Skip over any spaces before the equal sign.
518 while ((pos < length) && (dnString.charAt(pos) == ' '))
519 {
520 pos++;
521 }
522
523 if ((pos >= length) || (dnString.charAt(pos) != '='))
524 {
525 // We didn't find an equal sign.
526 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
527 ERR_DN_NO_EQUAL_SIGN.get(attrName));
528 }
529
530 // Skip over the equal sign, and then any spaces leading up to the
531 // attribute value.
532 pos++;
533 while ((pos < length) && (dnString.charAt(pos) == ' '))
534 {
535 pos++;
536 }
537
538
539 // Read the value for this RDN component.
540 if (pos >= length)
541 {
542 value = new ASN1OctetString();
543 rdnEndPos = pos;
544 }
545 else if (dnString.charAt(pos) == '#')
546 {
547 // It is a hex-encoded value, so we'll read until we find the end of
548 // the string or the first non-hex character, which must be a space, a
549 // comma, or a plus sign.
550 final byte[] valueArray = RDN.readHexString(dnString, ++pos);
551 value = new ASN1OctetString(valueArray);
552 pos += (valueArray.length * 2);
553 rdnEndPos = pos;
554 }
555 else
556 {
557 // It is a string value, which potentially includes escaped
558 // characters.
559 final StringBuilder buffer = new StringBuilder();
560 pos = RDN.readValueString(dnString, pos, buffer);
561 value = new ASN1OctetString(buffer.toString());
562 rdnEndPos = pos;
563 }
564
565
566 // Skip over any spaces until we find a comma, a plus sign, or the end
567 // of the value.
568 while ((pos < length) && (dnString.charAt(pos) == ' '))
569 {
570 pos++;
571 }
572
573 nameList.add(attrName);
574 valueList.add(value);
575
576 if (pos >= length)
577 {
578 // We've hit the end of the RDN and the end of the DN.
579 final String[] names = nameList.toArray(new String[nameList.size()]);
580 final ASN1OctetString[] values =
581 valueList.toArray(new ASN1OctetString[valueList.size()]);
582 rdnList.add(new RDN(names, values, schema,
583 getTrimmedRDN(dnString, rdnStartPos,rdnEndPos)));
584 expectMore = false;
585 break rdnLoop;
586 }
587
588 switch (dnString.charAt(pos))
589 {
590 case '+':
591 // There are still more RDN components to be read, so we're not done
592 // yet.
593 pos++;
594
595 if (pos >= length)
596 {
597 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
598 ERR_DN_ENDS_WITH_PLUS.get());
599 }
600 break;
601
602 case ',':
603 case ';':
604 // We've hit the end of the RDN, but there is still more of the DN
605 // to be read.
606 final String[] names =
607 nameList.toArray(new String[nameList.size()]);
608 final ASN1OctetString[] values =
609 valueList.toArray(new ASN1OctetString[valueList.size()]);
610 rdnList.add(new RDN(names, values, schema,
611 getTrimmedRDN(dnString, rdnStartPos,rdnEndPos)));
612 pos++;
613 expectMore = true;
614 continue rdnLoop;
615
616 default:
617 // It's an illegal character. This should never happen.
618 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
619 ERR_DN_UNEXPECTED_CHAR.get(
620 dnString.charAt(pos), pos));
621 }
622 }
623 }
624
625 // If we are expecting more information to be provided, then it means that
626 // the string ended with a comma or semicolon.
627 if (expectMore)
628 {
629 throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
630 ERR_DN_ENDS_WITH_COMMA.get());
631 }
632
633 // At this point, we should have all of the RDNs to use to create this DN.
634 rdns = new RDN[rdnList.size()];
635 rdnList.toArray(rdns);
636 }
637
638
639
640 /**
641 * Retrieves a trimmed version of the string representation of the RDN in the
642 * specified portion of the provided DN string. Only non-escaped trailing
643 * spaces will be removed.
644 *
645 * @param dnString The string representation of the DN from which to extract
646 * the string representation of the RDN.
647 * @param start The position of the first character in the RDN.
648 * @param end The position marking the end of the RDN.
649 *
650 * @return A properly-trimmed string representation of the RDN.
651 */
652 private static String getTrimmedRDN(final String dnString, final int start,
653 final int end)
654 {
655 final String rdnString = dnString.substring(start, end);
656 if (! rdnString.endsWith(" "))
657 {
658 return rdnString;
659 }
660
661 final StringBuilder buffer = new StringBuilder(rdnString);
662 while ((buffer.charAt(buffer.length() - 1) == ' ') &&
663 (buffer.charAt(buffer.length() - 2) != '\\'))
664 {
665 buffer.setLength(buffer.length() - 1);
666 }
667
668 return buffer.toString();
669 }
670
671
672
673 /**
674 * Indicates whether the provided string represents a valid DN.
675 *
676 * @param s The string for which to make the determination. It must not be
677 * {@code null}.
678 *
679 * @return {@code true} if the provided string represents a valid DN, or
680 * {@code false} if not.
681 */
682 public static boolean isValidDN(final String s)
683 {
684 try
685 {
686 new DN(s);
687 return true;
688 }
689 catch (LDAPException le)
690 {
691 return false;
692 }
693 }
694
695
696
697
698 /**
699 * Retrieves the leftmost (i.e., furthest from the naming context) RDN
700 * component for this DN.
701 *
702 * @return The leftmost RDN component for this DN, or {@code null} if this DN
703 * does not have any RDNs (i.e., it is the null DN).
704 */
705 public RDN getRDN()
706 {
707 if (rdns.length == 0)
708 {
709 return null;
710 }
711 else
712 {
713 return rdns[0];
714 }
715 }
716
717
718
719 /**
720 * Retrieves the string representation of the leftmost (i.e., furthest from
721 * the naming context) RDN component for this DN.
722 *
723 * @return The string representation of the leftmost RDN component for this
724 * DN, or {@code null} if this DN does not have any RDNs (i.e., it is
725 * the null DN).
726 */
727 public String getRDNString()
728 {
729 if (rdns.length == 0)
730 {
731 return null;
732 }
733 else
734 {
735 return rdns[0].toString();
736 }
737 }
738
739
740
741 /**
742 * Retrieves the string representation of the leftmost (i.e., furthest from
743 * the naming context) RDN component for the DN with the provided string
744 * representation.
745 *
746 * @param s The string representation of the DN to process. It must not be
747 * {@code null}.
748 *
749 * @return The string representation of the leftmost RDN component for this
750 * DN, or {@code null} if this DN does not have any RDNs (i.e., it is
751 * the null DN).
752 *
753 * @throws LDAPException If the provided string cannot be parsed as a DN.
754 */
755 public static String getRDNString(final String s)
756 throws LDAPException
757 {
758 return new DN(s).getRDNString();
759 }
760
761
762
763 /**
764 * Retrieves the set of RDNs that comprise this DN.
765 *
766 * @return The set of RDNs that comprise this DN.
767 */
768 public RDN[] getRDNs()
769 {
770 return rdns;
771 }
772
773
774
775 /**
776 * Retrieves the set of RDNs that comprise the DN with the provided string
777 * representation.
778 *
779 * @param s The string representation of the DN for which to retrieve the
780 * RDNs. It must not be {@code null}.
781 *
782 * @return The set of RDNs that comprise the DN with the provided string
783 * representation.
784 *
785 * @throws LDAPException If the provided string cannot be parsed as a DN.
786 */
787 public static RDN[] getRDNs(final String s)
788 throws LDAPException
789 {
790 return new DN(s).getRDNs();
791 }
792
793
794
795 /**
796 * Retrieves the set of string representations of the RDNs that comprise this
797 * DN.
798 *
799 * @return The set of string representations of the RDNs that comprise this
800 * DN.
801 */
802 public String[] getRDNStrings()
803 {
804 final String[] rdnStrings = new String[rdns.length];
805 for (int i=0; i < rdns.length; i++)
806 {
807 rdnStrings[i] = rdns[i].toString();
808 }
809 return rdnStrings;
810 }
811
812
813
814 /**
815 * Retrieves the set of string representations of the RDNs that comprise this
816 * DN.
817 *
818 * @param s The string representation of the DN for which to retrieve the
819 * RDN strings. It must not be {@code null}.
820 *
821 * @return The set of string representations of the RDNs that comprise this
822 * DN.
823 *
824 * @throws LDAPException If the provided string cannot be parsed as a DN.
825 */
826 public static String[] getRDNStrings(final String s)
827 throws LDAPException
828 {
829 return new DN(s).getRDNStrings();
830 }
831
832
833
834 /**
835 * Indicates whether this DN represents the null DN, which does not have any
836 * RDN components.
837 *
838 * @return {@code true} if this DN represents the null DN, or {@code false}
839 * if not.
840 */
841 public boolean isNullDN()
842 {
843 return (rdns.length == 0);
844 }
845
846
847
848 /**
849 * Retrieves the DN that is the parent for this DN. Note that neither the
850 * null DN nor DNs consisting of a single RDN component will be considered to
851 * have parent DNs.
852 *
853 * @return The DN that is the parent for this DN, or {@code null} if there
854 * is no parent.
855 */
856 public DN getParent()
857 {
858 switch (rdns.length)
859 {
860 case 0:
861 case 1:
862 return null;
863
864 case 2:
865 return new DN(rdns[1]);
866
867 case 3:
868 return new DN(rdns[1], rdns[2]);
869
870 case 4:
871 return new DN(rdns[1], rdns[2], rdns[3]);
872
873 case 5:
874 return new DN(rdns[1], rdns[2], rdns[3], rdns[4]);
875
876 default:
877 final RDN[] parentRDNs = new RDN[rdns.length - 1];
878 System.arraycopy(rdns, 1, parentRDNs, 0, parentRDNs.length);
879 return new DN(parentRDNs);
880 }
881 }
882
883
884
885 /**
886 * Retrieves the DN that is the parent for the DN with the provided string
887 * representation. Note that neither the null DN nor DNs consisting of a
888 * single RDN component will be considered to have parent DNs.
889 *
890 * @param s The string representation of the DN for which to retrieve the
891 * parent. It must not be {@code null}.
892 *
893 * @return The DN that is the parent for this DN, or {@code null} if there
894 * is no parent.
895 *
896 * @throws LDAPException If the provided string cannot be parsed as a DN.
897 */
898 public static DN getParent(final String s)
899 throws LDAPException
900 {
901 return new DN(s).getParent();
902 }
903
904
905
906 /**
907 * Retrieves the string representation of the DN that is the parent for this
908 * DN. Note that neither the null DN nor DNs consisting of a single RDN
909 * component will be considered to have parent DNs.
910 *
911 * @return The DN that is the parent for this DN, or {@code null} if there
912 * is no parent.
913 */
914 public String getParentString()
915 {
916 final DN parentDN = getParent();
917 if (parentDN == null)
918 {
919 return null;
920 }
921 else
922 {
923 return parentDN.toString();
924 }
925 }
926
927
928
929 /**
930 * Retrieves the string representation of the DN that is the parent for the
931 * DN with the provided string representation. Note that neither the null DN
932 * nor DNs consisting of a single RDN component will be considered to have
933 * parent DNs.
934 *
935 * @param s The string representation of the DN for which to retrieve the
936 * parent. It must not be {@code null}.
937 *
938 * @return The DN that is the parent for this DN, or {@code null} if there
939 * is no parent.
940 *
941 * @throws LDAPException If the provided string cannot be parsed as a DN.
942 */
943 public static String getParentString(final String s)
944 throws LDAPException
945 {
946 return new DN(s).getParentString();
947 }
948
949
950
951 /**
952 * Indicates whether this DN is an ancestor of the provided DN. It will be
953 * considered an ancestor of the provided DN if the array of RDN components
954 * for the provided DN ends with the elements that comprise the array of RDN
955 * components for this DN (i.e., if the provided DN is subordinate to, or
956 * optionally equal to, this DN). The null DN will be considered an ancestor
957 * for all other DNs (with the exception of the null DN if {@code allowEquals}
958 * is {@code false}).
959 *
960 * @param dn The DN for which to make the determination.
961 * @param allowEquals Indicates whether a DN should be considered an
962 * ancestor of itself.
963 *
964 * @return {@code true} if this DN may be considered an ancestor of the
965 * provided DN, or {@code false} if not.
966 */
967 public boolean isAncestorOf(final DN dn, final boolean allowEquals)
968 {
969 int thisPos = rdns.length - 1;
970 int thatPos = dn.rdns.length - 1;
971
972 if (thisPos < 0)
973 {
974 // This DN must be the null DN, which is an ancestor for all other DNs
975 // (and equal to the null DN, which we may still classify as being an
976 // ancestor).
977 return (allowEquals || (thatPos >= 0));
978 }
979
980 if ((thisPos > thatPos) || ((thisPos == thatPos) && (! allowEquals)))
981 {
982 // This DN has more RDN components than the provided DN, so it can't
983 // possibly be an ancestor, or has the same number of components and equal
984 // DNs shouldn't be considered ancestors.
985 return false;
986 }
987
988 while (thisPos >= 0)
989 {
990 if (! rdns[thisPos--].equals(dn.rdns[thatPos--]))
991 {
992 return false;
993 }
994 }
995
996 // If we've gotten here, then we can consider this DN to be an ancestor of
997 // the provided DN.
998 return true;
999 }
1000
1001
1002
1003 /**
1004 * Indicates whether this DN is an ancestor of the DN with the provided string
1005 * representation. It will be considered an ancestor of the provided DN if
1006 * the array of RDN components for the provided DN ends with the elements that
1007 * comprise the array of RDN components for this DN (i.e., if the provided DN
1008 * is subordinate to, or optionally equal to, this DN). The null DN will be
1009 * considered an ancestor for all other DNs (with the exception of the null DN
1010 * if {@code allowEquals} is {@code false}).
1011 *
1012 * @param s The string representation of the DN for which to make
1013 * the determination.
1014 * @param allowEquals Indicates whether a DN should be considered an
1015 * ancestor of itself.
1016 *
1017 * @return {@code true} if this DN may be considered an ancestor of the
1018 * provided DN, or {@code false} if not.
1019 *
1020 * @throws LDAPException If the provided string cannot be parsed as a DN.
1021 */
1022 public boolean isAncestorOf(final String s, final boolean allowEquals)
1023 throws LDAPException
1024 {
1025 return isAncestorOf(new DN(s), allowEquals);
1026 }
1027
1028
1029
1030 /**
1031 * Indicates whether the DN represented by the first string is an ancestor of
1032 * the DN represented by the second string. The first DN will be considered
1033 * an ancestor of the second DN if the array of RDN components for the first
1034 * DN ends with the elements that comprise the array of RDN components for the
1035 * second DN (i.e., if the first DN is subordinate to, or optionally equal to,
1036 * the second DN). The null DN will be considered an ancestor for all other
1037 * DNs (with the exception of the null DN if {@code allowEquals} is
1038 * {@code false}).
1039 *
1040 * @param s1 The string representation of the first DN for which to
1041 * make the determination.
1042 * @param s2 The string representation of the second DN for which
1043 * to make the determination.
1044 * @param allowEquals Indicates whether a DN should be considered an
1045 * ancestor of itself.
1046 *
1047 * @return {@code true} if the first DN may be considered an ancestor of the
1048 * second DN, or {@code false} if not.
1049 *
1050 * @throws LDAPException If either of the provided strings cannot be parsed
1051 * as a DN.
1052 */
1053 public static boolean isAncestorOf(final String s1, final String s2,
1054 final boolean allowEquals)
1055 throws LDAPException
1056 {
1057 return new DN(s1).isAncestorOf(new DN(s2), allowEquals);
1058 }
1059
1060
1061
1062 /**
1063 * Indicates whether this DN is a descendant of the provided DN. It will be
1064 * considered a descendant of the provided DN if the array of RDN components
1065 * for this DN ends with the elements that comprise the RDN components for the
1066 * provided DN (i.e., if this DN is subordinate to, or optionally equal to,
1067 * the provided DN). The null DN will not be considered a descendant for any
1068 * other DNs (with the exception of the null DN if {@code allowEquals} is
1069 * {@code true}).
1070 *
1071 * @param dn The DN for which to make the determination.
1072 * @param allowEquals Indicates whether a DN should be considered a
1073 * descendant of itself.
1074 *
1075 * @return {@code true} if this DN may be considered a descendant of the
1076 * provided DN, or {@code false} if not.
1077 */
1078 public boolean isDescendantOf(final DN dn, final boolean allowEquals)
1079 {
1080 int thisPos = rdns.length - 1;
1081 int thatPos = dn.rdns.length - 1;
1082
1083 if (thatPos < 0)
1084 {
1085 // The provided DN must be the null DN, which will be considered an
1086 // ancestor for all other DNs (and equal to the null DN), making this DN
1087 // considered a descendant for that DN.
1088 return (allowEquals || (thisPos >= 0));
1089 }
1090
1091 if ((thisPos < thatPos) || ((thisPos == thatPos) && (! allowEquals)))
1092 {
1093 // This DN has fewer DN components than the provided DN, so it can't
1094 // possibly be a descendant, or it has the same number of components and
1095 // equal DNs shouldn't be considered descendants.
1096 return false;
1097 }
1098
1099 while (thatPos >= 0)
1100 {
1101 if (! rdns[thisPos--].equals(dn.rdns[thatPos--]))
1102 {
1103 return false;
1104 }
1105 }
1106
1107 // If we've gotten here, then we can consider this DN to be a descendant of
1108 // the provided DN.
1109 return true;
1110 }
1111
1112
1113
1114 /**
1115 * Indicates whether this DN is a descendant of the DN with the provided
1116 * string representation. It will be considered a descendant of the provided
1117 * DN if the array of RDN components for this DN ends with the elements that
1118 * comprise the RDN components for the provided DN (i.e., if this DN is
1119 * subordinate to, or optionally equal to, the provided DN). The null DN will
1120 * not be considered a descendant for any other DNs (with the exception of the
1121 * null DN if {@code allowEquals} is {@code true}).
1122 *
1123 * @param s The string representation of the DN for which to make
1124 * the determination.
1125 * @param allowEquals Indicates whether a DN should be considered a
1126 * descendant of itself.
1127 *
1128 * @return {@code true} if this DN may be considered a descendant of the
1129 * provided DN, or {@code false} if not.
1130 *
1131 * @throws LDAPException If the provided string cannot be parsed as a DN.
1132 */
1133 public boolean isDescendantOf(final String s, final boolean allowEquals)
1134 throws LDAPException
1135 {
1136 return isDescendantOf(new DN(s), allowEquals);
1137 }
1138
1139
1140
1141 /**
1142 * Indicates whether the DN represented by the first string is a descendant of
1143 * the DN represented by the second string. The first DN will be considered a
1144 * descendant of the second DN if the array of RDN components for the first DN
1145 * ends with the elements that comprise the RDN components for the second DN
1146 * (i.e., if the first DN is subordinate to, or optionally equal to, the
1147 * second DN). The null DN will not be considered a descendant for any other
1148 * DNs (with the exception of the null DN if {@code allowEquals} is
1149 * {@code true}).
1150 *
1151 * @param s1 The string representation of the first DN for which to
1152 * make the determination.
1153 * @param s2 The string representation of the second DN for which
1154 * to make the determination.
1155 * @param allowEquals Indicates whether a DN should be considered an
1156 * ancestor of itself.
1157 *
1158 * @return {@code true} if this DN may be considered a descendant of the
1159 * provided DN, or {@code false} if not.
1160 *
1161 * @throws LDAPException If either of the provided strings cannot be parsed
1162 * as a DN.
1163 */
1164 public static boolean isDescendantOf(final String s1, final String s2,
1165 final boolean allowEquals)
1166 throws LDAPException
1167 {
1168 return new DN(s1).isDescendantOf(new DN(s2), allowEquals);
1169 }
1170
1171
1172
1173 /**
1174 * Indicates whether this DN falls within the range of the provided search
1175 * base DN and scope.
1176 *
1177 * @param baseDN The base DN for which to make the determination. It must
1178 * not be {@code null}.
1179 * @param scope The scope for which to make the determination. It must not
1180 * be {@code null}.
1181 *
1182 * @return {@code true} if this DN is within the range of the provided base
1183 * and scope, or {@code false} if not.
1184 *
1185 * @throws LDAPException If a problem occurs while making the determination.
1186 */
1187 public boolean matchesBaseAndScope(final String baseDN,
1188 final SearchScope scope)
1189 throws LDAPException
1190 {
1191 return matchesBaseAndScope(new DN(baseDN), scope);
1192 }
1193
1194
1195
1196 /**
1197 * Indicates whether this DN falls within the range of the provided search
1198 * base DN and scope.
1199 *
1200 * @param baseDN The base DN for which to make the determination. It must
1201 * not be {@code null}.
1202 * @param scope The scope for which to make the determination. It must not
1203 * be {@code null}.
1204 *
1205 * @return {@code true} if this DN is within the range of the provided base
1206 * and scope, or {@code false} if not.
1207 *
1208 * @throws LDAPException If a problem occurs while making the determination.
1209 */
1210 public boolean matchesBaseAndScope(final DN baseDN, final SearchScope scope)
1211 throws LDAPException
1212 {
1213 ensureNotNull(baseDN, scope);
1214
1215 switch (scope.intValue())
1216 {
1217 case SearchScope.BASE_INT_VALUE:
1218 return equals(baseDN);
1219
1220 case SearchScope.ONE_INT_VALUE:
1221 return baseDN.equals(getParent());
1222
1223 case SearchScope.SUB_INT_VALUE:
1224 return isDescendantOf(baseDN, true);
1225
1226 case SearchScope.SUBORDINATE_SUBTREE_INT_VALUE:
1227 return isDescendantOf(baseDN, false);
1228
1229 default:
1230 throw new LDAPException(ResultCode.PARAM_ERROR,
1231 ERR_DN_MATCHES_UNSUPPORTED_SCOPE.get(dnString,
1232 String.valueOf(scope)));
1233 }
1234 }
1235
1236
1237
1238
1239 /**
1240 * Generates a hash code for this DN.
1241 *
1242 * @return The generated hash code for this DN.
1243 */
1244 @Override() public int hashCode()
1245 {
1246 return toNormalizedString().hashCode();
1247 }
1248
1249
1250
1251 /**
1252 * Indicates whether the provided object is equal to this DN. In order for
1253 * the provided object to be considered equal, it must be a non-null DN with
1254 * the same set of RDN components.
1255 *
1256 * @param o The object for which to make the determination.
1257 *
1258 * @return {@code true} if the provided object is considered equal to this
1259 * DN, or {@code false} if not.
1260 */
1261 @Override()
1262 public boolean equals(final Object o)
1263 {
1264 if (o == null)
1265 {
1266 return false;
1267 }
1268
1269 if (this == o)
1270 {
1271 return true;
1272 }
1273
1274 if (! (o instanceof DN))
1275 {
1276 return false;
1277 }
1278
1279 final DN dn = (DN) o;
1280 return (toNormalizedString().equals(dn.toNormalizedString()));
1281 }
1282
1283
1284
1285 /**
1286 * Indicates whether the DN with the provided string representation is equal
1287 * to this DN.
1288 *
1289 * @param s The string representation of the DN to compare with this DN.
1290 *
1291 * @return {@code true} if the DN with the provided string representation is
1292 * equal to this DN, or {@code false} if not.
1293 *
1294 * @throws LDAPException If the provided string cannot be parsed as a DN.
1295 */
1296 public boolean equals(final String s)
1297 throws LDAPException
1298 {
1299 if (s == null)
1300 {
1301 return false;
1302 }
1303
1304 return equals(new DN(s));
1305 }
1306
1307
1308
1309 /**
1310 * Indicates whether the two provided strings represent the same DN.
1311 *
1312 * @param s1 The string representation of the first DN for which to make the
1313 * determination. It must not be {@code null}.
1314 * @param s2 The string representation of the second DN for which to make
1315 * the determination. It must not be {@code null}.
1316 *
1317 * @return {@code true} if the provided strings represent the same DN, or
1318 * {@code false} if not.
1319 *
1320 * @throws LDAPException If either of the provided strings cannot be parsed
1321 * as a DN.
1322 */
1323 public static boolean equals(final String s1, final String s2)
1324 throws LDAPException
1325 {
1326 return new DN(s1).equals(new DN(s2));
1327 }
1328
1329
1330
1331 /**
1332 * Retrieves a string representation of this DN.
1333 *
1334 * @return A string representation of this DN.
1335 */
1336 @Override()
1337 public String toString()
1338 {
1339 return dnString;
1340 }
1341
1342
1343
1344 /**
1345 * Retrieves a string representation of this DN with minimal encoding for
1346 * special characters. Only those characters specified in RFC 4514 section
1347 * 2.4 will be escaped. No escaping will be used for non-ASCII characters or
1348 * non-printable ASCII characters.
1349 *
1350 * @return A string representation of this DN with minimal encoding for
1351 * special characters.
1352 */
1353 public String toMinimallyEncodedString()
1354 {
1355 final StringBuilder buffer = new StringBuilder();
1356 toString(buffer, true);
1357 return buffer.toString();
1358 }
1359
1360
1361
1362 /**
1363 * Appends a string representation of this DN to the provided buffer.
1364 *
1365 * @param buffer The buffer to which to append the string representation of
1366 * this DN.
1367 */
1368 public void toString(final StringBuilder buffer)
1369 {
1370 toString(buffer, false);
1371 }
1372
1373
1374
1375 /**
1376 * Appends a string representation of this DN to the provided buffer.
1377 *
1378 * @param buffer The buffer to which the string representation is
1379 * to be appended.
1380 * @param minimizeEncoding Indicates whether to restrict the encoding of
1381 * special characters to the bare minimum required
1382 * by LDAP (as per RFC 4514 section 2.4). If this
1383 * is {@code true}, then only leading and trailing
1384 * spaces, double quotes, plus signs, commas,
1385 * semicolons, greater-than, less-than, and
1386 * backslash characters will be encoded.
1387 */
1388 public void toString(final StringBuilder buffer,
1389 final boolean minimizeEncoding)
1390 {
1391 for (int i=0; i < rdns.length; i++)
1392 {
1393 if (i > 0)
1394 {
1395 buffer.append(',');
1396 }
1397
1398 rdns[i].toString(buffer, minimizeEncoding);
1399 }
1400 }
1401
1402
1403
1404 /**
1405 * Retrieves a normalized string representation of this DN.
1406 *
1407 * @return A normalized string representation of this DN.
1408 */
1409 public String toNormalizedString()
1410 {
1411 if (normalizedString == null)
1412 {
1413 final StringBuilder buffer = new StringBuilder();
1414 toNormalizedString(buffer);
1415 normalizedString = buffer.toString();
1416 }
1417
1418 return normalizedString;
1419 }
1420
1421
1422
1423 /**
1424 * Appends a normalized string representation of this DN to the provided
1425 * buffer.
1426 *
1427 * @param buffer The buffer to which to append the normalized string
1428 * representation of this DN.
1429 */
1430 public void toNormalizedString(final StringBuilder buffer)
1431 {
1432 for (int i=0; i < rdns.length; i++)
1433 {
1434 if (i > 0)
1435 {
1436 buffer.append(',');
1437 }
1438
1439 buffer.append(rdns[i].toNormalizedString());
1440 }
1441 }
1442
1443
1444
1445 /**
1446 * Retrieves a normalized representation of the DN with the provided string
1447 * representation.
1448 *
1449 * @param s The string representation of the DN to normalize. It must not
1450 * be {@code null}.
1451 *
1452 * @return The normalized representation of the DN with the provided string
1453 * representation.
1454 *
1455 * @throws LDAPException If the provided string cannot be parsed as a DN.
1456 */
1457 public static String normalize(final String s)
1458 throws LDAPException
1459 {
1460 return normalize(s, null);
1461 }
1462
1463
1464
1465 /**
1466 * Retrieves a normalized representation of the DN with the provided string
1467 * representation.
1468 *
1469 * @param s The string representation of the DN to normalize. It must
1470 * not be {@code null}.
1471 * @param schema The schema to use to generate the normalized string
1472 * representation of the DN. It may be {@code null} if no
1473 * schema is available.
1474 *
1475 * @return The normalized representation of the DN with the provided string
1476 * representation.
1477 *
1478 * @throws LDAPException If the provided string cannot be parsed as a DN.
1479 */
1480 public static String normalize(final String s, final Schema schema)
1481 throws LDAPException
1482 {
1483 return new DN(s, schema).toNormalizedString();
1484 }
1485
1486
1487
1488 /**
1489 * Compares the provided DN to this DN to determine their relative order in
1490 * a sorted list.
1491 *
1492 * @param dn The DN to compare against this DN. It must not be
1493 * {@code null}.
1494 *
1495 * @return A negative integer if this DN should come before the provided DN
1496 * in a sorted list, a positive integer if this DN should come after
1497 * the provided DN in a sorted list, or zero if the provided DN can
1498 * be considered equal to this DN.
1499 */
1500 public int compareTo(final DN dn)
1501 {
1502 return compare(this, dn);
1503 }
1504
1505
1506
1507 /**
1508 * Compares the provided DN values to determine their relative order in a
1509 * sorted list.
1510 *
1511 * @param dn1 The first DN to be compared. It must not be {@code null}.
1512 * @param dn2 The second DN to be compared. It must not be {@code null}.
1513 *
1514 * @return A negative integer if the first DN should come before the second
1515 * DN in a sorted list, a positive integer if the first DN should
1516 * come after the second DN in a sorted list, or zero if the two DN
1517 * values can be considered equal.
1518 */
1519 public int compare(final DN dn1, final DN dn2)
1520 {
1521 ensureNotNull(dn1, dn2);
1522
1523 // We want the comparison to be in reverse order, so that DNs will be sorted
1524 // hierarchically.
1525 int pos1 = dn1.rdns.length - 1;
1526 int pos2 = dn2.rdns.length - 1;
1527 if (pos1 < 0)
1528 {
1529 if (pos2 < 0)
1530 {
1531 // Both DNs are the null DN, so they are equal.
1532 return 0;
1533 }
1534 else
1535 {
1536 // The first DN is the null DN and the second isn't, so the first DN
1537 // comes first.
1538 return -1;
1539 }
1540 }
1541 else if (pos2 < 0)
1542 {
1543 // The second DN is the null DN, which always comes first.
1544 return 1;
1545 }
1546
1547
1548 while ((pos1 >= 0) && (pos2 >= 0))
1549 {
1550 final int compValue = dn1.rdns[pos1].compareTo(dn2.rdns[pos2]);
1551 if (compValue != 0)
1552 {
1553 return compValue;
1554 }
1555
1556 pos1--;
1557 pos2--;
1558 }
1559
1560
1561 // If we've gotten here, then one of the DNs is equal to or a descendant of
1562 // the other.
1563 if (pos1 < 0)
1564 {
1565 if (pos2 < 0)
1566 {
1567 // They're both the same length, so they should be considered equal.
1568 return 0;
1569 }
1570 else
1571 {
1572 // The first is shorter than the second, so it should come first.
1573 return -1;
1574 }
1575 }
1576 else
1577 {
1578 // The second RDN is shorter than the first, so it should come first.
1579 return 1;
1580 }
1581 }
1582
1583
1584
1585 /**
1586 * Compares the DNs with the provided string representations to determine
1587 * their relative order in a sorted list.
1588 *
1589 * @param s1 The string representation for the first DN to be compared. It
1590 * must not be {@code null}.
1591 * @param s2 The string representation for the second DN to be compared. It
1592 * must not be {@code null}.
1593 *
1594 * @return A negative integer if the first DN should come before the second
1595 * DN in a sorted list, a positive integer if the first DN should
1596 * come after the second DN in a sorted list, or zero if the two DN
1597 * values can be considered equal.
1598 *
1599 * @throws LDAPException If either of the provided strings cannot be parsed
1600 * as a DN.
1601 */
1602 public static int compare(final String s1, final String s2)
1603 throws LDAPException
1604 {
1605 return compare(s1, s2, null);
1606 }
1607
1608
1609
1610 /**
1611 * Compares the DNs with the provided string representations to determine
1612 * their relative order in a sorted list.
1613 *
1614 * @param s1 The string representation for the first DN to be compared.
1615 * It must not be {@code null}.
1616 * @param s2 The string representation for the second DN to be compared.
1617 * It must not be {@code null}.
1618 * @param schema The schema to use to generate the normalized string
1619 * representations of the DNs. It may be {@code null} if no
1620 * schema is available.
1621 *
1622 * @return A negative integer if the first DN should come before the second
1623 * DN in a sorted list, a positive integer if the first DN should
1624 * come after the second DN in a sorted list, or zero if the two DN
1625 * values can be considered equal.
1626 *
1627 * @throws LDAPException If either of the provided strings cannot be parsed
1628 * as a DN.
1629 */
1630 public static int compare(final String s1, final String s2,
1631 final Schema schema)
1632 throws LDAPException
1633 {
1634 return new DN(s1, schema).compareTo(new DN(s2, schema));
1635 }
1636 }