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