| /* |
| * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package com.sun.jndi.toolkit.dir; |
| |
| import javax.naming.*; |
| import javax.naming.directory.*; |
| import java.util.Enumeration; |
| import java.util.StringTokenizer; |
| import java.util.Vector; |
| import java.util.Locale; |
| |
| /** |
| * A class for parsing LDAP search filters (defined in RFC 1960, 2254) |
| * |
| * @author Jon Ruiz |
| * @author Rosanna Lee |
| */ |
| public class SearchFilter implements AttrFilter { |
| |
| interface StringFilter extends AttrFilter { |
| public void parse() throws InvalidSearchFilterException; |
| } |
| |
| // %%% "filter" and "pos" are not declared "private" due to bug 4064984. |
| String filter; |
| int pos; |
| private StringFilter rootFilter; |
| |
| protected static final boolean debug = false; |
| |
| protected static final char BEGIN_FILTER_TOKEN = '('; |
| protected static final char END_FILTER_TOKEN = ')'; |
| protected static final char AND_TOKEN = '&'; |
| protected static final char OR_TOKEN = '|'; |
| protected static final char NOT_TOKEN = '!'; |
| protected static final char EQUAL_TOKEN = '='; |
| protected static final char APPROX_TOKEN = '~'; |
| protected static final char LESS_TOKEN = '<'; |
| protected static final char GREATER_TOKEN = '>'; |
| protected static final char EXTEND_TOKEN = ':'; |
| protected static final char WILDCARD_TOKEN = '*'; |
| |
| public SearchFilter(String filter) throws InvalidSearchFilterException { |
| this.filter = filter; |
| pos = 0; |
| normalizeFilter(); |
| rootFilter = this.createNextFilter(); |
| } |
| |
| // Returns true if targetAttrs passes the filter |
| public boolean check(Attributes targetAttrs) throws NamingException { |
| if (targetAttrs == null) |
| return false; |
| |
| return rootFilter.check(targetAttrs); |
| } |
| |
| /* |
| * Utility routines used by member classes |
| */ |
| |
| // does some pre-processing on the string to make it look exactly lik |
| // what the parser expects. This only needs to be called once. |
| protected void normalizeFilter() { |
| skipWhiteSpace(); // get rid of any leading whitespaces |
| |
| // Sometimes, search filters don't have "(" and ")" - add them |
| if(getCurrentChar() != BEGIN_FILTER_TOKEN) { |
| filter = BEGIN_FILTER_TOKEN + filter + END_FILTER_TOKEN; |
| } |
| // this would be a good place to strip whitespace if desired |
| |
| if(debug) {System.out.println("SearchFilter: normalized filter:" + |
| filter);} |
| } |
| |
| private void skipWhiteSpace() { |
| while (Character.isWhitespace(getCurrentChar())) { |
| consumeChar(); |
| } |
| } |
| |
| protected StringFilter createNextFilter() |
| throws InvalidSearchFilterException { |
| StringFilter filter; |
| |
| skipWhiteSpace(); |
| |
| try { |
| // make sure every filter starts with "(" |
| if(getCurrentChar() != BEGIN_FILTER_TOKEN) { |
| throw new InvalidSearchFilterException("expected \"" + |
| BEGIN_FILTER_TOKEN + |
| "\" at position " + |
| pos); |
| } |
| |
| // skip past the "(" |
| this.consumeChar(); |
| |
| skipWhiteSpace(); |
| |
| // use the next character to determine the type of filter |
| switch(getCurrentChar()) { |
| case AND_TOKEN: |
| if (debug) {System.out.println("SearchFilter: creating AND");} |
| filter = new CompoundFilter(true); |
| filter.parse(); |
| break; |
| case OR_TOKEN: |
| if (debug) {System.out.println("SearchFilter: creating OR");} |
| filter = new CompoundFilter(false); |
| filter.parse(); |
| break; |
| case NOT_TOKEN: |
| if (debug) {System.out.println("SearchFilter: creating OR");} |
| filter = new NotFilter(); |
| filter.parse(); |
| break; |
| default: |
| if (debug) {System.out.println("SearchFilter: creating SIMPLE");} |
| filter = new AtomicFilter(); |
| filter.parse(); |
| break; |
| } |
| |
| skipWhiteSpace(); |
| |
| // make sure every filter ends with ")" |
| if(getCurrentChar() != END_FILTER_TOKEN) { |
| throw new InvalidSearchFilterException("expected \"" + |
| END_FILTER_TOKEN + |
| "\" at position " + |
| pos); |
| } |
| |
| // skip past the ")" |
| this.consumeChar(); |
| } catch (InvalidSearchFilterException e) { |
| if (debug) {System.out.println("rethrowing e");} |
| throw e; // just rethrow these |
| |
| // catch all - any uncaught exception while parsing will end up here |
| } catch (Exception e) { |
| if(debug) {System.out.println(e.getMessage());e.printStackTrace();} |
| throw new InvalidSearchFilterException("Unable to parse " + |
| "character " + pos + " in \""+ |
| this.filter + "\""); |
| } |
| |
| return filter; |
| } |
| |
| protected char getCurrentChar() { |
| return filter.charAt(pos); |
| } |
| |
| protected char relCharAt(int i) { |
| return filter.charAt(pos + i); |
| } |
| |
| protected void consumeChar() { |
| pos++; |
| } |
| |
| protected void consumeChars(int i) { |
| pos += i; |
| } |
| |
| protected int relIndexOf(int ch) { |
| return filter.indexOf(ch, pos) - pos; |
| } |
| |
| protected String relSubstring(int beginIndex, int endIndex){ |
| if(debug){System.out.println("relSubString: " + beginIndex + |
| " " + endIndex);} |
| return filter.substring(beginIndex+pos, endIndex+pos); |
| } |
| |
| |
| /** |
| * A class for dealing with compound filters ("and" & "or" filters). |
| */ |
| final class CompoundFilter implements StringFilter { |
| private Vector<StringFilter> subFilters; |
| private boolean polarity; |
| |
| CompoundFilter(boolean polarity) { |
| subFilters = new Vector<>(); |
| this.polarity = polarity; |
| } |
| |
| public void parse() throws InvalidSearchFilterException { |
| SearchFilter.this.consumeChar(); // consume the "&" |
| while(SearchFilter.this.getCurrentChar() != END_FILTER_TOKEN) { |
| if (debug) {System.out.println("CompoundFilter: adding");} |
| StringFilter filter = SearchFilter.this.createNextFilter(); |
| subFilters.addElement(filter); |
| skipWhiteSpace(); |
| } |
| } |
| |
| public boolean check(Attributes targetAttrs) throws NamingException { |
| for(int i = 0; i<subFilters.size(); i++) { |
| StringFilter filter = subFilters.elementAt(i); |
| if(filter.check(targetAttrs) != this.polarity) { |
| return !polarity; |
| } |
| } |
| return polarity; |
| } |
| } /* CompoundFilter */ |
| |
| /** |
| * A class for dealing with NOT filters |
| */ |
| final class NotFilter implements StringFilter { |
| private StringFilter filter; |
| |
| public void parse() throws InvalidSearchFilterException { |
| SearchFilter.this.consumeChar(); // consume the "!" |
| filter = SearchFilter.this.createNextFilter(); |
| } |
| |
| public boolean check(Attributes targetAttrs) throws NamingException { |
| return !filter.check(targetAttrs); |
| } |
| } /* notFilter */ |
| |
| // note: declared here since member classes can't have static variables |
| static final int EQUAL_MATCH = 1; |
| static final int APPROX_MATCH = 2; |
| static final int GREATER_MATCH = 3; |
| static final int LESS_MATCH = 4; |
| |
| /** |
| * A class for dealing with atomic filters |
| */ |
| final class AtomicFilter implements StringFilter { |
| private String attrID; |
| private String value; |
| private int matchType; |
| |
| public void parse() throws InvalidSearchFilterException { |
| |
| skipWhiteSpace(); |
| |
| try { |
| // find the end |
| int endPos = SearchFilter.this.relIndexOf(END_FILTER_TOKEN); |
| |
| //determine the match type |
| int i = SearchFilter.this.relIndexOf(EQUAL_TOKEN); |
| if(debug) {System.out.println("AtomicFilter: = at " + i);} |
| int qualifier = SearchFilter.this.relCharAt(i-1); |
| switch(qualifier) { |
| case APPROX_TOKEN: |
| if (debug) {System.out.println("Atomic: APPROX found");} |
| matchType = APPROX_MATCH; |
| attrID = SearchFilter.this.relSubstring(0, i-1); |
| value = SearchFilter.this.relSubstring(i+1, endPos); |
| break; |
| |
| case GREATER_TOKEN: |
| if (debug) {System.out.println("Atomic: GREATER found");} |
| matchType = GREATER_MATCH; |
| attrID = SearchFilter.this.relSubstring(0, i-1); |
| value = SearchFilter.this.relSubstring(i+1, endPos); |
| break; |
| |
| case LESS_TOKEN: |
| if (debug) {System.out.println("Atomic: LESS found");} |
| matchType = LESS_MATCH; |
| attrID = SearchFilter.this.relSubstring(0, i-1); |
| value = SearchFilter.this.relSubstring(i+1, endPos); |
| break; |
| |
| case EXTEND_TOKEN: |
| if(debug) {System.out.println("Atomic: EXTEND found");} |
| throw new OperationNotSupportedException("Extensible match not supported"); |
| |
| default: |
| if (debug) {System.out.println("Atomic: EQUAL found");} |
| matchType = EQUAL_MATCH; |
| attrID = SearchFilter.this.relSubstring(0,i); |
| value = SearchFilter.this.relSubstring(i+1, endPos); |
| break; |
| } |
| |
| attrID = attrID.trim(); |
| value = value.trim(); |
| |
| //update our position |
| SearchFilter.this.consumeChars(endPos); |
| |
| } catch (Exception e) { |
| if (debug) {System.out.println(e.getMessage()); |
| e.printStackTrace();} |
| InvalidSearchFilterException sfe = |
| new InvalidSearchFilterException("Unable to parse " + |
| "character " + SearchFilter.this.pos + " in \""+ |
| SearchFilter.this.filter + "\""); |
| sfe.setRootCause(e); |
| throw(sfe); |
| } |
| |
| if(debug) {System.out.println("AtomicFilter: " + attrID + "=" + |
| value);} |
| } |
| |
| public boolean check(Attributes targetAttrs) { |
| Enumeration<?> candidates; |
| |
| try { |
| Attribute attr = targetAttrs.get(attrID); |
| if(attr == null) { |
| return false; |
| } |
| candidates = attr.getAll(); |
| } catch (NamingException ne) { |
| if (debug) {System.out.println("AtomicFilter: should never " + |
| "here");} |
| return false; |
| } |
| |
| while(candidates.hasMoreElements()) { |
| String val = candidates.nextElement().toString(); |
| if (debug) {System.out.println("Atomic: comparing: " + val);} |
| switch(matchType) { |
| case APPROX_MATCH: |
| case EQUAL_MATCH: |
| if(substringMatch(this.value, val)) { |
| if (debug) {System.out.println("Atomic: EQUAL match");} |
| return true; |
| } |
| break; |
| case GREATER_MATCH: |
| if (debug) {System.out.println("Atomic: GREATER match");} |
| if(val.compareTo(this.value) >= 0) { |
| return true; |
| } |
| break; |
| case LESS_MATCH: |
| if (debug) {System.out.println("Atomic: LESS match");} |
| if(val.compareTo(this.value) <= 0) { |
| return true; |
| } |
| break; |
| default: |
| if (debug) {System.out.println("AtomicFilter: unknown " + |
| "matchType");} |
| } |
| } |
| return false; |
| } |
| |
| // used for substring comparisons (where proto has "*" wildcards |
| private boolean substringMatch(String proto, String value) { |
| // simple case 1: "*" means attribute presence is being tested |
| if(proto.equals(Character.toString(WILDCARD_TOKEN))) { |
| if(debug) {System.out.println("simple presence assertion");} |
| return true; |
| } |
| |
| // simple case 2: if there are no wildcards, call String.equals() |
| if(proto.indexOf(WILDCARD_TOKEN) == -1) { |
| return proto.equalsIgnoreCase(value); |
| } |
| |
| if(debug) {System.out.println("doing substring comparison");} |
| // do the work: make sure all the substrings are present |
| int currentPos = 0; |
| StringTokenizer subStrs = new StringTokenizer(proto, "*", false); |
| |
| // do we need to begin with the first token? |
| if(proto.charAt(0) != WILDCARD_TOKEN && |
| !value.toLowerCase(Locale.ENGLISH).startsWith( |
| subStrs.nextToken().toLowerCase(Locale.ENGLISH))) { |
| if(debug) { |
| System.out.println("faild initial test"); |
| } |
| return false; |
| } |
| |
| while(subStrs.hasMoreTokens()) { |
| String currentStr = subStrs.nextToken(); |
| if (debug) {System.out.println("looking for \"" + |
| currentStr +"\"");} |
| currentPos = value.toLowerCase(Locale.ENGLISH).indexOf( |
| currentStr.toLowerCase(Locale.ENGLISH), currentPos); |
| |
| if(currentPos == -1) { |
| return false; |
| } |
| currentPos += currentStr.length(); |
| } |
| |
| // do we need to end with the last token? |
| if(proto.charAt(proto.length() - 1) != WILDCARD_TOKEN && |
| currentPos != value.length() ) { |
| if(debug) {System.out.println("faild final test");} |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } /* AtomicFilter */ |
| |
| // ----- static methods for producing string filters given attribute set |
| // ----- or object array |
| |
| |
| /** |
| * Creates an LDAP filter as a conjunction of the attributes supplied. |
| */ |
| public static String format(Attributes attrs) throws NamingException { |
| if (attrs == null || attrs.size() == 0) { |
| return "objectClass=*"; |
| } |
| |
| String answer; |
| answer = "(& "; |
| Attribute attr; |
| for (NamingEnumeration<? extends Attribute> e = attrs.getAll(); |
| e.hasMore(); ) { |
| attr = e.next(); |
| if (attr.size() == 0 || (attr.size() == 1 && attr.get() == null)) { |
| // only checking presence of attribute |
| answer += "(" + attr.getID() + "=" + "*)"; |
| } else { |
| for (NamingEnumeration<?> ve = attr.getAll(); |
| ve.hasMore(); ) { |
| String val = getEncodedStringRep(ve.next()); |
| if (val != null) { |
| answer += "(" + attr.getID() + "=" + val + ")"; |
| } |
| } |
| } |
| } |
| |
| answer += ")"; |
| //System.out.println("filter: " + answer); |
| return answer; |
| } |
| |
| // Writes the hex representation of a byte to a StringBuffer. |
| private static void hexDigit(StringBuffer buf, byte x) { |
| char c; |
| |
| c = (char) ((x >> 4) & 0xf); |
| if (c > 9) |
| c = (char) ((c-10) + 'A'); |
| else |
| c = (char)(c + '0'); |
| |
| buf.append(c); |
| c = (char) (x & 0xf); |
| if (c > 9) |
| c = (char)((c-10) + 'A'); |
| else |
| c = (char)(c + '0'); |
| buf.append(c); |
| } |
| |
| |
| /** |
| * Returns the string representation of an object (such as an attr value). |
| * If obj is a byte array, encode each item as \xx, where xx is hex encoding |
| * of the byte value. |
| * Else, if obj is not a String, use its string representation (toString()). |
| * Special characters in obj (or its string representation) are then |
| * encoded appropriately according to RFC 2254. |
| * * \2a |
| * ( \28 |
| * ) \29 |
| * \ \5c |
| * NUL \00 |
| */ |
| private static String getEncodedStringRep(Object obj) throws NamingException { |
| String str; |
| if (obj == null) |
| return null; |
| |
| if (obj instanceof byte[]) { |
| // binary data must be encoded as \hh where hh is a hex char |
| byte[] bytes = (byte[])obj; |
| StringBuffer b1 = new StringBuffer(bytes.length*3); |
| for (int i = 0; i < bytes.length; i++) { |
| b1.append('\\'); |
| hexDigit(b1, bytes[i]); |
| } |
| return b1.toString(); |
| } |
| if (!(obj instanceof String)) { |
| str = obj.toString(); |
| } else { |
| str = (String)obj; |
| } |
| int len = str.length(); |
| StringBuilder sb = new StringBuilder(len); |
| char ch; |
| for (int i = 0; i < len; i++) { |
| switch (ch=str.charAt(i)) { |
| case '*': |
| sb.append("\\2a"); |
| break; |
| case '(': |
| sb.append("\\28"); |
| break; |
| case ')': |
| sb.append("\\29"); |
| break; |
| case '\\': |
| sb.append("\\5c"); |
| break; |
| case 0: |
| sb.append("\\00"); |
| break; |
| default: |
| sb.append(ch); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| |
| /** |
| * Finds the first occurrence of {@code ch} in {@code val} starting |
| * from position {@code start}. It doesn't count if {@code ch} |
| * has been escaped by a backslash (\) |
| */ |
| public static int findUnescaped(char ch, String val, int start) { |
| int len = val.length(); |
| |
| while (start < len) { |
| int where = val.indexOf(ch, start); |
| // if at start of string, or not there at all, or if not escaped |
| if (where == start || where == -1 || val.charAt(where-1) != '\\') |
| return where; |
| |
| // start search after escaped star |
| start = where + 1; |
| } |
| return -1; |
| } |
| |
| /** |
| * Formats the expression {@code expr} using arguments from the array |
| * {@code args}. |
| * |
| * <code>{i}</code> specifies the <code>i</code>'th element from |
| * the array <code>args</code> is to be substituted for the |
| * string "<code>{i}</code>". |
| * |
| * To escape '{' or '}' (or any other character), use '\'. |
| * |
| * Uses getEncodedStringRep() to do encoding. |
| */ |
| |
| public static String format(String expr, Object[] args) |
| throws NamingException { |
| |
| int param; |
| int where = 0, start = 0; |
| StringBuilder answer = new StringBuilder(expr.length()); |
| |
| while ((where = findUnescaped('{', expr, start)) >= 0) { |
| int pstart = where + 1; // skip '{' |
| int pend = expr.indexOf('}', pstart); |
| |
| if (pend < 0) { |
| throw new InvalidSearchFilterException("unbalanced {: " + expr); |
| } |
| |
| // at this point, pend should be pointing at '}' |
| try { |
| param = Integer.parseInt(expr.substring(pstart, pend)); |
| } catch (NumberFormatException e) { |
| throw new InvalidSearchFilterException( |
| "integer expected inside {}: " + expr); |
| } |
| |
| if (param >= args.length) { |
| throw new InvalidSearchFilterException( |
| "number exceeds argument list: " + param); |
| } |
| |
| answer.append(expr.substring(start, where)).append(getEncodedStringRep(args[param])); |
| start = pend + 1; // skip '}' |
| } |
| |
| if (start < expr.length()) |
| answer.append(expr.substring(start)); |
| |
| return answer.toString(); |
| } |
| |
| /* |
| * returns an Attributes instance containing only attributeIDs given in |
| * "attributeIDs" whose values come from the given DSContext. |
| */ |
| public static Attributes selectAttributes(Attributes originals, |
| String[] attrIDs) throws NamingException { |
| |
| if (attrIDs == null) |
| return originals; |
| |
| Attributes result = new BasicAttributes(); |
| |
| for(int i=0; i<attrIDs.length; i++) { |
| Attribute attr = originals.get(attrIDs[i]); |
| if(attr != null) { |
| result.put(attr); |
| } |
| } |
| |
| return result; |
| } |
| |
| /* For testing filter |
| public static void main(String[] args) { |
| |
| Attributes attrs = new BasicAttributes(LdapClient.caseIgnore); |
| attrs.put("cn", "Rosanna Lee"); |
| attrs.put("sn", "Lee"); |
| attrs.put("fn", "Rosanna"); |
| attrs.put("id", "10414"); |
| attrs.put("machine", "jurassic"); |
| |
| |
| try { |
| System.out.println(format(attrs)); |
| |
| String expr = "(&(Age = {0})(Account Balance <= {1}))"; |
| Object[] fargs = new Object[2]; |
| // fill in the parameters |
| fargs[0] = new Integer(65); |
| fargs[1] = new Float(5000); |
| |
| System.out.println(format(expr, fargs)); |
| |
| |
| System.out.println(format("bin={0}", |
| new Object[] {new byte[] {0, 1, 2, 3, 4, 5}})); |
| |
| System.out.println(format("bin=\\{anything}", null)); |
| |
| } catch (NamingException e) { |
| e.printStackTrace(); |
| } |
| } |
| */ |
| |
| } |