| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| package org.apache.directory.shared.ldap.model.filter; |
| |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.directory.shared.i18n.I18n; |
| import org.apache.directory.shared.ldap.model.exception.LdapInvalidDnException; |
| import org.apache.directory.shared.ldap.model.exception.LdapURLEncodingException; |
| import org.apache.directory.shared.ldap.model.exception.LdapUriException; |
| import org.apache.directory.shared.ldap.model.exception.UrlDecoderException; |
| import org.apache.directory.shared.ldap.model.name.Dn; |
| import org.apache.directory.shared.util.Chars; |
| import org.apache.directory.shared.util.StringConstants; |
| import org.apache.directory.shared.util.Strings; |
| import org.apache.directory.shared.util.Unicode; |
| |
| |
| /** |
| * Decodes a LdapUrl, and checks that it complies with |
| * the RFC 2255. The grammar is the following : |
| * <pre> |
| * ldapurl = scheme "://" [hostport] ["/" |
| * [dn ["?" [attributes] ["?" [scope] |
| * ["?" [filter] ["?" extensions]]]]]] |
| * scheme = "ldap" |
| * attributes = attrdesc *("," attrdesc) |
| * scope = "base" / "one" / "sub" |
| * dn = Dn |
| * hostport = hostport from Section 5 of RFC 1738 |
| * attrdesc = AttributeDescription from Section 4.1.5 of RFC 2251 |
| * filter = filter from Section 4 of RFC 2254 |
| * extensions = extension *("," extension) |
| * extension = ["!"] extype ["=" exvalue] |
| * extype = token / xtoken |
| * exvalue = LDAPString |
| * token = oid from section 4.1 of RFC 2252 |
| * xtoken = ("X-" / "x-") token |
| * </pre> |
| * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> |
| */ |
| public class LdapURL |
| { |
| |
| // ~ Static fields/initializers |
| // ----------------------------------------------------------------- |
| |
| /** The constant for "ldaps://" scheme. */ |
| public static final String LDAPS_SCHEME = "ldaps://"; |
| |
| /** The constant for "ldap://" scheme. */ |
| public static final String LDAP_SCHEME = "ldap://"; |
| |
| /** A null LdapURL */ |
| public static final LdapURL EMPTY_URL = new LdapURL(); |
| |
| // ~ Instance fields |
| // ---------------------------------------------------------------------------- |
| |
| /** The scheme */ |
| private String scheme; |
| |
| /** The host */ |
| private String host; |
| |
| /** The port */ |
| private int port; |
| |
| /** The Dn */ |
| private Dn dn; |
| |
| /** The attributes */ |
| private List<String> attributes; |
| |
| /** The scope */ |
| private SearchScope scope; |
| |
| /** The filter as a string */ |
| private String filter; |
| |
| /** The extensions. */ |
| private List<Extension> extensionList; |
| |
| /** Stores the LdapURL as a String */ |
| private String string; |
| |
| /** Stores the LdapURL as a byte array */ |
| private byte[] bytes; |
| |
| /** modal parameter that forces explicit scope rendering in toString */ |
| private boolean forceScopeRendering; |
| |
| |
| // ~ Constructors |
| // ------------------------------------------------------------------------------- |
| |
| /** |
| * Construct an empty LdapURL |
| */ |
| public LdapURL() |
| { |
| scheme = LDAP_SCHEME; |
| host = null; |
| port = -1; |
| dn = null; |
| attributes = new ArrayList<String>(); |
| scope = SearchScope.OBJECT; |
| filter = null; |
| extensionList = new ArrayList<Extension>( 2 ); |
| } |
| |
| |
| /** |
| * Parse a LdapURL |
| * @param chars The chars containing the URL |
| * @throws org.apache.directory.shared.ldap.model.exception.LdapURLEncodingException If the URL is invalid |
| */ |
| public void parse( char[] chars ) throws LdapURLEncodingException |
| { |
| scheme = LDAP_SCHEME; |
| host = null; |
| port = -1; |
| dn = null; |
| attributes = new ArrayList<String>(); |
| scope = SearchScope.OBJECT; |
| filter = null; |
| extensionList = new ArrayList<Extension>( 2 ); |
| |
| if ( ( chars == null ) || ( chars.length == 0 ) ) |
| { |
| host = ""; |
| return; |
| } |
| |
| // ldapurl = scheme "://" [hostport] ["/" |
| // [dn ["?" [attributes] ["?" [scope] |
| // ["?" [filter] ["?" extensions]]]]]] |
| // scheme = "ldap" |
| |
| int pos = 0; |
| |
| // The scheme |
| if ( ( ( pos = Strings.areEquals(chars, 0, LDAP_SCHEME) ) == StringConstants.NOT_EQUAL ) |
| && ( ( pos = Strings.areEquals(chars, 0, LDAPS_SCHEME) ) == StringConstants.NOT_EQUAL ) ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04398 ) ); |
| } |
| else |
| { |
| scheme = new String( chars, 0, pos ); |
| } |
| |
| // The hostport |
| if ( ( pos = parseHostPort( chars, pos ) ) == -1 ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04399 ) ); |
| } |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| // An optional '/' |
| if ( !Chars.isCharASCII(chars, pos, '/') ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04400, pos, chars[pos] ) ); |
| } |
| |
| pos++; |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| // An optional Dn |
| if ( ( pos = parseDN( chars, pos ) ) == -1 ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04401 ) ); |
| } |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| // Optionals attributes |
| if ( !Chars.isCharASCII(chars, pos, '?') ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); |
| } |
| |
| pos++; |
| |
| if ( ( pos = parseAttributes( chars, pos ) ) == -1 ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04403 ) ); |
| } |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| // Optional scope |
| if ( !Chars.isCharASCII(chars, pos, '?') ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); |
| } |
| |
| pos++; |
| |
| if ( ( pos = parseScope( chars, pos ) ) == -1 ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04404 ) ); |
| } |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| // Optional filter |
| if ( !Chars.isCharASCII(chars, pos, '?') ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); |
| } |
| |
| pos++; |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| if ( ( pos = parseFilter( chars, pos ) ) == -1 ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04405 ) ); |
| } |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| |
| // Optional extensions |
| if ( !Chars.isCharASCII(chars, pos, '?') ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04402, pos, chars[pos] ) ); |
| } |
| |
| pos++; |
| |
| if ( ( pos = parseExtensions( chars, pos ) ) == -1 ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04406 ) ); |
| } |
| |
| if ( pos == chars.length ) |
| { |
| return; |
| } |
| else |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04407 ) ); |
| } |
| } |
| |
| |
| /** |
| * Create a new LdapURL from a String after having parsed it. |
| * |
| * @param string TheString that contains the LDAPURL |
| * @throws LdapURLEncodingException If the String does not comply with RFC 2255 |
| */ |
| public LdapURL( String string ) throws LdapURLEncodingException |
| { |
| if ( string == null ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04408 ) ); |
| } |
| |
| try |
| { |
| bytes = string.getBytes( "UTF-8" ); |
| this.string = string; |
| parse( string.toCharArray() ); |
| } |
| catch ( UnsupportedEncodingException uee ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04409, string ) ); |
| } |
| } |
| |
| |
| /** |
| * Create a new LdapURL after having parsed it. |
| * |
| * @param bytes The byte buffer that contains the LDAPURL |
| * @throws LdapURLEncodingException If the byte array does not comply with RFC 2255 |
| */ |
| public LdapURL( byte[] bytes ) throws LdapURLEncodingException |
| { |
| if ( ( bytes == null ) || ( bytes.length == 0 ) ) |
| { |
| throw new LdapURLEncodingException( I18n.err( I18n.ERR_04410 ) ); |
| } |
| |
| string = Strings.utf8ToString(bytes); |
| |
| this.bytes = new byte[bytes.length]; |
| System.arraycopy( bytes, 0, this.bytes, 0, bytes.length ); |
| |
| parse( string.toCharArray() ); |
| } |
| |
| |
| // ~ Methods |
| // ------------------------------------------------------------------------------------ |
| |
| /** |
| * Parse this rule : <br> |
| * <p> |
| * <host> ::= <hostname> ':' <hostnumber><br> |
| * <hostname> ::= *[ <domainlabel> "." ] <toplabel><br> |
| * <domainlabel> ::= <alphadigit> | <alphadigit> *[ |
| * <alphadigit> | "-" ] <alphadigit><br> |
| * <toplabel> ::= <alpha> | <alpha> *[ <alphadigit> | |
| * "-" ] <alphadigit><br> |
| * <hostnumber> ::= <digits> "." <digits> "." |
| * <digits> "." <digits> |
| * </p> |
| * |
| * @param chars The buffer to parse |
| * @param pos The current position in the byte buffer |
| * @return The new position in the byte buffer, or -1 if the rule does not |
| * apply to the byte buffer TODO check that the topLabel is valid |
| * (it must start with an alpha) |
| */ |
| @SuppressWarnings("PMD.CollapsibleIfStatements") |
| // Used because of comments |
| private int parseHost( char[] chars, int pos ) |
| { |
| |
| int start = pos; |
| boolean hadDot = false; |
| boolean hadMinus = false; |
| boolean isHostNumber = true; |
| boolean invalidIp = false; |
| int nbDots = 0; |
| int[] ipElem = new int[4]; |
| |
| // The host will be followed by a '/' or a ':', or by nothing if it's |
| // the end. |
| // We will search the end of the host part, and we will check some |
| // elements. |
| if ( Chars.isCharASCII(chars, pos, '-') ) |
| { |
| |
| // We can't have a '-' on first position |
| return -1; |
| } |
| |
| while ( ( pos < chars.length ) && ( chars[pos] != ':' ) && ( chars[pos] != '/' ) ) |
| { |
| |
| if ( Chars.isCharASCII(chars, pos, '.') ) |
| { |
| |
| if ( ( hadMinus ) || ( hadDot ) ) |
| { |
| |
| // We already had a '.' just before : this is not allowed. |
| // Or we had a '-' before a '.' : ths is not allowed either. |
| return -1; |
| } |
| |
| // Let's check the string we had before the dot. |
| if ( isHostNumber && ( nbDots < 4 ) ) |
| { |
| |
| // We had only digits. It may be an IP adress? Check it |
| if ( ipElem[nbDots] > 65535 ) |
| { |
| invalidIp = true; |
| } |
| } |
| |
| hadDot = true; |
| nbDots++; |
| pos++; |
| continue; |
| } |
| else |
| { |
| |
| if ( hadDot && Chars.isCharASCII(chars, pos, '-') ) |
| { |
| |
| // We can't have a '-' just after a '.' |
| return -1; |
| } |
| |
| hadDot = false; |
| } |
| |
| if ( Chars.isDigit(chars, pos) ) |
| { |
| |
| if ( isHostNumber && ( nbDots < 4 ) ) |
| { |
| ipElem[nbDots] = ( ipElem[nbDots] * 10 ) + ( chars[pos] - '0' ); |
| |
| if ( ipElem[nbDots] > 65535 ) |
| { |
| invalidIp = true; |
| } |
| } |
| |
| hadMinus = false; |
| } |
| else if ( Chars.isAlphaDigitMinus(chars, pos) ) |
| { |
| isHostNumber = false; |
| |
| hadMinus = Chars.isCharASCII(chars, pos, '-'); |
| } |
| else |
| { |
| return -1; |
| } |
| |
| pos++; |
| } |
| |
| if ( start == pos ) |
| { |
| |
| // An empty host is valid |
| return pos; |
| } |
| |
| // Checks the hostNumber |
| if ( isHostNumber ) |
| { |
| |
| // As this is a host number, we must have 3 dots. |
| if ( nbDots != 3 ) |
| { |
| return -1; |
| } |
| |
| if ( invalidIp ) |
| { |
| return -1; |
| } |
| } |
| |
| // Check if we have a '.' or a '-' in last position |
| if ( hadDot || hadMinus ) |
| { |
| return -1; |
| } |
| |
| host = new String( chars, start, pos - start ); |
| |
| return pos; |
| } |
| |
| |
| /** |
| * Parse this rule : <br> |
| * <p> |
| * <port> ::= <digits><br> |
| * <digits> ::= <digit> <digits-or-null><br> |
| * <digits-or-null> ::= <digit> <digits-or-null> | e<br> |
| * <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| * </p> |
| * The port must be between 0 and 65535. |
| * |
| * @param chars The buffer to parse |
| * @param pos The current position in the byte buffer |
| * @return The new position in the byte buffer, or -1 if the rule does not |
| * apply to the byte buffer |
| */ |
| private int parsePort( char[] chars, int pos ) |
| { |
| |
| if ( !Chars.isDigit(chars, pos) ) |
| { |
| return -1; |
| } |
| |
| port = chars[pos] - '0'; |
| |
| pos++; |
| |
| while ( Chars.isDigit(chars, pos) ) |
| { |
| port = ( port * 10 ) + ( chars[pos] - '0' ); |
| |
| if ( port > 65535 ) |
| { |
| return -1; |
| } |
| |
| pos++; |
| } |
| |
| return pos; |
| } |
| |
| |
| /** |
| * Parse this rule : <br> |
| * <p> |
| * <hostport> ::= <host> ':' <port> |
| * </p> |
| * |
| * @param chars The char array to parse |
| * @param pos The current position in the byte buffer |
| * @return The new position in the byte buffer, or -1 if the rule does not |
| * apply to the byte buffer |
| */ |
| private int parseHostPort( char[] chars, int pos ) |
| { |
| int hostPos = pos; |
| |
| if ( ( pos = parseHost( chars, pos ) ) == -1 ) |
| { |
| return -1; |
| } |
| |
| // We may have a port. |
| if ( Chars.isCharASCII(chars, pos, ':') ) |
| { |
| if ( pos == hostPos ) |
| { |
| // We should not have a port if we have no host |
| return -1; |
| } |
| |
| pos++; |
| } |
| else |
| { |
| return pos; |
| } |
| |
| // As we have a ':', we must have a valid port (between 0 and 65535). |
| if ( ( pos = parsePort( chars, pos ) ) == -1 ) |
| { |
| return -1; |
| } |
| |
| return pos; |
| } |
| |
| |
| /** |
| * From commons-httpclients. Converts the byte array of HTTP content |
| * characters to a string. If the specified charset is not supported, |
| * default system encoding is used. |
| * |
| * @param data the byte array to be encoded |
| * @param offset the index of the first byte to encode |
| * @param length the number of bytes to encode |
| * @param charset the desired character encoding |
| * @return The result of the conversion. |
| * @since 3.0 |
| */ |
| public static String getString( final byte[] data, int offset, int length, String charset ) |
| { |
| if ( data == null ) |
| { |
| throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) ); |
| } |
| |
| if ( ( charset == null ) || ( charset.length() == 0 ) ) |
| { |
| throw new IllegalArgumentException( I18n.err( I18n.ERR_04412 ) ); |
| } |
| |
| try |
| { |
| return new String( data, offset, length, charset ); |
| } |
| catch ( UnsupportedEncodingException e ) |
| { |
| return new String( data, offset, length ); |
| } |
| } |
| |
| |
| /** |
| * From commons-httpclients. Converts the byte array of HTTP content |
| * characters to a string. If the specified charset is not supported, |
| * default system encoding is used. |
| * |
| * @param data the byte array to be encoded |
| * @param charset the desired character encoding |
| * @return The result of the conversion. |
| * @since 3.0 |
| */ |
| public static String getString( final byte[] data, String charset ) |
| { |
| return getString( data, 0, data.length, charset ); |
| } |
| |
| |
| /** |
| * Converts the specified string to byte array of ASCII characters. |
| * |
| * @param data the string to be encoded |
| * @return The string as a byte array. |
| * @throws org.apache.directory.shared.ldap.model.exception.UrlDecoderException if encoding is not supported |
| */ |
| public static byte[] getAsciiBytes( final String data ) throws UrlDecoderException |
| { |
| |
| if ( data == null ) |
| { |
| throw new IllegalArgumentException( I18n.err( I18n.ERR_04411 ) ); |
| } |
| |
| try |
| { |
| return data.getBytes( "US-ASCII" ); |
| } |
| catch ( UnsupportedEncodingException e ) |
| { |
| throw new UrlDecoderException( I18n.err( I18n.ERR_04413 ) ); |
| } |
| } |
| |
| |
| /** |
| * From commons-codec. Decodes an array of URL safe 7-bit characters into an |
| * array of original bytes. Escaped characters are converted back to their |
| * original representation. |
| * |
| * @param bytes array of URL safe characters |
| * @return array of original bytes |
| * @throws UrlDecoderException Thrown if URL decoding is unsuccessful |
| */ |
| private static byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException |
| { |
| if ( bytes == null ) |
| { |
| return StringConstants.EMPTY_BYTES; |
| } |
| |
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
| |
| for ( int i = 0; i < bytes.length; i++ ) |
| { |
| int b = bytes[i]; |
| |
| if ( b == '%' ) |
| { |
| try |
| { |
| int u = Character.digit( ( char ) bytes[++i], 16 ); |
| int l = Character.digit( ( char ) bytes[++i], 16 ); |
| |
| if ( ( u == -1 ) || ( l == -1 ) ) |
| { |
| throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) ); |
| } |
| |
| buffer.write( ( char ) ( ( u << 4 ) + l ) ); |
| } |
| catch ( ArrayIndexOutOfBoundsException e ) |
| { |
| throw new UrlDecoderException( I18n.err( I18n.ERR_04414 ) ); |
| } |
| } |
| else |
| { |
| buffer.write( b ); |
| } |
| } |
| |
| return buffer.toByteArray(); |
| } |
| |
| |
| /** |
| * From commons-httpclients. Unescape and decode a given string regarded as |
| * an escaped string with the default protocol charset. |
| * |
| * @param escaped a string |
| * @return the unescaped string |
| * @throws LdapUriException if the string cannot be decoded (invalid) |
| */ |
| private static String decode( String escaped ) throws LdapUriException |
| { |
| try |
| { |
| byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) ); |
| return getString( rawdata, "UTF-8" ); |
| } |
| catch ( UrlDecoderException e ) |
| { |
| throw new LdapUriException( e.getMessage() ); |
| } |
| } |
| |
| |
| /** |
| * Parse a string and check that it complies with RFC 2253. Here, we will |
| * just call the Dn parser to do the job. |
| * |
| * @param chars The char array to be checked |
| * @param pos the starting position |
| * @return -1 if the char array does not contains a Dn |
| */ |
| private int parseDN( char[] chars, int pos ) |
| { |
| |
| int end = pos; |
| |
| for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) |
| { |
| end++; |
| } |
| |
| try |
| { |
| dn = new Dn( decode( new String( chars, pos, end - pos ) ) ); |
| } |
| catch ( LdapUriException ue ) |
| { |
| return -1; |
| } |
| catch ( LdapInvalidDnException de ) |
| { |
| return -1; |
| } |
| |
| return end; |
| } |
| |
| |
| /** |
| * Parse the attributes part |
| * |
| * @param chars The char array to be checked |
| * @param pos the starting position |
| * @return -1 if the char array does not contains attributes |
| */ |
| private int parseAttributes( char[] chars, int pos ) |
| { |
| |
| int start = pos; |
| int end = pos; |
| Set<String> hAttributes = new HashSet<String>(); |
| boolean hadComma = false; |
| |
| try |
| { |
| |
| for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) |
| { |
| |
| if ( Chars.isCharASCII(chars, i, ',') ) |
| { |
| hadComma = true; |
| |
| if ( ( end - start ) == 0 ) |
| { |
| |
| // An attributes must not be null |
| return -1; |
| } |
| else |
| { |
| String attribute = null; |
| |
| // get the attribute. It must not be blank |
| attribute = new String( chars, start, end - start ).trim(); |
| |
| if ( attribute.length() == 0 ) |
| { |
| return -1; |
| } |
| |
| String decodedAttr = decode( attribute ); |
| |
| if ( !hAttributes.contains( decodedAttr ) ) |
| { |
| attributes.add( decodedAttr ); |
| hAttributes.add( decodedAttr ); |
| } |
| } |
| |
| start = i + 1; |
| } |
| else |
| { |
| hadComma = false; |
| } |
| |
| end++; |
| } |
| |
| if ( hadComma ) |
| { |
| |
| // We are not allowed to have a comma at the end of the |
| // attributes |
| return -1; |
| } |
| else |
| { |
| |
| if ( end == start ) |
| { |
| |
| // We don't have any attributes. This is valid. |
| return end; |
| } |
| |
| // Store the last attribute |
| // get the attribute. It must not be blank |
| String attribute = null; |
| |
| attribute = new String( chars, start, end - start ).trim(); |
| |
| if ( attribute.length() == 0 ) |
| { |
| return -1; |
| } |
| |
| String decodedAttr = decode( attribute ); |
| |
| if ( !hAttributes.contains( decodedAttr ) ) |
| { |
| attributes.add( decodedAttr ); |
| hAttributes.add( decodedAttr ); |
| } |
| } |
| |
| return end; |
| } |
| catch ( LdapUriException ue ) |
| { |
| return -1; |
| } |
| } |
| |
| |
| /** |
| * Parse the filter part. We will use the FilterParserImpl class |
| * |
| * @param chars The char array to be checked |
| * @param pos the starting position |
| * @return -1 if the char array does not contains a filter |
| */ |
| private int parseFilter( char[] chars, int pos ) |
| { |
| |
| int end = pos; |
| |
| for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) |
| { |
| end++; |
| } |
| |
| if ( end == pos ) |
| { |
| // We have no filter |
| return end; |
| } |
| |
| try |
| { |
| filter = decode( new String( chars, pos, end - pos ) ); |
| FilterParser.parse( null, filter ); |
| } |
| catch ( LdapUriException ue ) |
| { |
| return -1; |
| } |
| catch ( ParseException pe ) |
| { |
| return -1; |
| } |
| |
| return end; |
| } |
| |
| |
| /** |
| * Parse the scope part. |
| * |
| * @param chars The char array to be checked |
| * @param pos the starting position |
| * @return -1 if the char array does not contains a scope |
| */ |
| private int parseScope( char[] chars, int pos ) |
| { |
| |
| if ( Chars.isCharASCII(chars, pos, 'b') || Chars.isCharASCII(chars, pos, 'B') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 'a') || Chars.isCharASCII(chars, pos, 'A') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 's') || Chars.isCharASCII(chars, pos, 'S') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 'e') || Chars.isCharASCII(chars, pos, 'E') ) |
| { |
| pos++; |
| scope = SearchScope.OBJECT; |
| return pos; |
| } |
| } |
| } |
| } |
| else if ( Chars.isCharASCII(chars, pos, 'o') || Chars.isCharASCII(chars, pos, 'O') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 'n') || Chars.isCharASCII(chars, pos, 'N') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 'e') || Chars.isCharASCII(chars, pos, 'E') ) |
| { |
| pos++; |
| |
| scope = SearchScope.ONELEVEL; |
| return pos; |
| } |
| } |
| } |
| else if ( Chars.isCharASCII(chars, pos, 's') || Chars.isCharASCII(chars, pos, 'S') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 'u') || Chars.isCharASCII(chars, pos, 'U') ) |
| { |
| pos++; |
| |
| if ( Chars.isCharASCII(chars, pos, 'b') || Chars.isCharASCII(chars, pos, 'B') ) |
| { |
| pos++; |
| |
| scope = SearchScope.SUBTREE; |
| return pos; |
| } |
| } |
| } |
| else if ( Chars.isCharASCII(chars, pos, '?') ) |
| { |
| // An empty scope. This is valid |
| return pos; |
| } |
| else if ( pos == chars.length ) |
| { |
| // An empty scope at the end of the URL. This is valid |
| return pos; |
| } |
| |
| // The scope is not one of "one", "sub" or "base". It's an error |
| return -1; |
| } |
| |
| |
| /** |
| * Parse extensions and critical extensions. |
| * |
| * The grammar is : |
| * extensions ::= extension [ ',' extension ]* |
| * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ] |
| * |
| * @param chars The char array to be checked |
| * @param pos the starting position |
| * @return -1 if the char array does not contains valid extensions or |
| * critical extensions |
| */ |
| private int parseExtensions( char[] chars, int pos ) |
| { |
| int start = pos; |
| boolean isCritical = false; |
| boolean isNewExtension = true; |
| boolean hasValue = false; |
| String extension = null; |
| String value = null; |
| |
| if ( pos == chars.length ) |
| { |
| return pos; |
| } |
| |
| try |
| { |
| for ( int i = pos; ( i < chars.length ); i++ ) |
| { |
| if ( Chars.isCharASCII(chars, i, ',') ) |
| { |
| if ( isNewExtension ) |
| { |
| // a ',' is not allowed when we have already had one |
| // or if we just started to parse the extensions. |
| return -1; |
| } |
| else |
| { |
| if ( extension == null ) |
| { |
| extension = decode( new String( chars, start, i - start ) ).trim(); |
| } |
| else |
| { |
| value = decode( new String( chars, start, i - start ) ).trim(); |
| } |
| |
| Extension ext = new Extension( isCritical, extension, value ); |
| extensionList.add( ext ); |
| |
| isNewExtension = true; |
| hasValue = false; |
| isCritical = false; |
| start = i + 1; |
| extension = null; |
| value = null; |
| } |
| } |
| else if ( Chars.isCharASCII(chars, i, '=') ) |
| { |
| if ( hasValue ) |
| { |
| // We may have two '=' for the same extension |
| continue; |
| } |
| |
| // An optionnal value |
| extension = decode( new String( chars, start, i - start ) ).trim(); |
| |
| if ( extension.length() == 0 ) |
| { |
| // We must have an extension |
| return -1; |
| } |
| |
| hasValue = true; |
| start = i + 1; |
| } |
| else if ( Chars.isCharASCII(chars, i, '!') ) |
| { |
| if ( hasValue ) |
| { |
| // We may have two '!' in the value |
| continue; |
| } |
| |
| if ( !isNewExtension ) |
| { |
| // '!' must appears first |
| return -1; |
| } |
| |
| isCritical = true; |
| start++; |
| } |
| else |
| { |
| isNewExtension = false; |
| } |
| } |
| |
| if ( extension == null ) |
| { |
| extension = decode( new String( chars, start, chars.length - start ) ).trim(); |
| } |
| else |
| { |
| value = decode( new String( chars, start, chars.length - start ) ).trim(); |
| } |
| |
| Extension ext = new Extension( isCritical, extension, value ); |
| extensionList.add( ext ); |
| |
| return chars.length; |
| } |
| catch ( LdapUriException ue ) |
| { |
| return -1; |
| } |
| } |
| |
| |
| /** |
| * Encode a String to avoid special characters. |
| * |
| * |
| * RFC 4516, section 2.1. (Percent-Encoding) |
| * |
| * A generated LDAP URL MUST consist only of the restricted set of |
| * characters included in one of the following three productions defined |
| * in [RFC3986]: |
| * |
| * <reserved> |
| * <unreserved> |
| * <pct-encoded> |
| * |
| * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as |
| * input. An octet MUST be encoded using the percent-encoding mechanism |
| * described in section 2.1 of [RFC3986] in any of these situations: |
| * |
| * The octet is not in the reserved set defined in section 2.2 of |
| * [RFC3986] or in the unreserved set defined in section 2.3 of |
| * [RFC3986]. |
| * |
| * It is the single Reserved character '?' and occurs inside a <dn>, |
| * <filter>, or other element of an LDAP URL. |
| * |
| * It is a comma character ',' that occurs inside an <exvalue>. |
| * |
| * |
| * RFC 3986, section 2.2 (Reserved Characters) |
| * |
| * reserved = gen-delims / sub-delims |
| * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" |
| * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
| * / "*" / "+" / "," / ";" / "=" |
| * |
| * |
| * RFC 3986, section 2.3 (Unreserved Characters) |
| * |
| * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
| * |
| * |
| * @param url The String to encode |
| * @param doubleEncode Set if we need to encode the comma |
| * @return An encoded string |
| */ |
| public static String urlEncode( String url, boolean doubleEncode ) |
| { |
| StringBuffer sb = new StringBuffer(); |
| |
| for ( int i = 0; i < url.length(); i++ ) |
| { |
| char c = url.charAt( i ); |
| |
| switch ( c ) |
| |
| { |
| // reserved and unreserved characters: |
| // just append to the buffer |
| |
| // reserved gen-delims, excluding '?' |
| // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" |
| case ':': |
| case '/': |
| case '#': |
| case '[': |
| case ']': |
| case '@': |
| |
| // reserved sub-delims, excluding ',' |
| // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
| // / "*" / "+" / "," / ";" / "=" |
| case '!': |
| case '$': |
| case '&': |
| case '\'': |
| case '(': |
| case ')': |
| case '*': |
| case '+': |
| case ';': |
| case '=': |
| |
| // unreserved |
| // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
| case 'a': |
| case 'b': |
| case 'c': |
| case 'd': |
| case 'e': |
| case 'f': |
| case 'g': |
| case 'h': |
| case 'i': |
| case 'j': |
| case 'k': |
| case 'l': |
| case 'm': |
| case 'n': |
| case 'o': |
| case 'p': |
| case 'q': |
| case 'r': |
| case 's': |
| case 't': |
| case 'u': |
| case 'v': |
| case 'w': |
| case 'x': |
| case 'y': |
| case 'z': |
| |
| case 'A': |
| case 'B': |
| case 'C': |
| case 'D': |
| case 'E': |
| case 'F': |
| case 'G': |
| case 'H': |
| case 'I': |
| case 'J': |
| case 'K': |
| case 'L': |
| case 'M': |
| case 'N': |
| case 'O': |
| case 'P': |
| case 'Q': |
| case 'R': |
| case 'S': |
| case 'T': |
| case 'U': |
| case 'V': |
| case 'W': |
| case 'X': |
| case 'Y': |
| case 'Z': |
| |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| |
| case '-': |
| case '.': |
| case '_': |
| case '~': |
| |
| sb.append( c ); |
| break; |
| |
| case ',': |
| |
| // special case for comma |
| if ( doubleEncode ) |
| { |
| sb.append( "%2c" ); |
| } |
| else |
| { |
| sb.append( c ); |
| } |
| break; |
| |
| default: |
| |
| // percent encoding |
| byte[] bytes = Unicode.charToBytes(c); |
| char[] hex = Strings.toHexString( bytes ).toCharArray(); |
| for ( int j = 0; j < hex.length; j++ ) |
| { |
| if ( j % 2 == 0 ) |
| { |
| sb.append( '%' ); |
| } |
| sb.append( hex[j] ); |
| |
| } |
| |
| break; |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| |
| /** |
| * Get a string representation of a LdapURL. |
| * |
| * @return A LdapURL string |
| * @see LdapURL#forceScopeRendering |
| */ |
| @Override |
| public String toString() |
| { |
| StringBuffer sb = new StringBuffer(); |
| |
| sb.append( scheme ); |
| |
| sb.append( ( host == null ) ? "" : host ); |
| |
| if ( port != -1 ) |
| { |
| sb.append( ':' ).append( port ); |
| } |
| |
| if ( dn != null ) |
| { |
| sb.append( '/' ).append( urlEncode( dn.getName(), false ) ); |
| |
| if ( ( attributes.size() != 0 ) || forceScopeRendering |
| || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) ) |
| { |
| sb.append( '?' ); |
| |
| boolean isFirst = true; |
| |
| for ( String attribute : attributes ) |
| { |
| if ( isFirst ) |
| { |
| isFirst = false; |
| } |
| else |
| { |
| sb.append( ',' ); |
| } |
| |
| sb.append( urlEncode( attribute, false ) ); |
| } |
| } |
| |
| if ( forceScopeRendering ) |
| { |
| sb.append( '?' ); |
| |
| sb.append( scope.getLdapUrlValue() ); |
| } |
| |
| else |
| { |
| if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || ( extensionList.size() != 0 ) ) |
| { |
| sb.append( '?' ); |
| |
| switch ( scope ) |
| { |
| case ONELEVEL: |
| case SUBTREE: |
| sb.append( scope.getLdapUrlValue() ); |
| break; |
| |
| default: |
| break; |
| } |
| |
| if ( ( filter != null ) || ( ( extensionList.size() != 0 ) ) ) |
| { |
| sb.append( "?" ); |
| |
| if ( filter != null ) |
| { |
| sb.append( urlEncode( filter, false ) ); |
| } |
| |
| if ( ( extensionList.size() != 0 ) ) |
| { |
| sb.append( '?' ); |
| |
| boolean isFirst = true; |
| |
| if ( extensionList.size() != 0 ) |
| { |
| for ( Extension extension : extensionList ) |
| { |
| if ( !isFirst ) |
| { |
| sb.append( ',' ); |
| } |
| else |
| { |
| isFirst = false; |
| } |
| |
| if ( extension.isCritical ) |
| { |
| sb.append( '!' ); |
| } |
| sb.append( urlEncode( extension.type, false ) ); |
| |
| if ( extension.value != null ) |
| { |
| sb.append( '=' ); |
| sb.append( urlEncode( extension.value, true ) ); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| else |
| { |
| sb.append( '/' ); |
| } |
| |
| return sb.toString(); |
| } |
| |
| |
| /** |
| * @return Returns the attributes. |
| */ |
| public List<String> getAttributes() |
| { |
| return attributes; |
| } |
| |
| |
| /** |
| * @return Returns the dn. |
| */ |
| public Dn getDn() |
| { |
| return dn; |
| } |
| |
| |
| /** |
| * @return Returns the extensions. |
| */ |
| public List<Extension> getExtensions() |
| { |
| return extensionList; |
| } |
| |
| |
| /** |
| * Gets the extension. |
| * |
| * @param type the extension type, case-insensitive |
| * |
| * @return Returns the extension, null if this URL does not contain |
| * such an extension. |
| */ |
| public Extension getExtension( String type ) |
| { |
| for ( Extension extension : getExtensions() ) |
| { |
| if ( extension.getType().equalsIgnoreCase( type ) ) |
| { |
| return extension; |
| } |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Gets the extension value. |
| * |
| * @param type the extension type, case-insensitive |
| * |
| * @return Returns the extension value, null if this URL does not |
| * contain such an extension or if the extension value is null. |
| */ |
| public String getExtensionValue( String type ) |
| { |
| for ( Extension extension : getExtensions() ) |
| { |
| if ( extension.getType().equalsIgnoreCase( type ) ) |
| { |
| return extension.getValue(); |
| } |
| } |
| return null; |
| } |
| |
| |
| /** |
| * @return Returns the filter. |
| */ |
| public String getFilter() |
| { |
| return filter; |
| } |
| |
| |
| /** |
| * @return Returns the host. |
| */ |
| public String getHost() |
| { |
| return host; |
| } |
| |
| |
| /** |
| * @return Returns the port. |
| */ |
| public int getPort() |
| { |
| return port; |
| } |
| |
| |
| /** |
| * Returns the scope, one of {@link SearchScope#OBJECT}, |
| * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}. |
| * |
| * @return Returns the scope. |
| */ |
| public SearchScope getScope() |
| { |
| return scope; |
| } |
| |
| |
| /** |
| * @return Returns the scheme. |
| */ |
| public String getScheme() |
| { |
| return scheme; |
| } |
| |
| |
| /** |
| * @return the number of bytes for this LdapURL |
| */ |
| public int getNbBytes() |
| { |
| return ( bytes != null ? bytes.length : 0 ); |
| } |
| |
| |
| /** |
| * @return a reference on the interned bytes representing this LdapURL |
| */ |
| public byte[] getBytesReference() |
| { |
| return bytes; |
| } |
| |
| |
| /** |
| * @return a copy of the bytes representing this LdapURL |
| */ |
| public byte[] getBytesCopy() |
| { |
| if ( bytes != null ) |
| { |
| byte[] copy = new byte[bytes.length]; |
| System.arraycopy( bytes, 0, copy, 0, bytes.length ); |
| return copy; |
| } |
| else |
| { |
| return null; |
| } |
| } |
| |
| |
| /** |
| * @return the LdapURL as a String |
| */ |
| public String getString() |
| { |
| return string; |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int hashCode() |
| { |
| return this.toString().hashCode(); |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean equals( Object obj ) |
| { |
| if ( this == obj ) |
| { |
| return true; |
| } |
| if ( obj == null ) |
| { |
| return false; |
| } |
| if ( getClass() != obj.getClass() ) |
| { |
| return false; |
| } |
| |
| final LdapURL other = ( LdapURL ) obj; |
| return this.toString().equals( other.toString() ); |
| } |
| |
| |
| /** |
| * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default. |
| * |
| * @param scheme the new scheme |
| */ |
| public void setScheme( String scheme ) |
| { |
| if ( ( ( scheme != null ) && LDAP_SCHEME.equals( scheme ) ) || LDAPS_SCHEME.equals( scheme ) ) |
| { |
| this.scheme = scheme; |
| } |
| else |
| { |
| this.scheme = LDAP_SCHEME; |
| } |
| |
| } |
| |
| |
| /** |
| * Sets the host. |
| * |
| * @param host the new host |
| */ |
| public void setHost( String host ) |
| { |
| this.host = host; |
| } |
| |
| |
| /** |
| * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default. |
| * |
| * @param port the new port |
| */ |
| public void setPort( int port ) |
| { |
| if ( ( port < 1 ) || ( port > 65535 ) ) |
| { |
| this.port = -1; |
| } |
| else |
| { |
| this.port = port; |
| } |
| } |
| |
| |
| /** |
| * Sets the dn. |
| * |
| * @param dn the new dn |
| */ |
| public void setDn( Dn dn ) |
| { |
| this.dn = dn; |
| } |
| |
| |
| /** |
| * Sets the attributes, null removes all existing attributes. |
| * |
| * @param attributes the new attributes |
| */ |
| public void setAttributes( List<String> attributes ) |
| { |
| if ( attributes == null ) |
| { |
| this.attributes.clear(); |
| } |
| else |
| { |
| this.attributes = attributes; |
| } |
| } |
| |
| |
| /** |
| * Sets the scope. Must be one of {@link SearchScope#OBJECT}, |
| * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}, |
| * otherwise {@link SearchScope#OBJECT} is assumed as default. |
| * |
| * @param scope the new scope |
| */ |
| public void setScope( int scope ) |
| { |
| try |
| { |
| this.scope = SearchScope.getSearchScope( scope ); |
| } |
| catch ( IllegalArgumentException iae ) |
| { |
| this.scope = SearchScope.OBJECT; |
| } |
| } |
| |
| |
| /** |
| * Sets the scope. Must be one of {@link SearchScope#OBJECT}, |
| * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}, |
| * otherwise {@link SearchScope#OBJECT} is assumed as default. |
| * |
| * @param scope the new scope |
| */ |
| public void setScope( SearchScope scope ) |
| { |
| if ( scope == null ) |
| { |
| this.scope = SearchScope.OBJECT; |
| } |
| else |
| { |
| this.scope = scope; |
| } |
| } |
| |
| |
| /** |
| * Sets the filter. |
| * |
| * @param filter the new filter |
| */ |
| public void setFilter( String filter ) |
| { |
| this.filter = filter; |
| } |
| |
| |
| /** |
| * If set to true forces the toString method to render the scope |
| * regardless of optional nature. Use this when you want explicit |
| * search URL scope rendering. |
| * |
| * @param forceScopeRendering the forceScopeRendering to set |
| */ |
| public void setForceScopeRendering( boolean forceScopeRendering ) |
| { |
| this.forceScopeRendering = forceScopeRendering; |
| } |
| |
| |
| /** |
| * If set to true forces the toString method to render the scope |
| * regardless of optional nature. Use this when you want explicit |
| * search URL scope rendering. |
| * |
| * @return the forceScopeRendering |
| */ |
| public boolean isForceScopeRendering() |
| { |
| return forceScopeRendering; |
| } |
| |
| /** |
| * An inner bean to hold extension information. |
| * |
| * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> |
| */ |
| public static class Extension |
| { |
| private boolean isCritical; |
| private String type; |
| private String value; |
| |
| |
| /** |
| * Creates a new instance of Extension. |
| * |
| * @param isCritical true for critical extension |
| * @param type the extension type |
| * @param value the extension value |
| */ |
| public Extension( boolean isCritical, String type, String value ) |
| { |
| super(); |
| this.isCritical = isCritical; |
| this.type = type; |
| this.value = value; |
| } |
| |
| |
| /** |
| * Checks if is critical. |
| * |
| * @return true, if is critical |
| */ |
| public boolean isCritical() |
| { |
| return isCritical; |
| } |
| |
| |
| /** |
| * Sets the critical flag. |
| * |
| * @param critical the new critical flag |
| */ |
| public void setCritical( boolean critical ) |
| { |
| this.isCritical = critical; |
| } |
| |
| |
| /** |
| * Gets the type. |
| * |
| * @return the type |
| */ |
| public String getType() |
| { |
| return type; |
| } |
| |
| |
| /** |
| * Sets the type. |
| * |
| * @param type the new type |
| */ |
| public void setType( String type ) |
| { |
| this.type = type; |
| } |
| |
| |
| /** |
| * Gets the value. |
| * |
| * @return the value |
| */ |
| public String getValue() |
| { |
| return value; |
| } |
| |
| |
| /** |
| * Sets the value. |
| * |
| * @param value the new value |
| */ |
| public void setValue( String value ) |
| { |
| this.value = value; |
| } |
| } |
| |
| } |