001: /*
002: * Copyright 1999 Sun Microsystems, Inc. All Rights Reserved.
003: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004: *
005: * This code is free software; you can redistribute it and/or modify it
006: * under the terms of the GNU General Public License version 2 only, as
007: * published by the Free Software Foundation. Sun designates this
008: * particular file as subject to the "Classpath" exception as provided
009: * by Sun in the LICENSE file that accompanied this code.
010: *
011: * This code is distributed in the hope that it will be useful, but WITHOUT
012: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014: * version 2 for more details (a copy is included in the LICENSE file that
015: * accompanied this code).
016: *
017: * You should have received a copy of the GNU General Public License version
018: * 2 along with this work; if not, write to the Free Software Foundation,
019: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020: *
021: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022: * CA 95054 USA or visit www.sun.com if you need additional information or
023: * have any questions.
024: */
025: package com.sun.jndi.toolkit.dir;
026:
027: import javax.naming.*;
028: import javax.naming.directory.*;
029: import java.util.Enumeration;
030: import java.util.StringTokenizer;
031: import java.util.Vector;
032:
033: /**
034: * A class for parsing LDAP search filters (defined in RFC 1960, 2254)
035: *
036: * @author Jon Ruiz
037: * @author Rosanna Lee
038: */
039: public class SearchFilter implements AttrFilter {
040:
041: interface StringFilter extends AttrFilter {
042: public void parse() throws InvalidSearchFilterException;
043: }
044:
045: // %%% "filter" and "pos" are not declared "private" due to bug 4064984.
046: String filter;
047: int pos;
048: private StringFilter rootFilter;
049:
050: protected static final boolean debug = false;
051:
052: protected static final char BEGIN_FILTER_TOKEN = '(';
053: protected static final char END_FILTER_TOKEN = ')';
054: protected static final char AND_TOKEN = '&';
055: protected static final char OR_TOKEN = '|';
056: protected static final char NOT_TOKEN = '!';
057: protected static final char EQUAL_TOKEN = '=';
058: protected static final char APPROX_TOKEN = '~';
059: protected static final char LESS_TOKEN = '<';
060: protected static final char GREATER_TOKEN = '>';
061: protected static final char EXTEND_TOKEN = ':';
062: protected static final char WILDCARD_TOKEN = '*';
063:
064: public SearchFilter(String filter)
065: throws InvalidSearchFilterException {
066: this .filter = filter;
067: pos = 0;
068: normalizeFilter();
069: rootFilter = this .createNextFilter();
070: }
071:
072: // Returns true if targetAttrs passes the filter
073: public boolean check(Attributes targetAttrs) throws NamingException {
074: if (targetAttrs == null)
075: return false;
076:
077: return rootFilter.check(targetAttrs);
078: }
079:
080: /*
081: * Utility routines used by member classes
082: */
083:
084: // does some pre-processing on the string to make it look exactly lik
085: // what the parser expects. This only needs to be called once.
086: protected void normalizeFilter() {
087: skipWhiteSpace(); // get rid of any leading whitespaces
088:
089: // Sometimes, search filters don't have "(" and ")" - add them
090: if (getCurrentChar() != BEGIN_FILTER_TOKEN) {
091: filter = BEGIN_FILTER_TOKEN + filter + END_FILTER_TOKEN;
092: }
093: // this would be a good place to strip whitespace if desired
094:
095: if (debug) {
096: System.out.println("SearchFilter: normalized filter:"
097: + filter);
098: }
099: }
100:
101: private void skipWhiteSpace() {
102: while (Character.isWhitespace(getCurrentChar())) {
103: consumeChar();
104: }
105: }
106:
107: protected StringFilter createNextFilter()
108: throws InvalidSearchFilterException {
109: StringFilter filter;
110:
111: skipWhiteSpace();
112:
113: try {
114: // make sure every filter starts with "("
115: if (getCurrentChar() != BEGIN_FILTER_TOKEN) {
116: throw new InvalidSearchFilterException("expected \""
117: + BEGIN_FILTER_TOKEN + "\" at position " + pos);
118: }
119:
120: // skip past the "("
121: this .consumeChar();
122:
123: skipWhiteSpace();
124:
125: // use the next character to determine the type of filter
126: switch (getCurrentChar()) {
127: case AND_TOKEN:
128: if (debug) {
129: System.out.println("SearchFilter: creating AND");
130: }
131: filter = new CompoundFilter(true);
132: filter.parse();
133: break;
134: case OR_TOKEN:
135: if (debug) {
136: System.out.println("SearchFilter: creating OR");
137: }
138: filter = new CompoundFilter(false);
139: filter.parse();
140: break;
141: case NOT_TOKEN:
142: if (debug) {
143: System.out.println("SearchFilter: creating OR");
144: }
145: filter = new NotFilter();
146: filter.parse();
147: break;
148: default:
149: if (debug) {
150: System.out.println("SearchFilter: creating SIMPLE");
151: }
152: filter = new AtomicFilter();
153: filter.parse();
154: break;
155: }
156:
157: skipWhiteSpace();
158:
159: // make sure every filter ends with ")"
160: if (getCurrentChar() != END_FILTER_TOKEN) {
161: throw new InvalidSearchFilterException("expected \""
162: + END_FILTER_TOKEN + "\" at position " + pos);
163: }
164:
165: // skip past the ")"
166: this .consumeChar();
167: } catch (InvalidSearchFilterException e) {
168: if (debug) {
169: System.out.println("rethrowing e");
170: }
171: throw e; // just rethrow these
172:
173: // catch all - any uncaught exception while parsing will end up here
174: } catch (Exception e) {
175: if (debug) {
176: System.out.println(e.getMessage());
177: e.printStackTrace();
178: }
179: throw new InvalidSearchFilterException("Unable to parse "
180: + "character " + pos + " in \"" + this .filter
181: + "\"");
182: }
183:
184: return filter;
185: }
186:
187: protected char getCurrentChar() {
188: return filter.charAt(pos);
189: }
190:
191: protected char relCharAt(int i) {
192: return filter.charAt(pos + i);
193: }
194:
195: protected void consumeChar() {
196: pos++;
197: }
198:
199: protected void consumeChars(int i) {
200: pos += i;
201: }
202:
203: protected int relIndexOf(int ch) {
204: return filter.indexOf(ch, pos) - pos;
205: }
206:
207: protected String relSubstring(int beginIndex, int endIndex) {
208: if (debug) {
209: System.out.println("relSubString: " + beginIndex + " "
210: + endIndex);
211: }
212: return filter.substring(beginIndex + pos, endIndex + pos);
213: }
214:
215: /**
216: * A class for dealing with compound filters ("and" & "or" filters).
217: */
218: final class CompoundFilter implements StringFilter {
219: private Vector subFilters;
220: private boolean polarity;
221:
222: CompoundFilter(boolean polarity) {
223: subFilters = new Vector();
224: this .polarity = polarity;
225: }
226:
227: public void parse() throws InvalidSearchFilterException {
228: SearchFilter.this .consumeChar(); // consume the "&"
229: while (SearchFilter.this .getCurrentChar() != END_FILTER_TOKEN) {
230: if (debug) {
231: System.out.println("CompoundFilter: adding");
232: }
233: StringFilter filter = SearchFilter.this
234: .createNextFilter();
235: subFilters.addElement(filter);
236: skipWhiteSpace();
237: }
238: }
239:
240: public boolean check(Attributes targetAttrs)
241: throws NamingException {
242: for (int i = 0; i < subFilters.size(); i++) {
243: StringFilter filter = (StringFilter) subFilters
244: .elementAt(i);
245: if (filter.check(targetAttrs) != this .polarity) {
246: return !polarity;
247: }
248: }
249: return polarity;
250: }
251: } /* CompoundFilter */
252:
253: /**
254: * A class for dealing with NOT filters
255: */
256: final class NotFilter implements StringFilter {
257: private StringFilter filter;
258:
259: public void parse() throws InvalidSearchFilterException {
260: SearchFilter.this .consumeChar(); // consume the "!"
261: filter = SearchFilter.this .createNextFilter();
262: }
263:
264: public boolean check(Attributes targetAttrs)
265: throws NamingException {
266: return !filter.check(targetAttrs);
267: }
268: } /* notFilter */
269:
270: // note: declared here since member classes can't have static variables
271: static final int EQUAL_MATCH = 1;
272: static final int APPROX_MATCH = 2;
273: static final int GREATER_MATCH = 3;
274: static final int LESS_MATCH = 4;
275:
276: /**
277: * A class for dealing wtih atomic filters
278: */
279: final class AtomicFilter implements StringFilter {
280: private String attrID;
281: private String value;
282: private int matchType;
283:
284: public void parse() throws InvalidSearchFilterException {
285:
286: skipWhiteSpace();
287:
288: try {
289: // find the end
290: int endPos = SearchFilter.this
291: .relIndexOf(END_FILTER_TOKEN);
292:
293: //determine the match type
294: int i = SearchFilter.this .relIndexOf(EQUAL_TOKEN);
295: if (debug) {
296: System.out.println("AtomicFilter: = at " + i);
297: }
298: int qualifier = SearchFilter.this .relCharAt(i - 1);
299: switch (qualifier) {
300: case APPROX_TOKEN:
301: if (debug) {
302: System.out.println("Atomic: APPROX found");
303: }
304: matchType = APPROX_MATCH;
305: attrID = SearchFilter.this .relSubstring(0, i - 1);
306: value = SearchFilter.this .relSubstring(i + 1,
307: endPos);
308: break;
309:
310: case GREATER_TOKEN:
311: if (debug) {
312: System.out.println("Atomic: GREATER found");
313: }
314: matchType = GREATER_MATCH;
315: attrID = SearchFilter.this .relSubstring(0, i - 1);
316: value = SearchFilter.this .relSubstring(i + 1,
317: endPos);
318: break;
319:
320: case LESS_TOKEN:
321: if (debug) {
322: System.out.println("Atomic: LESS found");
323: }
324: matchType = LESS_MATCH;
325: attrID = SearchFilter.this .relSubstring(0, i - 1);
326: value = SearchFilter.this .relSubstring(i + 1,
327: endPos);
328: break;
329:
330: case EXTEND_TOKEN:
331: if (debug) {
332: System.out.println("Atomic: EXTEND found");
333: }
334: throw new OperationNotSupportedException(
335: "Extensible match not supported");
336:
337: default:
338: if (debug) {
339: System.out.println("Atomic: EQUAL found");
340: }
341: matchType = EQUAL_MATCH;
342: attrID = SearchFilter.this .relSubstring(0, i);
343: value = SearchFilter.this .relSubstring(i + 1,
344: endPos);
345: break;
346: }
347:
348: attrID = attrID.trim();
349: value = value.trim();
350:
351: //update our position
352: SearchFilter.this .consumeChars(endPos);
353:
354: } catch (Exception e) {
355: if (debug) {
356: System.out.println(e.getMessage());
357: e.printStackTrace();
358: }
359: InvalidSearchFilterException sfe = new InvalidSearchFilterException(
360: "Unable to parse " + "character "
361: + SearchFilter.this .pos + " in \""
362: + SearchFilter.this .filter + "\"");
363: sfe.setRootCause(e);
364: throw (sfe);
365: }
366:
367: if (debug) {
368: System.out.println("AtomicFilter: " + attrID + "="
369: + value);
370: }
371: }
372:
373: public boolean check(Attributes targetAttrs) {
374: Enumeration candidates;
375:
376: try {
377: Attribute attr = targetAttrs.get(attrID);
378: if (attr == null) {
379: return false;
380: }
381: candidates = attr.getAll();
382: } catch (NamingException ne) {
383: if (debug) {
384: System.out.println("AtomicFilter: should never "
385: + "here");
386: }
387: return false;
388: }
389:
390: while (candidates.hasMoreElements()) {
391: String val = candidates.nextElement().toString();
392: if (debug) {
393: System.out.println("Atomic: comparing: " + val);
394: }
395: switch (matchType) {
396: case APPROX_MATCH:
397: case EQUAL_MATCH:
398: if (substringMatch(this .value, val)) {
399: if (debug) {
400: System.out.println("Atomic: EQUAL match");
401: }
402: return true;
403: }
404: break;
405: case GREATER_MATCH:
406: if (debug) {
407: System.out.println("Atomic: GREATER match");
408: }
409: if (val.compareTo(this .value) >= 0) {
410: return true;
411: }
412: break;
413: case LESS_MATCH:
414: if (debug) {
415: System.out.println("Atomic: LESS match");
416: }
417: if (val.compareTo(this .value) <= 0) {
418: return true;
419: }
420: break;
421: default:
422: if (debug) {
423: System.out.println("AtomicFilter: unkown "
424: + "matchType");
425: }
426: }
427: }
428: return false;
429: }
430:
431: // used for substring comparisons (where proto has "*" wildcards
432: private boolean substringMatch(String proto, String value) {
433: // simple case 1: "*" means attribute presence is being tested
434: if (proto.equals(new Character(WILDCARD_TOKEN).toString())) {
435: if (debug) {
436: System.out.println("simple presence assertion");
437: }
438: return true;
439: }
440:
441: // simple case 2: if there are no wildcards, call String.equals()
442: if (proto.indexOf(WILDCARD_TOKEN) == -1) {
443: return proto.equalsIgnoreCase(value);
444: }
445:
446: if (debug) {
447: System.out.println("doing substring comparison");
448: }
449: // do the work: make sure all the substrings are present
450: int currentPos = 0;
451: StringTokenizer subStrs = new StringTokenizer(proto, "*",
452: false);
453:
454: // do we need to begin with the first token?
455: if (proto.charAt(0) != WILDCARD_TOKEN
456: && !value.toString().toLowerCase().startsWith(
457: subStrs.nextToken().toLowerCase())) {
458: if (debug) {
459: System.out.println("faild initial test");
460: }
461: return false;
462: }
463:
464: while (subStrs.hasMoreTokens()) {
465: String currentStr = subStrs.nextToken();
466: if (debug) {
467: System.out.println("looking for \"" + currentStr
468: + "\"");
469: }
470: currentPos = value.toLowerCase().indexOf(
471: currentStr.toLowerCase(), currentPos);
472: if (currentPos == -1) {
473: return false;
474: }
475: currentPos += currentStr.length();
476: }
477:
478: // do we need to end with the last token?
479: if (proto.charAt(proto.length() - 1) != WILDCARD_TOKEN
480: && currentPos != value.length()) {
481: if (debug) {
482: System.out.println("faild final test");
483: }
484: return false;
485: }
486:
487: return true;
488: }
489:
490: } /* AtomicFilter */
491:
492: // ----- static methods for producing string filters given attribute set
493: // ----- or object array
494:
495: /**
496: * Creates an LDAP filter as a conjuction of the attributes supplied.
497: */
498: public static String format(Attributes attrs)
499: throws NamingException {
500: if (attrs == null || attrs.size() == 0) {
501: return "objectClass=*";
502: }
503:
504: String answer;
505: answer = "(& ";
506: Attribute attr;
507: for (NamingEnumeration e = attrs.getAll(); e.hasMore();) {
508: attr = (Attribute) e.next();
509: if (attr.size() == 0
510: || (attr.size() == 1 && attr.get() == null)) {
511: // only checking presence of attribute
512: answer += "(" + attr.getID() + "=" + "*)";
513: } else {
514: for (NamingEnumeration ve = attr.getAll(); ve.hasMore();) {
515: String val = getEncodedStringRep(ve.next());
516: if (val != null) {
517: answer += "(" + attr.getID() + "=" + val + ")";
518: }
519: }
520: }
521: }
522:
523: answer += ")";
524: //System.out.println("filter: " + answer);
525: return answer;
526: }
527:
528: // Writes the hex representation of a byte to a StringBuffer.
529: private static void hexDigit(StringBuffer buf, byte x) {
530: char c;
531:
532: c = (char) ((x >> 4) & 0xf);
533: if (c > 9)
534: c = (char) ((c - 10) + 'A');
535: else
536: c = (char) (c + '0');
537:
538: buf.append(c);
539: c = (char) (x & 0xf);
540: if (c > 9)
541: c = (char) ((c - 10) + 'A');
542: else
543: c = (char) (c + '0');
544: buf.append(c);
545: }
546:
547: /**
548: * Returns the string representation of an object (such as an attr value).
549: * If obj is a byte array, encode each item as \xx, where xx is hex encoding
550: * of the byte value.
551: * Else, if obj is not a String, use its string representation (toString()).
552: * Special characters in obj (or its string representation) are then
553: * encoded appropriately according to RFC 2254.
554: * * \2a
555: * ( \28
556: * ) \29
557: * \ \5c
558: * NUL \00
559: */
560: private static String getEncodedStringRep(Object obj)
561: throws NamingException {
562: String str;
563: if (obj == null)
564: return null;
565:
566: if (obj instanceof byte[]) {
567: // binary data must be encoded as \hh where hh is a hex char
568: byte[] bytes = (byte[]) obj;
569: StringBuffer b1 = new StringBuffer(bytes.length * 3);
570: for (int i = 0; i < bytes.length; i++) {
571: b1.append('\\');
572: hexDigit(b1, bytes[i]);
573: }
574: return b1.toString();
575: }
576: if (!(obj instanceof String)) {
577: str = obj.toString();
578: } else {
579: str = (String) obj;
580: }
581: int len = str.length();
582: StringBuffer buf = new StringBuffer(len);
583: char ch;
584: for (int i = 0; i < len; i++) {
585: switch (ch = str.charAt(i)) {
586: case '*':
587: buf.append("\\2a");
588: break;
589: case '(':
590: buf.append("\\28");
591: break;
592: case ')':
593: buf.append("\\29");
594: break;
595: case '\\':
596: buf.append("\\5c");
597: break;
598: case 0:
599: buf.append("\\00");
600: break;
601: default:
602: buf.append(ch);
603: }
604: }
605: return buf.toString();
606: }
607:
608: /**
609: * Finds the first occurrence of <tt>ch</tt> in <tt>val</tt> starting
610: * from position <tt>start</tt>. It doesn't count if <tt>ch</tt>
611: * has been escaped by a backslash (\)
612: */
613: public static int findUnescaped(char ch, String val, int start) {
614: int len = val.length();
615:
616: while (start < len) {
617: int where = val.indexOf(ch, start);
618: // if at start of string, or not there at all, or if not escaped
619: if (where == start || where == -1
620: || val.charAt(where - 1) != '\\')
621: return where;
622:
623: // start search after escaped star
624: start = where + 1;
625: }
626: return -1;
627: }
628:
629: /**
630: * Formats the expression <tt>expr</tt> using arguments from the array
631: * <tt>args</tt>.
632: *
633: * <code>{i}</code> specifies the <code>i</code>'th element from
634: * the array <code>args</code> is to be substituted for the
635: * string "<code>{i}</code>".
636: *
637: * To escape '{' or '}' (or any other character), use '\'.
638: *
639: * Uses getEncodedStringRep() to do encoding.
640: */
641:
642: public static String format(String expr, Object[] args)
643: throws NamingException {
644:
645: int param;
646: int where = 0, start = 0;
647: StringBuffer answer = new StringBuffer(expr.length());
648:
649: while ((where = findUnescaped('{', expr, start)) >= 0) {
650: int pstart = where + 1; // skip '{'
651: int pend = expr.indexOf('}', pstart);
652:
653: if (pend < 0) {
654: throw new InvalidSearchFilterException("unbalanced {: "
655: + expr);
656: }
657:
658: // at this point, pend should be pointing at '}'
659: try {
660: param = Integer.parseInt(expr.substring(pstart, pend));
661: } catch (NumberFormatException e) {
662: throw new InvalidSearchFilterException(
663: "integer expected inside {}: " + expr);
664: }
665:
666: if (param >= args.length) {
667: throw new InvalidSearchFilterException(
668: "number exceeds argument list: " + param);
669: }
670:
671: answer.append(expr.substring(start, where)).append(
672: getEncodedStringRep(args[param]));
673: start = pend + 1; // skip '}'
674: }
675:
676: if (start < expr.length())
677: answer.append(expr.substring(start));
678:
679: return answer.toString();
680: }
681:
682: /*
683: * returns an Attributes instance containing only attributeIDs given in
684: * "attributeIDs" whose values come from the given DSContext.
685: */
686: public static Attributes selectAttributes(Attributes originals,
687: String[] attrIDs) throws NamingException {
688:
689: if (attrIDs == null)
690: return originals;
691:
692: Attributes result = new BasicAttributes();
693:
694: for (int i = 0; i < attrIDs.length; i++) {
695: Attribute attr = originals.get(attrIDs[i]);
696: if (attr != null) {
697: result.put(attr);
698: }
699: }
700:
701: return result;
702: }
703:
704: /* For testing filter
705: public static void main(String[] args) {
706:
707: Attributes attrs = new BasicAttributes(LdapClient.caseIgnore);
708: attrs.put("cn", "Rosanna Lee");
709: attrs.put("sn", "Lee");
710: attrs.put("fn", "Rosanna");
711: attrs.put("id", "10414");
712: attrs.put("machine", "jurassic");
713:
714:
715: try {
716: System.out.println(format(attrs));
717:
718: String expr = "(&(Age = {0})(Account Balance <= {1}))";
719: Object[] fargs = new Object[2];
720: // fill in the parameters
721: fargs[0] = new Integer(65);
722: fargs[1] = new Float(5000);
723:
724: System.out.println(format(expr, fargs));
725:
726:
727: System.out.println(format("bin={0}",
728: new Object[] {new byte[] {0, 1, 2, 3, 4, 5}}));
729:
730: System.out.println(format("bin=\\{anything}", null));
731:
732: } catch (NamingException e) {
733: e.printStackTrace();
734: }
735: }
736: */
737:
738: }
|