001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.types;
028    import org.opends.messages.Message;
029    
030    
031    
032    import java.util.Iterator;
033    import java.util.LinkedHashSet;
034    import java.util.LinkedList;
035    import java.util.StringTokenizer;
036    
037    import org.opends.server.core.DirectoryServer;
038    
039    import static org.opends.server.loggers.debug.DebugLogger.*;
040    import org.opends.server.loggers.debug.DebugTracer;
041    import static org.opends.messages.UtilityMessages.*;
042    import static org.opends.server.util.StaticUtils.*;
043    
044    
045    
046    /**
047     * This class defines a data structure that represents the components
048     * of an LDAP URL, including the scheme, host, port, base DN,
049     * attributes, scope, filter, and extensions.  It has the ability to
050     * create an LDAP URL based on all of these individual components, as
051     * well as parsing them from their string representations.
052     */
053    @org.opends.server.types.PublicAPI(
054         stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
055         mayInstantiate=true,
056         mayExtend=false,
057         mayInvoke=true)
058    public final class LDAPURL
059    {
060      /**
061       * The tracer object for the debug logger.
062       */
063      private static final DebugTracer TRACER = getTracer();
064    
065      /**
066       * The default scheme that will be used if none is provided.
067       */
068      public static final String DEFAULT_SCHEME = "ldap";
069    
070    
071    
072      /**
073       * The default port value that will be used if none is provided.
074       */
075      public static final int DEFAULT_PORT = 389;
076    
077    
078    
079      /**
080       * The default base DN that will be used if none is provided.
081       */
082      public static final DN DEFAULT_BASE_DN = DN.nullDN();
083    
084    
085    
086      /**
087       * The default search scope that will be used if none is provided.
088       */
089      public static final SearchScope DEFAULT_SEARCH_SCOPE =
090           SearchScope.BASE_OBJECT;
091    
092    
093    
094      /**
095       * The default search filter that will be used if none is provided.
096       */
097      public static final SearchFilter DEFAULT_SEARCH_FILTER =
098           SearchFilter.createPresenceFilter(
099                DirectoryServer.getObjectClassAttributeType());
100    
101    
102    
103      // The base DN for this LDAP URL.
104      private DN baseDN;
105    
106      // The port number for this LDAP URL.
107      private int port;
108    
109      // The set of attributes for this LDAP URL.
110      private LinkedHashSet<String> attributes;
111    
112      // The set of extensions for this LDAP URL.
113      private LinkedList<String> extensions;
114    
115      // The search scope for this LDAP URL.
116      private SearchScope scope;
117    
118      // The search filter for this LDAP URL.
119      private SearchFilter filter;
120    
121      // The host for this LDAP URL.
122      private String host;
123    
124      // The raw base DN for this LDAP URL.
125      private String rawBaseDN;
126    
127      // The raw filter for this LDAP URL.
128      private String rawFilter;
129    
130      // The scheme (i.e., protocol) for this LDAP URL.
131      private String scheme;
132    
133    
134    
135      /**
136       * Creates a new LDAP URL with the provided information.
137       *
138       * @param  scheme      The scheme (i.e., protocol) for this LDAP
139       *                     URL.
140       * @param  host        The address for this LDAP URL.
141       * @param  port        The port number for this LDAP URL.
142       * @param  rawBaseDN   The raw base DN for this LDAP URL.
143       * @param  attributes  The set of requested attributes for this LDAP
144       *                     URL.
145       * @param  scope       The search scope for this LDAP URL.
146       * @param  rawFilter   The string representation of the search
147       *                     filter for this LDAP URL.
148       * @param  extensions  The set of extensions for this LDAP URL.
149       */
150      public LDAPURL(String scheme, String host, int port,
151                     String rawBaseDN, LinkedHashSet<String> attributes,
152                     SearchScope scope, String rawFilter,
153                     LinkedList<String> extensions)
154      {
155        this.host = toLowerCase(host);
156    
157        baseDN = null;
158        filter = null;
159    
160    
161        if (scheme == null)
162        {
163          this.scheme = "ldap";
164        }
165        else
166        {
167          this.scheme = toLowerCase(scheme);
168        }
169    
170        if ((port <= 0) || (port > 65535))
171        {
172          this.port = DEFAULT_PORT;
173        }
174        else
175        {
176          this.port = port;
177        }
178    
179        if (rawBaseDN == null)
180        {
181          this.rawBaseDN = "";
182        }
183        else
184        {
185          this.rawBaseDN = rawBaseDN;
186        }
187    
188        if (attributes == null)
189        {
190          this.attributes = new LinkedHashSet<String>();
191        }
192        else
193        {
194          this.attributes = attributes;
195        }
196    
197        if (scope == null)
198        {
199          this.scope = DEFAULT_SEARCH_SCOPE;
200        }
201        else
202        {
203          this.scope = scope;
204        }
205    
206        if (rawFilter == null)
207        {
208          this.rawFilter = "(objectClass=*)";
209        }
210        else
211        {
212          this.rawFilter = rawFilter;
213        }
214    
215        if (extensions == null)
216        {
217          this.extensions = new LinkedList<String>();
218        }
219        else
220        {
221          this.extensions = extensions;
222        }
223      }
224    
225    
226    
227      /**
228       * Creates a new LDAP URL with the provided information.
229       *
230       * @param  scheme      The scheme (i.e., protocol) for this LDAP
231       *                     URL.
232       * @param  host        The address for this LDAP URL.
233       * @param  port        The port number for this LDAP URL.
234       * @param  baseDN      The base DN for this LDAP URL.
235       * @param  attributes  The set of requested attributes for this LDAP
236       *                     URL.
237       * @param  scope       The search scope for this LDAP URL.
238       * @param  filter      The search filter for this LDAP URL.
239       * @param  extensions  The set of extensions for this LDAP URL.
240       */
241      public LDAPURL(String scheme, String host, int port, DN baseDN,
242                     LinkedHashSet<String> attributes, SearchScope scope,
243                     SearchFilter filter, LinkedList<String> extensions)
244      {
245        this.host = toLowerCase(host);
246    
247    
248        if (scheme == null)
249        {
250          this.scheme = "ldap";
251        }
252        else
253        {
254          this.scheme = toLowerCase(scheme);
255        }
256    
257        if ((port <= 0) || (port > 65535))
258        {
259          this.port = DEFAULT_PORT;
260        }
261        else
262        {
263          this.port = port;
264        }
265    
266        if (baseDN == null)
267        {
268          this.baseDN    = DEFAULT_BASE_DN;
269          this.rawBaseDN = DEFAULT_BASE_DN.toString();
270        }
271        else
272        {
273          this.baseDN    = baseDN;
274          this.rawBaseDN = baseDN.toString();
275        }
276    
277        if (attributes == null)
278        {
279          this.attributes = new LinkedHashSet<String>();
280        }
281        else
282        {
283          this.attributes = attributes;
284        }
285    
286        if (scope == null)
287        {
288          this.scope = DEFAULT_SEARCH_SCOPE;
289        }
290        else
291        {
292          this.scope = scope;
293        }
294    
295        if (filter == null)
296        {
297          this.filter    = DEFAULT_SEARCH_FILTER;
298          this.rawFilter = DEFAULT_SEARCH_FILTER.toString();
299        }
300        else
301        {
302          this.filter    = filter;
303          this.rawFilter = filter.toString();
304        }
305    
306        if (extensions == null)
307        {
308          this.extensions = new LinkedList<String>();
309        }
310        else
311        {
312          this.extensions = extensions;
313        }
314      }
315    
316    
317    
318      /**
319       * Decodes the provided string as an LDAP URL.
320       *
321       * @param  url          The URL string to be decoded.
322       * @param  fullyDecode  Indicates whether the URL should be fully
323       *                      decoded (e.g., parsing the base DN and
324       *                      search filter) or just leaving them in their
325       *                      string representations.  The latter may be
326       *                      required for client-side use.
327       *
328       * @return  The LDAP URL decoded from the provided string.
329       *
330       * @throws  DirectoryException  If a problem occurs while attempting
331       *                              to decode the provided string as an
332       *                              LDAP URL.
333       */
334      public static LDAPURL decode(String url, boolean fullyDecode)
335             throws DirectoryException
336      {
337        // Find the "://" component, which will separate the scheme from
338        // the host.
339        String scheme;
340        int schemeEndPos = url.indexOf("://");
341        if (schemeEndPos < 0)
342        {
343          Message message =
344              ERR_LDAPURL_NO_COLON_SLASH_SLASH.get(String.valueOf(url));
345          throw new DirectoryException(
346                         ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
347        }
348        else if (schemeEndPos == 0)
349        {
350          Message message =
351              ERR_LDAPURL_NO_SCHEME.get(String.valueOf(url));
352          throw new DirectoryException(
353                         ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
354        }
355        else
356        {
357          scheme = urlDecode(url.substring(0, schemeEndPos));
358        }
359    
360    
361        // If the "://" was the end of the URL, then we're done.
362        int length = url.length();
363        if (length == schemeEndPos+3)
364        {
365          return new LDAPURL(scheme, null, DEFAULT_PORT, DEFAULT_BASE_DN,
366                             null, DEFAULT_SEARCH_SCOPE,
367                             DEFAULT_SEARCH_FILTER, null);
368        }
369    
370    
371        // Look at the next character.  If it's anything but a slash, then
372        // it should be part of the host and optional port.
373        String host     = null;
374        int    port     = DEFAULT_PORT;
375        int    startPos = schemeEndPos + 3;
376        int    pos      = startPos;
377        while (pos < length)
378        {
379          char c = url.charAt(pos);
380          if (c == '/')
381          {
382            break;
383          }
384          else
385          {
386            pos++;
387          }
388        }
389    
390        if (pos > startPos)
391        {
392          String hostPort = url.substring(startPos, pos);
393          int colonPos = hostPort.indexOf(':');
394          if (colonPos < 0)
395          {
396            host = urlDecode(hostPort);
397          }
398          else if (colonPos == 0)
399          {
400            Message message =
401                ERR_LDAPURL_NO_HOST.get(String.valueOf(url));
402            throw new DirectoryException(
403                           ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
404          }
405          else if (colonPos == (hostPort.length() - 1))
406          {
407            Message message =
408                ERR_LDAPURL_NO_PORT.get(String.valueOf(url));
409            throw new DirectoryException(
410                           ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
411          }
412          else
413          {
414            host = urlDecode(hostPort.substring(0, colonPos));
415    
416            try
417            {
418              port = Integer.parseInt(hostPort.substring(colonPos+1));
419            }
420            catch (Exception e)
421            {
422              if (debugEnabled())
423              {
424                TRACER.debugCaught(DebugLogLevel.ERROR, e);
425              }
426    
427              Message message = ERR_LDAPURL_CANNOT_DECODE_PORT.get(
428                  String.valueOf(url), hostPort.substring(colonPos+1));
429              throw new DirectoryException(
430                            ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
431            }
432    
433            if ((port <= 0) || (port > 65535))
434            {
435              Message message =
436                  ERR_LDAPURL_INVALID_PORT.get(String.valueOf(url), port);
437              throw new DirectoryException(
438                            ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
439            }
440          }
441        }
442    
443    
444        // Move past the slash.  If we're at or past the end of the
445        // string, then we're done.
446        pos++;
447        if (pos > length)
448        {
449          return new LDAPURL(scheme, host, port, DEFAULT_BASE_DN, null,
450                             DEFAULT_SEARCH_SCOPE, DEFAULT_SEARCH_FILTER,
451                             null);
452        }
453        else
454        {
455          startPos = pos;
456        }
457    
458    
459        // The next delimiter should be a question mark.  If there isn't
460        // one, then the rest of the value must be the base DN.
461        String baseDNString = null;
462        pos = url.indexOf('?', startPos);
463        if (pos < 0)
464        {
465          baseDNString = url.substring(startPos);
466          startPos = length;
467        }
468        else
469        {
470          baseDNString = url.substring(startPos, pos);
471          startPos = pos+1;
472        }
473    
474        DN baseDN;
475        if (fullyDecode)
476        {
477          baseDN = DN.decode(urlDecode(baseDNString));
478        }
479        else
480        {
481          baseDN = null;
482        }
483    
484    
485        if (startPos >= length)
486        {
487          if (fullyDecode)
488          {
489            return new LDAPURL(scheme, host, port, baseDN, null,
490                               DEFAULT_SEARCH_SCOPE,
491                               DEFAULT_SEARCH_FILTER, null);
492          }
493          else
494          {
495            return new LDAPURL(scheme, host, port, baseDNString, null,
496                               DEFAULT_SEARCH_SCOPE, null, null);
497          }
498        }
499    
500    
501        // Find the next question mark (or the end of the string if there
502        // aren't any more) and get the attribute list from it.
503        String attrsString;
504        pos = url.indexOf('?', startPos);
505        if (pos < 0)
506        {
507          attrsString = url.substring(startPos);
508          startPos = length;
509        }
510        else
511        {
512          attrsString = url.substring(startPos, pos);
513          startPos = pos+1;
514        }
515    
516        LinkedHashSet<String> attributes = new LinkedHashSet<String>();
517        StringTokenizer tokenizer = new StringTokenizer(attrsString, ",");
518        while (tokenizer.hasMoreTokens())
519        {
520          attributes.add(urlDecode(tokenizer.nextToken()));
521        }
522    
523        if (startPos >= length)
524        {
525          if (fullyDecode)
526          {
527            return new LDAPURL(scheme, host, port, baseDN, attributes,
528                               DEFAULT_SEARCH_SCOPE,
529                               DEFAULT_SEARCH_FILTER, null);
530          }
531          else
532          {
533            return new LDAPURL(scheme, host, port, baseDNString,
534                               attributes, DEFAULT_SEARCH_SCOPE, null,
535                               null);
536          }
537        }
538    
539    
540        // Find the next question mark (or the end of the string if there
541        // aren't any more) and get the scope from it.
542        String scopeString;
543        pos = url.indexOf('?', startPos);
544        if (pos < 0)
545        {
546          scopeString = toLowerCase(urlDecode(url.substring(startPos)));
547          startPos = length;
548        }
549        else
550        {
551          scopeString =
552               toLowerCase(urlDecode(url.substring(startPos, pos)));
553          startPos = pos+1;
554        }
555    
556        SearchScope scope;
557        if (scopeString.equals(""))
558        {
559          scope = DEFAULT_SEARCH_SCOPE;
560        }
561        else if (scopeString.equals("base"))
562        {
563          scope = SearchScope.BASE_OBJECT;
564        }
565        else if (scopeString.equals("one"))
566        {
567          scope = SearchScope.SINGLE_LEVEL;
568        }
569        else if (scopeString.equals("sub"))
570        {
571          scope = SearchScope.WHOLE_SUBTREE;
572        }
573        else if (scopeString.equals("subord") ||
574                 scopeString.equals("subordinate"))
575        {
576          scope = SearchScope.SUBORDINATE_SUBTREE;
577        }
578        else
579        {
580          Message message = ERR_LDAPURL_INVALID_SCOPE_STRING.get(
581              String.valueOf(url), String.valueOf(scopeString));
582          throw new DirectoryException(
583                         ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
584        }
585    
586        if (startPos >= length)
587        {
588          if (fullyDecode)
589          {
590            return new LDAPURL(scheme, host, port, baseDN, attributes,
591                               scope, DEFAULT_SEARCH_FILTER, null);
592          }
593          else
594          {
595            return new LDAPURL(scheme, host, port, baseDNString,
596                               attributes, scope, null, null);
597          }
598        }
599    
600    
601        // Find the next question mark (or the end of the string if there
602        // aren't any more) and get the filter from it.
603        String filterString;
604        pos = url.indexOf('?', startPos);
605        if (pos < 0)
606        {
607          filterString = urlDecode(url.substring(startPos));
608          startPos = length;
609        }
610        else
611        {
612          filterString = urlDecode(url.substring(startPos, pos));
613          startPos = pos+1;
614        }
615    
616        SearchFilter filter;
617        if (fullyDecode)
618        {
619          if (filterString.equals(""))
620          {
621            filter = DEFAULT_SEARCH_FILTER;
622          }
623          else
624          {
625            filter = SearchFilter.createFilterFromString(filterString);
626          }
627    
628          if (startPos >= length)
629          {
630            if (fullyDecode)
631            {
632              return new LDAPURL(scheme, host, port, baseDN, attributes,
633                                 scope, filter, null);
634            }
635            else
636            {
637              return new LDAPURL(scheme, host, port, baseDNString,
638                                 attributes, scope, filterString, null);
639            }
640          }
641        }
642        else
643        {
644          filter = null;
645        }
646    
647    
648        // The rest of the string must be the set of extensions.
649        String extensionsString = url.substring(startPos);
650        LinkedList<String> extensions = new LinkedList<String>();
651        tokenizer = new StringTokenizer(extensionsString, ",");
652        while (tokenizer.hasMoreTokens())
653        {
654          extensions.add(urlDecode(tokenizer.nextToken()));
655        }
656    
657    
658        if (fullyDecode)
659        {
660          return new LDAPURL(scheme, host, port, baseDN, attributes,
661                             scope, filter, extensions);
662        }
663        else
664        {
665          return new LDAPURL(scheme, host, port, baseDNString, attributes,
666                             scope, filterString, extensions);
667        }
668      }
669    
670    
671    
672      /**
673       * Converts the provided string to a form that has decoded "special"
674       * characters that have been encoded for use in an LDAP URL.
675       *
676       * @param  s  The string to be decoded.
677       *
678       * @return  The decoded string.
679       *
680       * @throws  DirectoryException  If a problem occurs while attempting
681       *                              to decode the contents of the
682       *                              provided string.
683       */
684      private static String urlDecode(String s)
685              throws DirectoryException
686      {
687        if (s == null)
688        {
689          return "";
690        }
691    
692        byte[] stringBytes  = getBytes(s);
693        int    length       = stringBytes.length;
694        byte[] decodedBytes = new byte[length];
695        int    pos          = 0;
696    
697        for (int i=0; i < length; i++)
698        {
699          if (stringBytes[i] == '%')
700          {
701            // There must be at least two bytes left.  If not, then that's
702            // a problem.
703            if (i+2 > length)
704            {
705              Message message = ERR_LDAPURL_PERCENT_TOO_CLOSE_TO_END.get(
706                  String.valueOf(s), i);
707              throw new DirectoryException(
708                            ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
709            }
710    
711            byte b;
712            switch (stringBytes[++i])
713            {
714              case '0':
715                b = (byte) 0x00;
716                break;
717              case '1':
718                b = (byte) 0x10;
719                break;
720              case '2':
721                b = (byte) 0x20;
722                break;
723              case '3':
724                b = (byte) 0x30;
725                break;
726              case '4':
727                b = (byte) 0x40;
728                break;
729              case '5':
730                b = (byte) 0x50;
731                break;
732              case '6':
733                b = (byte) 0x60;
734                break;
735              case '7':
736                b = (byte) 0x70;
737                break;
738              case '8':
739                b = (byte) 0x80;
740                break;
741              case '9':
742                b = (byte) 0x90;
743                break;
744              case 'a':
745              case 'A':
746                b = (byte) 0xA0;
747                break;
748              case 'b':
749              case 'B':
750                b = (byte) 0xB0;
751                break;
752              case 'c':
753              case 'C':
754                b = (byte) 0xC0;
755                break;
756              case 'd':
757              case 'D':
758                b = (byte) 0xD0;
759                break;
760              case 'e':
761              case 'E':
762                b = (byte) 0xE0;
763                break;
764              case 'f':
765              case 'F':
766                b = (byte) 0xF0;
767                break;
768              default:
769                Message message = ERR_LDAPURL_INVALID_HEX_BYTE.get(
770                    String.valueOf(s), i);
771                throw new DirectoryException(
772                               ResultCode.INVALID_ATTRIBUTE_SYNTAX,
773                               message);
774            }
775    
776            switch (stringBytes[++i])
777            {
778              case '0':
779                break;
780              case '1':
781                b |= 0x01;
782                break;
783              case '2':
784                b |= 0x02;
785                break;
786              case '3':
787                b |= 0x03;
788                break;
789              case '4':
790                b |= 0x04;
791                break;
792              case '5':
793                b |= 0x05;
794                break;
795              case '6':
796                b |= 0x06;
797                break;
798              case '7':
799                b |= 0x07;
800                break;
801              case '8':
802                b |= 0x08;
803                break;
804              case '9':
805                b |= 0x09;
806                break;
807              case 'a':
808              case 'A':
809                b |= 0x0A;
810                break;
811              case 'b':
812              case 'B':
813                b |= 0x0B;
814                break;
815              case 'c':
816              case 'C':
817                b |= 0x0C;
818                break;
819              case 'd':
820              case 'D':
821                b |= 0x0D;
822                break;
823              case 'e':
824              case 'E':
825                b |= 0x0E;
826                break;
827              case 'f':
828              case 'F':
829                b |= 0x0F;
830                break;
831              default:
832                Message message = ERR_LDAPURL_INVALID_HEX_BYTE.get(
833                    String.valueOf(s), i);
834                throw new DirectoryException(
835                               ResultCode.INVALID_ATTRIBUTE_SYNTAX,
836                               message);
837            }
838    
839            decodedBytes[pos++] = b;
840          }
841          else
842          {
843            decodedBytes[pos++] = stringBytes[i];
844          }
845        }
846    
847        try
848        {
849          return new String(decodedBytes, 0, pos, "UTF-8");
850        }
851        catch (Exception e)
852        {
853          if (debugEnabled())
854          {
855            TRACER.debugCaught(DebugLogLevel.ERROR, e);
856          }
857    
858          // This should never happen.
859          Message message = ERR_LDAPURL_CANNOT_CREATE_UTF8_STRING.get(
860              getExceptionMessage(e));
861          throw new DirectoryException(
862                         ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
863        }
864      }
865    
866    
867    
868      /**
869       * Encodes the provided string portion for inclusion in an LDAP URL.
870       *
871       * @param  s            The string portion to be encoded.
872       * @param  isExtension  Indicates whether the provided component is
873       *                      an extension and therefore needs to have
874       *                      commas encoded.
875       *
876       * @return  The URL-encoded version of the string portion.
877       */
878      private static String urlEncode(String s, boolean isExtension)
879      {
880        if (s == null)
881        {
882          return "";
883        }
884    
885    
886        int length = s.length();
887        StringBuilder buffer = new StringBuilder(length);
888        urlEncode(s, isExtension, buffer);
889    
890        return buffer.toString();
891      }
892    
893    
894    
895      /**
896       * Encodes the provided string portion for inclusion in an LDAP URL
897       * and appends it to the provided buffer.
898       *
899       * @param  s            The string portion to be encoded.
900       * @param  isExtension  Indicates whether the provided component is
901       *                      an extension and therefore needs to have
902       *                      commas encoded.
903       * @param  buffer       The buffer to which the information should
904       *                      be appended.
905       */
906      private static void urlEncode(String s, boolean isExtension,
907                                    StringBuilder buffer)
908      {
909        if (s == null)
910        {
911          return;
912        }
913    
914        int length = s.length();
915    
916        for (int i=0; i < length; i++)
917        {
918          char c = s.charAt(i);
919          if (isAlpha(c) || isDigit(c))
920          {
921            buffer.append(c);
922            continue;
923          }
924    
925          if (c == ',')
926          {
927            if (isExtension)
928            {
929              hexEncode(c, buffer);
930            }
931            else
932            {
933              buffer.append(c);
934            }
935    
936            continue;
937          }
938    
939          switch (c)
940          {
941            case '-':
942            case '.':
943            case '_':
944            case '~':
945            case ':':
946            case '/':
947            case '#':
948            case '[':
949            case ']':
950            case '@':
951            case '!':
952            case '$':
953            case '&':
954            case '\'':
955            case '(':
956            case ')':
957            case '*':
958            case '+':
959            case ';':
960            case '=':
961              buffer.append(c);
962              break;
963            default:
964              hexEncode(c, buffer);
965              break;
966          }
967        }
968      }
969    
970    
971    
972      /**
973       * Appends a percent-encoded representation of the provided
974       * character to the given buffer.
975       *
976       * @param  c       The character to add to the buffer.
977       * @param  buffer  The buffer to which the percent-encoded
978       *                 representation should be written.
979       */
980      private static void hexEncode(char c, StringBuilder buffer)
981      {
982        if ((c & (byte) 0xFF) == c)
983        {
984          // It's a single byte.
985          buffer.append('%');
986          buffer.append(byteToHex((byte) c));
987        }
988        else
989        {
990          // It requires two bytes, and each should be prefixed by a
991          // percent sign.
992          buffer.append('%');
993          byte b1 = (byte) ((c >>> 8) & 0xFF);
994          buffer.append(byteToHex(b1));
995    
996          buffer.append('%');
997          byte b2 = (byte) (c & 0xFF);
998          buffer.append(byteToHex(b2));
999        }
1000      }
1001    
1002    
1003    
1004      /**
1005       * Retrieves the scheme for this LDAP URL.
1006       *
1007       * @return  The scheme for this LDAP URL.
1008       */
1009      public String getScheme()
1010      {
1011        return scheme;
1012      }
1013    
1014    
1015    
1016      /**
1017       * Specifies the scheme for this LDAP URL.
1018       *
1019       * @param  scheme  The scheme for this LDAP URL.
1020       */
1021      public void setScheme(String scheme)
1022      {
1023        if (scheme == null)
1024        {
1025          this.scheme = DEFAULT_SCHEME;
1026        }
1027        else
1028        {
1029          this.scheme = scheme;
1030        }
1031      }
1032    
1033    
1034    
1035      /**
1036       * Retrieves the host for this LDAP URL.
1037       *
1038       * @return  The host for this LDAP URL, or <CODE>null</CODE> if none
1039       *          was provided.
1040       */
1041      public String getHost()
1042      {
1043        return host;
1044      }
1045    
1046    
1047    
1048      /**
1049       * Specifies the host for this LDAP URL.
1050       *
1051       * @param  host  The host for this LDAP URL.
1052       */
1053      public void setHost(String host)
1054      {
1055        this.host = host;
1056      }
1057    
1058    
1059    
1060      /**
1061       * Retrieves the port for this LDAP URL.
1062       *
1063       * @return  The port for this LDAP URL.
1064       */
1065      public int getPort()
1066      {
1067        return port;
1068      }
1069    
1070    
1071    
1072      /**
1073       * Specifies the port for this LDAP URL.
1074       *
1075       * @param  port  The port for this LDAP URL.
1076       */
1077      public void setPort(int port)
1078      {
1079        if ((port <= 0) || (port > 65535))
1080        {
1081          this.port = DEFAULT_PORT;
1082        }
1083        else
1084        {
1085          this.port = port;
1086        }
1087      }
1088    
1089    
1090    
1091      /**
1092       * Retrieve the raw, unprocessed base DN for this LDAP URL.
1093       *
1094       * @return  The raw, unprocessed base DN for this LDAP URL, or
1095       *          <CODE>null</CODE> if none was given (in which case a
1096       *          default of the null DN "" should be assumed).
1097       */
1098      public String getRawBaseDN()
1099      {
1100        return rawBaseDN;
1101      }
1102    
1103    
1104    
1105      /**
1106       * Specifies the raw, unprocessed base DN for this LDAP URL.
1107       *
1108       * @param  rawBaseDN  The raw, unprocessed base DN for this LDAP
1109       *                    URL.
1110       */
1111      public void setRawBaseDN(String rawBaseDN)
1112      {
1113        this.rawBaseDN = rawBaseDN;
1114        this.baseDN    = null;
1115      }
1116    
1117    
1118    
1119      /**
1120       * Retrieves the processed DN for this LDAP URL.
1121       *
1122       * @return  The processed DN for this LDAP URL.
1123       *
1124       * @throws  DirectoryException  If the raw base DN cannot be decoded
1125       *                              as a valid DN.
1126       */
1127      public DN getBaseDN()
1128             throws DirectoryException
1129      {
1130        if (baseDN == null)
1131        {
1132          if ((rawBaseDN == null) || (rawBaseDN.length() == 0))
1133          {
1134            return DEFAULT_BASE_DN;
1135          }
1136    
1137          baseDN = DN.decode(rawBaseDN);
1138        }
1139    
1140        return baseDN;
1141      }
1142    
1143    
1144    
1145      /**
1146       * Specifies the base DN for this LDAP URL.
1147       *
1148       * @param  baseDN  The base DN for this LDAP URL.
1149       */
1150      public void setBaseDN(DN baseDN)
1151      {
1152        if (baseDN == null)
1153        {
1154          this.baseDN    = null;
1155          this.rawBaseDN = null;
1156        }
1157        else
1158        {
1159          this.baseDN    = baseDN;
1160          this.rawBaseDN = baseDN.toString();
1161        }
1162      }
1163    
1164    
1165    
1166      /**
1167       * Retrieves the set of attributes for this LDAP URL.  The contents
1168       * of the returned set may be altered by the caller.
1169       *
1170       * @return  The set of attributes for this LDAP URL.
1171       */
1172      public LinkedHashSet<String> getAttributes()
1173      {
1174        return attributes;
1175      }
1176    
1177    
1178    
1179      /**
1180       * Retrieves the search scope for this LDAP URL.
1181       *
1182       * @return  The search scope for this LDAP URL, or <CODE>null</CODE>
1183       *          if none was given (in which case the base-level scope
1184       *          should be assumed).
1185       */
1186      public SearchScope getScope()
1187      {
1188        return scope;
1189      }
1190    
1191    
1192    
1193      /**
1194       * Specifies the search scope for this LDAP URL.
1195       *
1196       * @param  scope  The search scope for this LDAP URL.
1197       */
1198      public void setScope(SearchScope scope)
1199      {
1200        if (scope == null)
1201        {
1202          this.scope = DEFAULT_SEARCH_SCOPE;
1203        }
1204        else
1205        {
1206          this.scope = scope;
1207        }
1208      }
1209    
1210    
1211    
1212      /**
1213       * Retrieves the raw, unprocessed search filter for this LDAP URL.
1214       *
1215       * @return  The raw, unprocessed search filter for this LDAP URL, or
1216       *          <CODE>null</CODE> if none was given (in which case a
1217       *          default filter of "(objectClass=*)" should be assumed).
1218       */
1219      public String getRawFilter()
1220      {
1221        return rawFilter;
1222      }
1223    
1224    
1225    
1226      /**
1227       * Specifies the raw, unprocessed search filter for this LDAP URL.
1228       *
1229       * @param  rawFilter  The raw, unprocessed search filter for this
1230       *                    LDAP URL.
1231       */
1232      public void setRawFilter(String rawFilter)
1233      {
1234        this.rawFilter = rawFilter;
1235        this.filter    = null;
1236      }
1237    
1238    
1239    
1240      /**
1241       * Retrieves the processed search filter for this LDAP URL.
1242       *
1243       * @return  The processed search filter for this LDAP URL.
1244       *
1245       * @throws  DirectoryException  If a problem occurs while attempting
1246       *                              to decode the raw filter.
1247       */
1248      public SearchFilter getFilter()
1249             throws DirectoryException
1250      {
1251        if (filter == null)
1252        {
1253          if (rawFilter == null)
1254          {
1255            filter = DEFAULT_SEARCH_FILTER;
1256          }
1257          else
1258          {
1259            filter = SearchFilter.createFilterFromString(rawFilter);
1260          }
1261        }
1262    
1263        return filter;
1264      }
1265    
1266    
1267    
1268      /**
1269       * Specifies the search filter for this LDAP URL.
1270       *
1271       * @param  filter  The search filter for this LDAP URL.
1272       */
1273      public void setFilter(SearchFilter filter)
1274      {
1275        if (filter == null)
1276        {
1277          this.rawFilter = null;
1278          this.filter    = null;
1279        }
1280        else
1281        {
1282          this.rawFilter = filter.toString();
1283          this.filter    = filter;
1284        }
1285      }
1286    
1287    
1288    
1289      /**
1290       * Retrieves the set of extensions for this LDAP URL.  The contents
1291       * of the returned list may be altered by the caller.
1292       *
1293       * @return  The set of extensions for this LDAP URL.
1294       */
1295      public LinkedList<String> getExtensions()
1296      {
1297        return extensions;
1298      }
1299    
1300    
1301    
1302      /**
1303       * Indicates whether the provided entry matches the criteria defined
1304       * in this LDAP URL.
1305       *
1306       * @param  entry  The entry for which to make the determination.
1307       *
1308       * @return  {@code true} if the provided entry does match the
1309       *          criteria specified in this LDAP URL, or {@code false} if
1310       *          it does not.
1311       *
1312       * @throws  DirectoryException  If a problem occurs while attempting
1313       *                              to make the determination.
1314       */
1315      public boolean matchesEntry(Entry entry)
1316             throws DirectoryException
1317      {
1318        SearchScope scope = getScope();
1319        if (scope == null)
1320        {
1321          scope = SearchScope.BASE_OBJECT;
1322        }
1323    
1324        return (entry.matchesBaseAndScope(getBaseDN(), scope) &&
1325                getFilter().matchesEntry(entry));
1326      }
1327    
1328    
1329    
1330      /**
1331       * Indicates whether the provided object is equal to this LDAP URL.
1332       *
1333       * @param  o  The object for which to make the determination.
1334       *
1335       * @return  <CODE>true</CODE> if the object is equal to this LDAP
1336       *          URL, or <CODE>false</CODE> if not.
1337       */
1338      public boolean equals(Object o)
1339      {
1340        if (o == null)
1341        {
1342          return false;
1343        }
1344    
1345        if (o == this)
1346        {
1347          return true;
1348        }
1349    
1350        if (! (o instanceof LDAPURL))
1351        {
1352          return false;
1353        }
1354    
1355        LDAPURL url = (LDAPURL) o;
1356    
1357        if (! scheme.equals(url.getScheme()))
1358        {
1359          return false;
1360        }
1361    
1362        if (host == null)
1363        {
1364          if (url.getHost() != null)
1365          {
1366            return false;
1367          }
1368        }
1369        else
1370        {
1371          if (! host.equalsIgnoreCase(url.getHost()))
1372          {
1373            return false;
1374          }
1375        }
1376    
1377        if (port != url.getPort())
1378        {
1379          return false;
1380        }
1381    
1382    
1383        try
1384        {
1385          DN dn = getBaseDN();
1386          if (! dn.equals(url.getBaseDN()))
1387          {
1388            return false;
1389          }
1390        }
1391        catch (Exception e)
1392        {
1393          if (debugEnabled())
1394          {
1395            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1396          }
1397    
1398          if (rawBaseDN == null)
1399          {
1400            if (url.getRawBaseDN() != null)
1401            {
1402              return false;
1403            }
1404          }
1405          else
1406          {
1407            if (! rawBaseDN.equals(url.getRawBaseDN()))
1408            {
1409              return false;
1410            }
1411          }
1412        }
1413    
1414    
1415        if (scope != url.getScope())
1416        {
1417          return false;
1418        }
1419    
1420    
1421        try
1422        {
1423          SearchFilter f = getFilter();
1424          if (! f.equals(url.getFilter()))
1425          {
1426            return false;
1427          }
1428        }
1429        catch (Exception e)
1430        {
1431          if (debugEnabled())
1432          {
1433            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1434          }
1435    
1436          if (rawFilter == null)
1437          {
1438            if (url.getRawFilter() != null)
1439            {
1440              return false;
1441            }
1442          }
1443          else
1444          {
1445            if (! rawFilter.equals(url.getRawFilter()))
1446            {
1447              return false;
1448            }
1449          }
1450        }
1451    
1452    
1453        if (attributes.size() != url.getAttributes().size())
1454        {
1455          return false;
1456        }
1457    
1458        LinkedHashSet<String> urlAttrs = url.getAttributes();
1459    outerAttrLoop:
1460        for (String attr : attributes)
1461        {
1462          if (urlAttrs.contains(attr))
1463          {
1464            continue;
1465          }
1466    
1467          for (String attr2 : urlAttrs)
1468          {
1469            if (attr.equalsIgnoreCase(attr2))
1470            {
1471              continue outerAttrLoop;
1472            }
1473          }
1474    
1475          return false;
1476        }
1477    
1478    
1479        if (extensions.size() != url.getExtensions().size())
1480        {
1481          return false;
1482        }
1483    
1484    outerExtLoop:
1485        for (String ext : extensions)
1486        {
1487          for (String urlExt : url.getExtensions())
1488          {
1489            if (ext.equals(urlExt))
1490            {
1491              continue outerExtLoop;
1492            }
1493          }
1494    
1495          return false;
1496        }
1497    
1498    
1499        // If we've gotten here, then we'll consider them equal.
1500        return true;
1501      }
1502    
1503    
1504    
1505      /**
1506       * Retrieves the hash code for this LDAP URL.
1507       *
1508       * @return  The hash code for this LDAP URL.
1509       */
1510      public int hashCode()
1511      {
1512        int hashCode = 0;
1513    
1514        hashCode += scheme.hashCode();
1515    
1516        if (host != null)
1517        {
1518          hashCode += toLowerCase(host).hashCode();
1519        }
1520    
1521        hashCode += port;
1522    
1523        try
1524        {
1525          hashCode += getBaseDN().hashCode();
1526        }
1527        catch (Exception e)
1528        {
1529          if (debugEnabled())
1530          {
1531            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1532          }
1533    
1534          if (rawBaseDN != null)
1535          {
1536            hashCode += rawBaseDN.hashCode();
1537          }
1538        }
1539    
1540        hashCode += getScope().intValue();
1541    
1542        for (String attr : attributes)
1543        {
1544          hashCode += toLowerCase(attr).hashCode();
1545        }
1546    
1547        try
1548        {
1549          hashCode += getFilter().hashCode();
1550        }
1551        catch (Exception e)
1552        {
1553          if (debugEnabled())
1554          {
1555            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1556          }
1557    
1558          if (rawFilter != null)
1559          {
1560            hashCode += rawFilter.hashCode();
1561          }
1562        }
1563    
1564        for (String ext : extensions)
1565        {
1566          hashCode += ext.hashCode();
1567        }
1568    
1569        return hashCode;
1570      }
1571    
1572    
1573    
1574      /**
1575       * Retrieves a string representation of this LDAP URL.
1576       *
1577       * @return  A string representation of this LDAP URL.
1578       */
1579      public String toString()
1580      {
1581        StringBuilder buffer = new StringBuilder();
1582        toString(buffer, false);
1583        return buffer.toString();
1584      }
1585    
1586    
1587    
1588      /**
1589       * Appends a string representation of this LDAP URL to the provided
1590       * buffer.
1591       *
1592       * @param  buffer    The buffer to which the information is to be
1593       *                   appended.
1594       * @param  baseOnly  Indicates whether the resulting URL string
1595       *                   should only include the portion up to the base
1596       *                   DN, omitting the attributes, scope, filter, and
1597       *                   extensions.
1598       */
1599      public void toString(StringBuilder buffer, boolean baseOnly)
1600      {
1601        urlEncode(scheme, false, buffer);
1602        buffer.append("://");
1603    
1604        if (host != null)
1605        {
1606          urlEncode(host, false, buffer);
1607          buffer.append(":");
1608          buffer.append(port);
1609        }
1610    
1611        buffer.append("/");
1612        urlEncode(rawBaseDN, false, buffer);
1613    
1614        if (baseOnly)
1615        {
1616          // If there are extensions, then we need to include them.
1617          // Technically, we only have to include critical extensions, but
1618          // we'll use all of them.
1619          if (! extensions.isEmpty())
1620          {
1621            buffer.append("????");
1622            Iterator<String> iterator = extensions.iterator();
1623            urlEncode(iterator.next(), true, buffer);
1624    
1625            while (iterator.hasNext())
1626            {
1627              buffer.append(",");
1628              urlEncode(iterator.next(), true, buffer);
1629            }
1630          }
1631    
1632          return;
1633        }
1634    
1635        buffer.append("?");
1636        if (! attributes.isEmpty())
1637        {
1638          Iterator<String> iterator = attributes.iterator();
1639          urlEncode(iterator.next(), false, buffer);
1640    
1641          while (iterator.hasNext())
1642          {
1643            buffer.append(",");
1644            urlEncode(iterator.next(), false, buffer);
1645          }
1646        }
1647    
1648        buffer.append("?");
1649        switch (scope)
1650        {
1651          case BASE_OBJECT:
1652            buffer.append("base");
1653            break;
1654          case SINGLE_LEVEL:
1655            buffer.append("one");
1656            break;
1657          case WHOLE_SUBTREE:
1658            buffer.append("sub");
1659            break;
1660          case SUBORDINATE_SUBTREE:
1661            buffer.append("subordinate");
1662            break;
1663        }
1664    
1665        buffer.append("?");
1666        urlEncode(rawFilter, false, buffer);
1667    
1668        if (! extensions.isEmpty())
1669        {
1670          buffer.append("?");
1671          Iterator<String> iterator = extensions.iterator();
1672          urlEncode(iterator.next(), true, buffer);
1673    
1674          while (iterator.hasNext())
1675          {
1676            buffer.append(",");
1677            urlEncode(iterator.next(), true, buffer);
1678          }
1679        }
1680      }
1681    }
1682