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