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