/* * Copyright 1999 Sun Microsystems, Inc. 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. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.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; /** * 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 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 = (StringFilter)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 wtih 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: unkown " + "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(new Character(WILDCARD_TOKEN).toString())) { 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.toString().toLowerCase().startsWith( subStrs.nextToken().toLowerCase())) { 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().indexOf( currentStr.toLowerCase(), 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 conjuction 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 e = attrs.getAll(); e.hasMore(); ) { attr = (Attribute)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(); StringBuffer buf = new StringBuffer(len); char ch; for (int i = 0; i < len; i++) { switch (ch=str.charAt(i)) { case '*': buf.append("\\2a"); break; case '(': buf.append("\\28"); break; case ')': buf.append("\\29"); break; case '\\': buf.append("\\5c"); break; case 0: buf.append("\\00"); break; default: buf.append(ch); } } return buf.toString(); } /** * Finds the first occurrence of <tt>ch</tt> in <tt>val</tt> starting * from position <tt>start</tt>. It doesn't count if <tt>ch</tt> * 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 <tt>expr</tt> using arguments from the array * <tt>args</tt>. * * <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; StringBuffer answer = new StringBuffer(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(); } } */ }