001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.shared.ldap.model.url;
021
022
023import java.io.ByteArrayOutputStream;
024import java.io.UnsupportedEncodingException;
025import java.text.ParseException;
026import java.util.ArrayList;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import org.apache.directory.shared.i18n.I18n;
034import org.apache.directory.shared.ldap.model.exception.LdapInvalidDnException;
035import org.apache.directory.shared.ldap.model.exception.LdapURLEncodingException;
036import org.apache.directory.shared.ldap.model.exception.LdapUriException;
037import org.apache.directory.shared.ldap.model.exception.UrlDecoderException;
038import org.apache.directory.shared.ldap.model.filter.FilterParser;
039import org.apache.directory.shared.ldap.model.message.SearchScope;
040import org.apache.directory.shared.ldap.model.name.Dn;
041import org.apache.directory.shared.util.Chars;
042import org.apache.directory.shared.util.StringConstants;
043import org.apache.directory.shared.util.Strings;
044import org.apache.directory.shared.util.Unicode;
045
046
047/**
048 * Decodes a LdapUrl, and checks that it complies with
049 * the RFC 2255. The grammar is the following :
050 * <pre>
051 * ldapurl    = scheme "://" [hostport] ["/"
052 *                   [dn ["?" [attributes] ["?" [scope]
053 *                   ["?" [filter] ["?" extensions]]]]]]
054 * scheme     = "ldap"
055 * attributes = attrdesc *("," attrdesc)
056 * scope      = "base" / "one" / "sub"
057 * dn         = Dn
058 * hostport   = hostport from Section 5 of RFC 1738
059 * attrdesc   = AttributeDescription from Section 4.1.5 of RFC 2251
060 * filter     = filter from Section 4 of RFC 2254
061 * extensions = extension *("," extension)
062 * extension  = ["!"] extype ["=" exvalue]
063 * extype     = token / xtoken
064 * exvalue    = LDAPString
065 * token      = oid from section 4.1 of RFC 2252
066 * xtoken     = ("X-" / "x-") token
067 * </pre>
068 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
069 */
070public class LdapUrl
071{
072    /** The constant for "ldaps://" scheme. */
073    public static final String LDAPS_SCHEME = "ldaps://";
074
075    /** The constant for "ldap://" scheme. */
076    public static final String LDAP_SCHEME = "ldap://";
077
078    /** A null LdapUrl */
079    public static final LdapUrl EMPTY_URL = new LdapUrl();
080
081    /** The scheme */
082    private String scheme;
083
084    /** The host */
085    private String host;
086
087    /** The port */
088    private int port;
089
090    /** The Dn */
091    private Dn dn;
092
093    /** The attributes */
094    private List<String> attributes;
095
096    /** The scope */
097    private SearchScope scope;
098
099    /** The filter as a string */
100    private String filter;
101
102    /** The extensions. */
103    private List<Extension> extensionList;
104
105    /** Stores the LdapUrl as a String */
106    private String string;
107
108    /** Stores the LdapUrl as a byte array */
109    private byte[] bytes;
110
111    /** modal parameter that forces explicit scope rendering in toString */
112    private boolean forceScopeRendering;
113
114    /** A regexp for attributes */
115    private static final Pattern ATTRIBUTE = Pattern.compile( "(?:(?:\\d|[1-9]\\d*)(?:\\.(?:\\d|[1-9]\\d*))+)|(?:[a-zA-Z][a-zA-Z0-9-]*)" );
116
117    /**
118     * Construct an empty LdapUrl
119     */
120    public LdapUrl()
121    {
122        scheme = LDAP_SCHEME;
123        host = null;
124        port = -1;
125        dn = null;
126        attributes = new ArrayList<String>();
127        scope = SearchScope.OBJECT;
128        filter = null;
129        extensionList = new ArrayList<Extension>( 2 );
130    }
131
132
133    /**
134     * Parse a LdapUrl.
135     * 
136     * @param chars The chars containing the URL
137     * @throws org.apache.directory.shared.ldap.model.exception.LdapURLEncodingException If the URL is invalid
138     */
139    private void parse( char[] chars ) throws LdapURLEncodingException
140    {
141        scheme = LDAP_SCHEME;
142        host = null;
143        port = -1;
144        dn = null;
145        attributes = new ArrayList<String>();
146        scope = SearchScope.OBJECT;
147        filter = null;
148        extensionList = new ArrayList<Extension>( 2 );
149
150        if ( ( chars == null ) || ( chars.length == 0 ) )
151        {
152            host = "";
153            return;
154        }
155
156        // ldapurl = scheme "://" [hostport] ["/"
157        // [dn ["?" [attributes] ["?" [scope]
158        // ["?" [filter] ["?" extensions]]]]]]
159        // scheme = "ldap"
160        int pos = 0;
161
162        // The scheme
163        if ( ( ( pos = Strings.areEquals(chars, 0, LDAP_SCHEME) ) == StringConstants.NOT_EQUAL )
164            && ( ( pos = Strings.areEquals(chars, 0, LDAPS_SCHEME) ) == StringConstants.NOT_EQUAL ) )
165        {
166            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04398 ) );
167        }
168        else
169        {
170            scheme = new String( chars, 0, pos );
171        }
172
173        // The hostport
174        if ( ( pos = parseHostPort( chars, pos ) ) == -1 )
175        {
176            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04399 ) );
177        }
178
179        if ( pos == chars.length )
180        {
181            return;
182        }
183
184        // An optional '/'
185        if ( !Chars.isCharASCII(chars, pos, '/') )
186        {
187            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04400, pos, chars[pos] ) );
188        }
189
190        pos++;
191
192        if ( pos == chars.length )
193        {
194            return;
195        }
196
197        // An optional Dn
198        if ( ( pos = parseDN( chars, pos ) ) == -1 )
199        {
200            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04401 ) );
201        }
202
203        if ( pos == chars.length )
204        {
205            return;
206        }
207
208        // Optionals attributes
209        if ( !Chars.isCharASCII(chars, pos, '?') )
210        {
211            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
212        }
213
214        pos++;
215
216        if ( ( pos = parseAttributes( chars, pos ) ) == -1 )
217        {
218            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04403 ) );
219        }
220
221        if ( pos == chars.length )
222        {
223            return;
224        }
225
226        // Optional scope
227        if ( !Chars.isCharASCII(chars, pos, '?') )
228        {
229            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
230        }
231
232        pos++;
233
234        if ( ( pos = parseScope( chars, pos ) ) == -1 )
235        {
236            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04404 ) );
237        }
238
239        if ( pos == chars.length )
240        {
241            return;
242        }
243
244        // Optional filter
245        if ( !Chars.isCharASCII(chars, pos, '?') )
246        {
247            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
248        }
249
250        pos++;
251
252        if ( pos == chars.length )
253        {
254            return;
255        }
256
257        if ( ( pos = parseFilter( chars, pos ) ) == -1 )
258        {
259            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04405 ) );
260        }
261
262        if ( pos == chars.length )
263        {
264            return;
265        }
266
267        // Optional extensions
268        if ( !Chars.isCharASCII(chars, pos, '?') )
269        {
270            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) );
271        }
272
273        pos++;
274
275        if ( ( pos = parseExtensions( chars, pos ) ) == -1 )
276        {
277            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04406 ) );
278        }
279
280        if ( pos == chars.length )
281        {
282            return;
283        }
284        else
285        {
286            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04407 ) );
287        }
288    }
289
290
291    /**
292     * Create a new LdapUrl from a String after having parsed it.
293     *
294     * @param string TheString that contains the LdapUrl
295     * @throws LdapURLEncodingException If the String does not comply with RFC 2255
296     */
297    public LdapUrl( String string ) throws LdapURLEncodingException
298    {
299        if (string == null )
300        {
301            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04408 ) );
302        }
303
304        try
305        {
306            bytes = string.getBytes( "UTF-8" );
307            this.string = string;
308            parse( string.toCharArray() );
309        }
310        catch ( UnsupportedEncodingException uee )
311        {
312            throw new LdapURLEncodingException( I18n.err( I18n.ERR_04409, string ) );
313        }
314    }
315
316
317    /**
318     * Parse this rule : <br>
319     * <p>
320     * &lt;host&gt; ::= &lt;hostname&gt; ':' &lt;hostnumber&gt;<br>
321     * &lt;hostname&gt; ::= *[ &lt;domainlabel&gt; "." ] &lt;toplabel&gt;<br>
322     * &lt;domainlabel&gt; ::= &lt;alphadigit&gt; | &lt;alphadigit&gt; *[
323     * &lt;alphadigit&gt; | "-" ] &lt;alphadigit&gt;<br>
324     * &lt;toplabel&gt; ::= &lt;alpha&gt; | &lt;alpha&gt; *[ &lt;alphadigit&gt; |
325     * "-" ] &lt;alphadigit&gt;<br>
326     * &lt;hostnumber&gt; ::= &lt;digits&gt; "." &lt;digits&gt; "."
327     * &lt;digits&gt; "." &lt;digits&gt;
328     * </p>
329     *
330     * @param chars The buffer to parse
331     * @param pos The current position in the byte buffer
332     * @return The new position in the byte buffer, or -1 if the rule does not
333     *         apply to the byte buffer TODO check that the topLabel is valid
334     *         (it must start with an alpha)
335     */
336    @SuppressWarnings("PMD.CollapsibleIfStatements")
337    // Used because of comments
338    private int parseHost( char[] chars, int pos )
339    {
340
341        int start = pos;
342        boolean hadDot = false;
343        boolean hadMinus = false;
344        boolean isHostNumber = true;
345        boolean invalidIp = false;
346        int nbDots = 0;
347        int[] ipElem = new int[4];
348
349        // The host will be followed by a '/' or a ':', or by nothing if it's
350        // the end.
351        // We will search the end of the host part, and we will check some
352        // elements.
353        if ( Chars.isCharASCII(chars, pos, '-') )
354        {
355
356            // We can't have a '-' on first position
357            return -1;
358        }
359
360        while ( ( pos < chars.length ) && ( chars[pos] != ':' ) && ( chars[pos] != '/' ) )
361        {
362
363            if ( Chars.isCharASCII(chars, pos, '.') )
364            {
365
366                if ( ( hadMinus ) || ( hadDot ) )
367                {
368
369                    // We already had a '.' just before : this is not allowed.
370                    // Or we had a '-' before a '.' : ths is not allowed either.
371                    return -1;
372                }
373
374                // Let's check the string we had before the dot.
375                if ( isHostNumber && ( nbDots < 4 ) )
376                {
377
378                    // We had only digits. It may be an IP adress? Check it
379                    if ( ipElem[nbDots] > 65535 )
380                    {
381                        invalidIp = true;
382                    }
383                }
384
385                hadDot = true;
386                nbDots++;
387                pos++;
388                continue;
389            }
390            else
391            {
392
393                if ( hadDot && Chars.isCharASCII(chars, pos, '-') )
394                {
395
396                    // We can't have a '-' just after a '.'
397                    return -1;
398                }
399
400                hadDot = false;
401            }
402
403            if ( Chars.isDigit(chars, pos) )
404            {
405
406                if ( isHostNumber && ( nbDots < 4 ) )
407                {
408                    ipElem[nbDots] = ( ipElem[nbDots] * 10 ) + ( chars[pos] - '0' );
409
410                    if ( ipElem[nbDots] > 65535 )
411                    {
412                        invalidIp = true;
413                    }
414                }
415
416                hadMinus = false;
417            }
418            else if ( Chars.isAlphaDigitMinus(chars, pos) )
419            {
420                isHostNumber = false;
421
422                hadMinus = Chars.isCharASCII(chars, pos, '-');
423            }
424            else
425            {
426                return -1;
427            }
428
429            pos++;
430        }
431
432        if ( start == pos )
433        {
434
435            // An empty host is valid
436            return pos;
437        }
438
439        // Checks the hostNumber
440        if ( isHostNumber )
441        {
442
443            // As this is a host number, we must have 3 dots.
444            if ( nbDots != 3 )
445            {
446                return -1;
447            }
448
449            if ( invalidIp )
450            {
451                return -1;
452            }
453        }
454
455        // Check if we have a '.' or a '-' in last position
456        if ( hadDot || hadMinus )
457        {
458            return -1;
459        }
460
461        host = new String( chars, start, pos - start );
462
463        return pos;
464    }
465
466
467    /**
468     * Parse this rule : <br>
469     * <p>
470     * &lt;port&gt; ::= &lt;digits&gt;<br>
471     * &lt;digits&gt; ::= &lt;digit&gt; &lt;digits-or-null&gt;<br>
472     * &lt;digits-or-null&gt; ::= &lt;digit&gt; &lt;digits-or-null&gt; | e<br>
473     * &lt;digit&gt; ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
474     * </p>
475     * The port must be between 0 and 65535.
476     *
477     * @param chars The buffer to parse
478     * @param pos The current position in the byte buffer
479     * @return The new position in the byte buffer, or -1 if the rule does not
480     *         apply to the byte buffer
481     */
482    private int parsePort( char[] chars, int pos )
483    {
484
485        if ( !Chars.isDigit(chars, pos) )
486        {
487            return -1;
488        }
489
490        port = chars[pos] - '0';
491
492        pos++;
493
494        while ( Chars.isDigit(chars, pos) )
495        {
496            port = ( port * 10 ) + ( chars[pos] - '0' );
497
498            if ( port > 65535 )
499            {
500                return -1;
501            }
502
503            pos++;
504        }
505
506        return pos;
507    }
508
509
510    /**
511     * Parse this rule : <br>
512     * <p>
513     * &lt;hostport&gt; ::= &lt;host&gt; ':' &lt;port&gt;
514     * </p>
515     *
516     * @param chars The char array to parse
517     * @param pos The current position in the byte buffer
518     * @return The new position in the byte buffer, or -1 if the rule does not
519     *         apply to the byte buffer
520     */
521    private int parseHostPort( char[] chars, int pos )
522    {
523        int hostPos = pos;
524
525        if ( ( pos = parseHost( chars, pos ) ) == -1 )
526        {
527            return -1;
528        }
529
530        // We may have a port.
531        if ( Chars.isCharASCII(chars, pos, ':') )
532        {
533            if ( pos == hostPos )
534            {
535                // We should not have a port if we have no host
536                return -1;
537            }
538
539            pos++;
540        }
541        else
542        {
543            return pos;
544        }
545
546        // As we have a ':', we must have a valid port (between 0 and 65535).
547        if ( ( pos = parsePort( chars, pos ) ) == -1 )
548        {
549            return -1;
550        }
551
552        return pos;
553    }
554    
555    
556    /**
557     * Converts the specified string to byte array of ASCII characters.
558     *
559     * @param data the string to be encoded
560     * @return The string as a byte array.
561     * @throws org.apache.directory.shared.ldap.model.exception.UrlDecoderException if encoding is not supported
562     */
563    private static byte[] getAsciiBytes( final String data ) throws UrlDecoderException
564    {
565
566        if ( data == null )
567        {
568            throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) );
569        }
570
571        try
572        {
573            return data.getBytes( "US-ASCII" );
574        }
575        catch ( UnsupportedEncodingException e )
576        {
577            throw new UrlDecoderException( I18n.err( I18n.ERR_04413 ) );
578        }
579    }
580
581
582    /**
583     * From commons-codec. Decodes an array of URL safe 7-bit characters into an
584     * array of original bytes. Escaped characters are converted back to their
585     * original representation.
586     *
587     * @param bytes array of URL safe characters
588     * @return array of original bytes
589     * @throws UrlDecoderException Thrown if URL decoding is unsuccessful
590     */
591    private static byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException
592    {
593        if ( bytes == null )
594        {
595            return StringConstants.EMPTY_BYTES;
596        }
597
598        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
599
600        for ( int i = 0; i < bytes.length; i++ )
601        {
602            int b = bytes[i];
603
604            if ( b == '%' )
605            {
606                try
607                {
608                    int u = Character.digit( ( char ) bytes[++i], 16 );
609                    int l = Character.digit( ( char ) bytes[++i], 16 );
610
611                    if ( ( u == -1 ) || ( l == -1 ) )
612                    {
613                        throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) );
614                    }
615
616                    buffer.write( ( char ) ( ( u << 4 ) + l ) );
617                }
618                catch ( ArrayIndexOutOfBoundsException e )
619                {
620                    throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) );
621                }
622            }
623            else
624            {
625                buffer.write( b );
626            }
627        }
628
629        return buffer.toByteArray();
630    }
631
632
633    /**
634     * From commons-httpclients. Unescape and decode a given string regarded as
635     * an escaped string with the default protocol charset.
636     *
637     * @param escaped a string
638     * @return the unescaped string
639     * @throws LdapUriException if the string cannot be decoded (invalid)
640     */
641    private static String decode( String escaped ) throws LdapUriException
642    {
643        try
644        {
645            byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) );
646            return Strings.getString( rawdata, "UTF-8" );
647        }
648        catch ( UrlDecoderException e )
649        {
650            throw new LdapUriException( e.getMessage(), e );
651        }
652    }
653
654
655    /**
656     * Parse a string and check that it complies with RFC 2253. Here, we will
657     * just call the Dn parser to do the job.
658     *
659     * @param chars The char array to be checked
660     * @param pos the starting position
661     * @return -1 if the char array does not contains a Dn
662     */
663    private int parseDN( char[] chars, int pos )
664    {
665
666        int end = pos;
667
668        for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
669        {
670            end++;
671        }
672
673        try
674        {
675            String dnStr = new String( chars, pos, end - pos );
676            dn = new Dn( decode( dnStr ) );
677        }
678        catch ( LdapUriException ue )
679        {
680            return -1;
681        }
682        catch ( LdapInvalidDnException de )
683        {
684            return -1;
685        }
686
687        return end;
688    }
689
690    /**
691     * Parse the following rule :
692     * <pre>
693     * oid ::= numericOid | descr
694     * descr ::= keystring
695     * keystring ::= leadkeychar *keychar
696     * leadkeychar ::= [a-zA-Z]
697     * keychar ::= [a-zA-Z0-0-]
698     * numericOid ::= number 1*( DOT number )
699     * number ::= 0 | [1-9][0-9]* 
700     * 
701     * @param attribute
702     * @throws LdapURLEncodingException
703     */
704    private void validateAttribute( String attribute ) throws LdapURLEncodingException
705    {
706        Matcher matcher = ATTRIBUTE.matcher( attribute );
707        
708        if ( !matcher.matches() )
709        {
710            throw new LdapURLEncodingException( "Attribute " + attribute + " is invalid" );
711        }
712    }
713
714    /**
715     * Parse the attributes part
716     *
717     * @param chars The char array to be checked
718     * @param pos the starting position
719     * @return -1 if the char array does not contains attributes
720     */
721    private int parseAttributes( char[] chars, int pos )
722    {
723        int start = pos;
724        int end = pos;
725        Set<String> hAttributes = new HashSet<String>();
726        boolean hadComma = false;
727
728        try
729        {
730
731            for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
732            {
733
734                if ( Chars.isCharASCII(chars, i, ',') )
735                {
736                    hadComma = true;
737
738                    if ( ( end - start ) == 0 )
739                    {
740
741                        // An attributes must not be null
742                        return -1;
743                    }
744                    else
745                    {
746                        String attribute = null;
747
748                        // get the attribute. It must not be blank
749                        attribute = new String( chars, start, end - start ).trim();
750
751                        if ( attribute.length() == 0 )
752                        {
753                            return -1;
754                        }
755                        
756                        // Check that the attribute is valid
757                        try
758                        {
759                            validateAttribute( attribute );
760                        }
761                        catch ( LdapURLEncodingException luee )
762                        {
763                            return -1;
764                        }
765
766                        String decodedAttr = decode( attribute );
767
768                        if ( !hAttributes.contains( decodedAttr ) )
769                        {
770                            attributes.add( decodedAttr );
771                            hAttributes.add( decodedAttr );
772                        }
773                    }
774
775                    start = i + 1;
776                }
777                else
778                {
779                    hadComma = false;
780                }
781
782                end++;
783            }
784
785            if ( hadComma )
786            {
787
788                // We are not allowed to have a comma at the end of the
789                // attributes
790                return -1;
791            }
792            else
793            {
794
795                if ( end == start )
796                {
797
798                    // We don't have any attributes. This is valid.
799                    return end;
800                }
801
802                // Store the last attribute
803                // get the attribute. It must not be blank
804                String attribute = null;
805
806                attribute = new String( chars, start, end - start ).trim();
807
808                if ( attribute.length() == 0 )
809                {
810                    return -1;
811                }
812
813                String decodedAttr = decode( attribute );
814
815                if ( !hAttributes.contains( decodedAttr ) )
816                {
817                    attributes.add( decodedAttr );
818                    hAttributes.add( decodedAttr );
819                }
820            }
821
822            return end;
823        }
824        catch ( LdapUriException ue )
825        {
826            return -1;
827        }
828    }
829
830
831    /**
832     * Parse the filter part. We will use the FilterParserImpl class
833     *
834     * @param chars The char array to be checked
835     * @param pos the starting position
836     * @return -1 if the char array does not contains a filter
837     */
838    private int parseFilter( char[] chars, int pos )
839    {
840
841        int end = pos;
842
843        for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ )
844        {
845            end++;
846        }
847
848        if ( end == pos )
849        {
850            // We have no filter
851            return end;
852        }
853
854        try
855        {
856            filter = decode( new String( chars, pos, end - pos ) );
857            FilterParser.parse( null, filter );
858        }
859        catch ( LdapUriException ue )
860        {
861            return -1;
862        }
863        catch ( ParseException pe )
864        {
865            return -1;
866        }
867
868        return end;
869    }
870
871
872    /**
873     * Parse the scope part.
874     *
875     * @param chars The char array to be checked
876     * @param pos the starting position
877     * @return -1 if the char array does not contains a scope
878     */
879    private int parseScope( char[] chars, int pos )
880    {
881
882        if ( Chars.isCharASCII(chars, pos, 'b') || Chars.isCharASCII(chars, pos, 'B') )
883        {
884            pos++;
885
886            if ( Chars.isCharASCII(chars, pos, 'a') || Chars.isCharASCII(chars, pos, 'A') )
887            {
888                pos++;
889
890                if ( Chars.isCharASCII(chars, pos, 's') || Chars.isCharASCII(chars, pos, 'S') )
891                {
892                    pos++;
893
894                    if ( Chars.isCharASCII(chars, pos, 'e') || Chars.isCharASCII(chars, pos, 'E') )
895                    {
896                        pos++;
897                        scope = SearchScope.OBJECT;
898                        return pos;
899                    }
900                }
901            }
902        }
903        else if ( Chars.isCharASCII(chars, pos, 'o') || Chars.isCharASCII(chars, pos, 'O') )
904        {
905            pos++;
906
907            if ( Chars.isCharASCII(chars, pos, 'n') || Chars.isCharASCII(chars, pos, 'N') )
908            {
909                pos++;
910
911                if ( Chars.isCharASCII(chars, pos, 'e') || Chars.isCharASCII(chars, pos, 'E') )
912                {
913                    pos++;
914
915                    scope = SearchScope.ONELEVEL;
916                    return pos;
917                }
918            }
919        }
920        else if ( Chars.isCharASCII(chars, pos, 's') || Chars.isCharASCII(chars, pos, 'S') )
921        {
922            pos++;
923
924            if ( Chars.isCharASCII(chars, pos, 'u') || Chars.isCharASCII(chars, pos, 'U') )
925            {
926                pos++;
927
928                if ( Chars.isCharASCII(chars, pos, 'b') || Chars.isCharASCII(chars, pos, 'B') )
929                {
930                    pos++;
931
932                    scope = SearchScope.SUBTREE;
933                    return pos;
934                }
935            }
936        }
937        else if ( Chars.isCharASCII(chars, pos, '?') )
938        {
939            // An empty scope. This is valid
940            return pos;
941        }
942        else if ( pos == chars.length )
943        {
944            // An empty scope at the end of the URL. This is valid
945            return pos;
946        }
947
948        // The scope is not one of "one", "sub" or "base". It's an error
949        return -1;
950    }
951
952
953    /**
954     * Parse extensions and critical extensions.
955     *
956     * The grammar is :
957     * extensions ::= extension [ ',' extension ]*
958     * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ]
959     *
960     * @param chars The char array to be checked
961     * @param pos the starting position
962     * @return -1 if the char array does not contains valid extensions or
963     *         critical extensions
964     */
965    private int parseExtensions( char[] chars, int pos )
966    {
967        int start = pos;
968        boolean isCritical = false;
969        boolean isNewExtension = true;
970        boolean hasValue = false;
971        String extension = null;
972        String value = null;
973
974        if ( pos == chars.length )
975        {
976            return pos;
977        }
978
979        try
980        {
981            for ( int i = pos; ( i < chars.length ); i++ )
982            {
983                if ( Chars.isCharASCII(chars, i, ',') )
984                {
985                    if ( isNewExtension )
986                    {
987                        // a ',' is not allowed when we have already had one
988                        // or if we just started to parse the extensions.
989                        return -1;
990                    }
991                    else
992                    {
993                        if ( extension == null )
994                        {
995                            extension = decode( new String( chars, start, i - start ) ).trim();
996                        }
997                        else
998                        {
999                            value = decode( new String( chars, start, i - start ) ).trim();
1000                        }
1001
1002                        Extension ext = new Extension( isCritical, extension, value );
1003                        extensionList.add( ext );
1004
1005                        isNewExtension = true;
1006                        hasValue = false;
1007                        isCritical = false;
1008                        start = i + 1;
1009                        extension = null;
1010                        value = null;
1011                    }
1012                }
1013                else if ( Chars.isCharASCII(chars, i, '=') )
1014                {
1015                    if ( hasValue )
1016                    {
1017                        // We may have two '=' for the same extension
1018                        continue;
1019                    }
1020
1021                    // An optionnal value
1022                    extension = decode( new String( chars, start, i - start ) ).trim();
1023
1024                    if ( extension.length() == 0 )
1025                    {
1026                        // We must have an extension
1027                        return -1;
1028                    }
1029
1030                    hasValue = true;
1031                    start = i + 1;
1032                }
1033                else if ( Chars.isCharASCII(chars, i, '!') )
1034                {
1035                    if ( hasValue )
1036                    {
1037                        // We may have two '!' in the value
1038                        continue;
1039                    }
1040
1041                    if ( !isNewExtension )
1042                    {
1043                        // '!' must appears first
1044                        return -1;
1045                    }
1046
1047                    isCritical = true;
1048                    start++;
1049                }
1050                else
1051                {
1052                    isNewExtension = false;
1053                }
1054            }
1055
1056            if ( extension == null )
1057            {
1058                extension = decode( new String( chars, start, chars.length - start ) ).trim();
1059            }
1060            else
1061            {
1062                value = decode( new String( chars, start, chars.length - start ) ).trim();
1063            }
1064
1065            Extension ext = new Extension( isCritical, extension, value );
1066            extensionList.add( ext );
1067
1068            return chars.length;
1069        }
1070        catch ( LdapUriException ue )
1071        {
1072            return -1;
1073        }
1074    }
1075
1076
1077    /**
1078     * Encode a String to avoid special characters.
1079     *
1080     *
1081     * RFC 4516, section 2.1. (Percent-Encoding)
1082     *
1083     * A generated LDAP URL MUST consist only of the restricted set of
1084     * characters included in one of the following three productions defined
1085     * in [RFC3986]:
1086     *
1087     *   <reserved>
1088     *   <unreserved>
1089     *   <pct-encoded>
1090     *
1091     * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as
1092     * input.  An octet MUST be encoded using the percent-encoding mechanism
1093     * described in section 2.1 of [RFC3986] in any of these situations:
1094     *
1095     *  The octet is not in the reserved set defined in section 2.2 of
1096     *  [RFC3986] or in the unreserved set defined in section 2.3 of
1097     *  [RFC3986].
1098     *
1099     *  It is the single Reserved character '?' and occurs inside a <dn>,
1100     *  <filter>, or other element of an LDAP URL.
1101     *
1102     *  It is a comma character ',' that occurs inside an <exvalue>.
1103     *
1104     *
1105     * RFC 3986, section 2.2 (Reserved Characters)
1106     *
1107     * reserved    = gen-delims / sub-delims
1108     * gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1109     * sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
1110     *              / "*" / "+" / "," / ";" / "="
1111     *
1112     *
1113     * RFC 3986, section 2.3 (Unreserved Characters)
1114     *
1115     * unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
1116     *
1117     *
1118     * @param url The String to encode
1119     * @param doubleEncode Set if we need to encode the comma
1120     * @return An encoded string
1121     */
1122    public static String urlEncode( String url, boolean doubleEncode )
1123    {
1124        StringBuffer sb = new StringBuffer();
1125
1126        for ( int i = 0; i < url.length(); i++ )
1127        {
1128            char c = url.charAt( i );
1129
1130            switch ( c )
1131
1132            {
1133                // reserved and unreserved characters:
1134                // just append to the buffer
1135
1136                // reserved gen-delims, excluding '?'
1137                // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
1138                case ':':
1139                case '/':
1140                case '#':
1141                case '[':
1142                case ']':
1143                case '@':
1144
1145                    // reserved sub-delims, excluding ','
1146                    // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
1147                    //               / "*" / "+" / "," / ";" / "="
1148                case '!':
1149                case '$':
1150                case '&':
1151                case '\'':
1152                case '(':
1153                case ')':
1154                case '*':
1155                case '+':
1156                case ';':
1157                case '=':
1158
1159                    // unreserved
1160                    // unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
1161                case 'a':
1162                case 'b':
1163                case 'c':
1164                case 'd':
1165                case 'e':
1166                case 'f':
1167                case 'g':
1168                case 'h':
1169                case 'i':
1170                case 'j':
1171                case 'k':
1172                case 'l':
1173                case 'm':
1174                case 'n':
1175                case 'o':
1176                case 'p':
1177                case 'q':
1178                case 'r':
1179                case 's':
1180                case 't':
1181                case 'u':
1182                case 'v':
1183                case 'w':
1184                case 'x':
1185                case 'y':
1186                case 'z':
1187
1188                case 'A':
1189                case 'B':
1190                case 'C':
1191                case 'D':
1192                case 'E':
1193                case 'F':
1194                case 'G':
1195                case 'H':
1196                case 'I':
1197                case 'J':
1198                case 'K':
1199                case 'L':
1200                case 'M':
1201                case 'N':
1202                case 'O':
1203                case 'P':
1204                case 'Q':
1205                case 'R':
1206                case 'S':
1207                case 'T':
1208                case 'U':
1209                case 'V':
1210                case 'W':
1211                case 'X':
1212                case 'Y':
1213                case 'Z':
1214
1215                case '0':
1216                case '1':
1217                case '2':
1218                case '3':
1219                case '4':
1220                case '5':
1221                case '6':
1222                case '7':
1223                case '8':
1224                case '9':
1225
1226                case '-':
1227                case '.':
1228                case '_':
1229                case '~':
1230
1231                    sb.append( c );
1232                    break;
1233
1234                case ',':
1235
1236                    // special case for comma
1237                    if ( doubleEncode )
1238                    {
1239                        sb.append( "%2c" );
1240                    }
1241                    else
1242                    {
1243                        sb.append( c );
1244                    }
1245                    break;
1246
1247                default:
1248
1249                    // percent encoding
1250                    byte[] bytes = Unicode.charToBytes(c);
1251                    char[] hex = Strings.toHexString( bytes ).toCharArray();
1252                    for ( int j = 0; j < hex.length; j++ )
1253                    {
1254                        if ( j % 2 == 0 )
1255                        {
1256                            sb.append( '%' );
1257                        }
1258                        sb.append( hex[j] );
1259
1260                    }
1261
1262                    break;
1263            }
1264        }
1265
1266        return sb.toString();
1267    }
1268
1269
1270    /**
1271     * Get a string representation of a LdapUrl.
1272     *
1273     * @return A LdapUrl string
1274     * @see LdapUrl#forceScopeRendering
1275     */
1276    @Override
1277    public String toString()
1278    {
1279        StringBuffer sb = new StringBuffer();
1280
1281        sb.append( scheme );
1282
1283        sb.append( ( host == null ) ? "" : host );
1284
1285        if ( port != -1 )
1286        {
1287            sb.append( ':' ).append( port );
1288        }
1289
1290        if ( dn != null )
1291        {
1292            sb.append( '/' ).append( urlEncode( dn.getName(), false ) );
1293
1294            if ( ( attributes.size() != 0 ) || forceScopeRendering
1295                || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) )
1296            {
1297                sb.append( '?' );
1298
1299                boolean isFirst = true;
1300
1301                for ( String attribute : attributes )
1302                {
1303                    if ( isFirst )
1304                    {
1305                        isFirst = false;
1306                    }
1307                    else
1308                    {
1309                        sb.append( ',' );
1310                    }
1311
1312                    sb.append( urlEncode( attribute, false ) );
1313                }
1314            }
1315
1316            if ( forceScopeRendering )
1317            {
1318                sb.append( '?' );
1319
1320                sb.append( scope.getLdapUrlValue() );
1321            }
1322            else
1323            {
1324                if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) )
1325                {
1326                    sb.append( '?' );
1327
1328                    switch ( scope )
1329                    {
1330                        case ONELEVEL:
1331                        case SUBTREE:
1332                            sb.append( scope.getLdapUrlValue() );
1333                            break;
1334
1335                        default:
1336                            break;
1337                    }
1338
1339                    if ( ( filter != null ) || ( ( extensionList.size() != 0 ) ) )
1340                    {
1341                        sb.append( "?" );
1342
1343                        if ( filter != null )
1344                        {
1345                            sb.append( urlEncode( filter, false ) );
1346                        }
1347
1348                        if ( ( extensionList.size() != 0 ) )
1349                        {
1350                            sb.append( '?' );
1351
1352                            boolean isFirst = true;
1353
1354                            if ( extensionList.size() != 0 )
1355                            {
1356                                for ( Extension extension : extensionList )
1357                                {
1358                                    if ( !isFirst )
1359                                    {
1360                                        sb.append( ',' );
1361                                    }
1362                                    else
1363                                    {
1364                                        isFirst = false;
1365                                    }
1366
1367                                    if ( extension.isCritical )
1368                                    {
1369                                        sb.append( '!' );
1370                                    }
1371                                    sb.append( urlEncode( extension.type, false ) );
1372
1373                                    if ( extension.value != null )
1374                                    {
1375                                        sb.append( '=' );
1376                                        sb.append( urlEncode( extension.value, true ) );
1377                                    }
1378                                }
1379                            }
1380                        }
1381                    }
1382                }
1383            }
1384        }
1385        else
1386        {
1387            sb.append( '/' );
1388        }
1389
1390        return sb.toString();
1391    }
1392
1393
1394    /**
1395     * @return Returns the attributes.
1396     */
1397    public List<String> getAttributes()
1398    {
1399        return attributes;
1400    }
1401
1402
1403    /**
1404     * @return Returns the dn.
1405     */
1406    public Dn getDn()
1407    {
1408        return dn;
1409    }
1410
1411
1412    /**
1413     * @return Returns the extensions.
1414     */
1415    public List<Extension> getExtensions()
1416    {
1417        return extensionList;
1418    }
1419
1420
1421    /**
1422     * Gets the extension.
1423     *
1424     * @param type the extension type, case-insensitive
1425     *
1426     * @return Returns the extension, null if this URL does not contain
1427     *         such an extension.
1428     */
1429    public Extension getExtension( String type )
1430    {
1431        for ( Extension extension : getExtensions() )
1432        {
1433            if ( extension.getType().equalsIgnoreCase( type ) )
1434            {
1435                return extension;
1436            }
1437        }
1438        return null;
1439    }
1440
1441
1442    /**
1443     * Gets the extension value.
1444     *
1445     * @param type the extension type, case-insensitive
1446     *
1447     * @return Returns the extension value, null if this URL does not
1448     *         contain such an extension or if the extension value is null.
1449     */
1450    public String getExtensionValue( String type )
1451    {
1452        for ( Extension extension : getExtensions() )
1453        {
1454            if ( extension.getType().equalsIgnoreCase( type ) )
1455            {
1456                return extension.getValue();
1457            }
1458        }
1459        return null;
1460    }
1461
1462
1463    /**
1464     * @return Returns the filter.
1465     */
1466    public String getFilter()
1467    {
1468        return filter;
1469    }
1470
1471
1472    /**
1473     * @return Returns the host.
1474     */
1475    public String getHost()
1476    {
1477        return host;
1478    }
1479
1480
1481    /**
1482     * @return Returns the port.
1483     */
1484    public int getPort()
1485    {
1486        return port;
1487    }
1488
1489
1490    /**
1491     * Returns the scope, one of {@link SearchScope#OBJECT},
1492     * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}.
1493     *
1494     * @return Returns the scope.
1495     */
1496    public SearchScope getScope()
1497    {
1498        return scope;
1499    }
1500
1501
1502    /**
1503     * @return Returns the scheme.
1504     */
1505    public String getScheme()
1506    {
1507        return scheme;
1508    }
1509
1510
1511    /**
1512     * @return the number of bytes for this LdapUrl
1513     */
1514    public int getNbBytes()
1515    {
1516        return ( bytes != null ? bytes.length : 0 );
1517    }
1518
1519
1520    /**
1521     * @return a reference on the interned bytes representing this LdapUrl
1522     */
1523    public byte[] getBytesReference()
1524    {
1525        return bytes;
1526    }
1527
1528
1529    /**
1530     * @return a copy of the bytes representing this LdapUrl
1531     */
1532    public byte[] getBytesCopy()
1533    {
1534        if ( bytes != null )
1535        {
1536            byte[] copy = new byte[bytes.length];
1537            System.arraycopy( bytes, 0, copy, 0, bytes.length );
1538            return copy;
1539        }
1540        else
1541        {
1542            return null;
1543        }
1544    }
1545
1546
1547    /**
1548     * @return the LdapUrl as a String
1549     */
1550    public String getString()
1551    {
1552        return string;
1553    }
1554
1555
1556    /**
1557     * {@inheritDoc}
1558     */
1559    @Override
1560    public int hashCode()
1561    {
1562        return this.toString().hashCode();
1563    }
1564
1565
1566    /**
1567     * {@inheritDoc}
1568     */
1569    @Override
1570    public boolean equals( Object obj )
1571    {
1572        if ( this == obj )
1573        {
1574            return true;
1575        }
1576        if ( obj == null )
1577        {
1578            return false;
1579        }
1580        if ( getClass() != obj.getClass() )
1581        {
1582            return false;
1583        }
1584
1585        final LdapUrl other = ( LdapUrl ) obj;
1586        return this.toString().equals( other.toString() );
1587    }
1588
1589
1590    /**
1591     * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default.
1592     *
1593     * @param scheme the new scheme
1594     */
1595    public void setScheme( String scheme )
1596    {
1597        if ( ( ( scheme != null ) && LDAP_SCHEME.equals( scheme ) ) || LDAPS_SCHEME.equals( scheme ) )
1598        {
1599            this.scheme = scheme;
1600        }
1601        else
1602        {
1603            this.scheme = LDAP_SCHEME;
1604        }
1605
1606    }
1607
1608
1609    /**
1610     * Sets the host.
1611     *
1612     * @param host the new host
1613     */
1614    public void setHost( String host )
1615    {
1616        this.host = host;
1617    }
1618
1619
1620    /**
1621     * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default.
1622     *
1623     * @param port the new port
1624     */
1625    public void setPort( int port )
1626    {
1627        if ( ( port < 1 ) || ( port > 65535 ) )
1628        {
1629            this.port = -1;
1630        }
1631        else
1632        {
1633            this.port = port;
1634        }
1635    }
1636
1637
1638    /**
1639     * Sets the dn.
1640     *
1641     * @param dn the new dn
1642     */
1643    public void setDn( Dn dn )
1644    {
1645        this.dn = dn;
1646    }
1647
1648
1649    /**
1650     * Sets the attributes, null removes all existing attributes.
1651     *
1652     * @param attributes the new attributes
1653     */
1654    public void setAttributes( List<String> attributes )
1655    {
1656        if ( attributes == null )
1657        {
1658            this.attributes.clear();
1659        }
1660        else
1661        {
1662            this.attributes = attributes;
1663        }
1664    }
1665
1666
1667    /**
1668     * Sets the scope. Must be one of {@link SearchScope#OBJECT},
1669     * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE},
1670     * otherwise {@link SearchScope#OBJECT} is assumed as default.
1671     *
1672     * @param scope the new scope
1673     */
1674    public void setScope( int scope )
1675    {
1676        try
1677        {
1678            this.scope = SearchScope.getSearchScope( scope );
1679        }
1680        catch ( IllegalArgumentException iae )
1681        {
1682            this.scope = SearchScope.OBJECT;
1683        }
1684    }
1685
1686
1687    /**
1688     * Sets the scope. Must be one of {@link SearchScope#OBJECT},
1689     * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE},
1690     * otherwise {@link SearchScope#OBJECT} is assumed as default.
1691     *
1692     * @param scope the new scope
1693     */
1694    public void setScope( SearchScope scope )
1695    {
1696        if ( scope == null )
1697        {
1698            this.scope = SearchScope.OBJECT;
1699        }
1700        else
1701        {
1702            this.scope = scope;
1703        }
1704    }
1705
1706
1707    /**
1708     * Sets the filter.
1709     *
1710     * @param filter the new filter
1711     */
1712    public void setFilter( String filter )
1713    {
1714        this.filter = filter;
1715    }
1716
1717
1718    /**
1719     * If set to true forces the toString method to render the scope
1720     * regardless of optional nature.  Use this when you want explicit
1721     * search URL scope rendering.
1722     *
1723     * @param forceScopeRendering the forceScopeRendering to set
1724     */
1725    public void setForceScopeRendering( boolean forceScopeRendering )
1726    {
1727        this.forceScopeRendering = forceScopeRendering;
1728    }
1729
1730
1731    /**
1732     * An inner bean to hold extension information.
1733     *
1734     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
1735         */
1736    public static class Extension
1737    {
1738        private boolean isCritical;
1739        private String type;
1740        private String value;
1741
1742
1743        /**
1744         * Creates a new instance of Extension.
1745         *
1746         * @param isCritical true for critical extension
1747         * @param type the extension type
1748         * @param value the extension value
1749         */
1750        public Extension( boolean isCritical, String type, String value )
1751        {
1752            super();
1753            this.isCritical = isCritical;
1754            this.type = type;
1755            this.value = value;
1756        }
1757
1758
1759        /**
1760         * Checks if is critical.
1761         *
1762         * @return true, if is critical
1763         */
1764        public boolean isCritical()
1765        {
1766            return isCritical;
1767        }
1768
1769
1770        /**
1771         * Sets the critical flag.
1772         *
1773         * @param critical the new critical flag
1774         */
1775        public void setCritical( boolean critical )
1776        {
1777            this.isCritical = critical;
1778        }
1779
1780
1781        /**
1782         * Gets the type.
1783         *
1784         * @return the type
1785         */
1786        public String getType()
1787        {
1788            return type;
1789        }
1790
1791
1792        /**
1793         * Sets the type.
1794         *
1795         * @param type the new type
1796         */
1797        public void setType( String type )
1798        {
1799            this.type = type;
1800        }
1801
1802
1803        /**
1804         * Gets the value.
1805         *
1806         * @return the value
1807         */
1808        public String getValue()
1809        {
1810            return value;
1811        }
1812
1813
1814        /**
1815         * Sets the value.
1816         *
1817         * @param value the new value
1818         */
1819        public void setValue( String value )
1820        {
1821            this.value = value;
1822        }
1823    }
1824}