/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * ident "%Z%%M% %I% %E% SMI" * * Copyright 2001,2003 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. * */ // SCCS Status: %W% %G% // Parser.java: LDAP Parser for those service stores that need it. // Author: James Kempf // Created On: Mon Apr 27 08:11:08 1998 // Last Modified By: James Kempf // Last Modified On: Mon Mar 1 08:29:36 1999 // Update Count: 45 // package com.sun.slp; import java.util.*; import java.io.*; /** * The Parser class implements LDAP query parsing for ServiceStoreInMemory. * It is an internal class because it must know about the internal * structure of the hashtables. * * @version %R%.%L% %D% * @author James Kempf */ abstract class Parser extends Object { final private static char NONASCII_LOWER = '\u0080'; final private static char NONASCII_UPPER = '\uffff'; final static char EQUAL = '='; final static char LESS = '<'; final static char GREATER = '>'; private final static char STAR = '*'; final static char PRESENT = STAR; private final static char OPAREN = '('; private final static char CPAREN = ')'; private final static char APPROX = '~'; private final static char NOT = '!'; private final static char AND = '&'; private final static char OR = '|'; private final static char SPACE = ' '; /** * Record for returning stuff to the service store. * * @version %R%.%L% %D% * @author James Kempf */ static final class ParserRecord extends Object { Hashtable services = new Hashtable(); Hashtable signatures = new Hashtable(); } /** * The QueryEvaluator interface evaluates a term in a query, given * the attribute id, the operator, the object, and whether the * term is currently under negation from a not operator. Only those * ServiceStore implemenations that want to use the Parser * class to perform query parsing must provide this. * * @version %R%.%L% %D% * @author James Kempf */ interface QueryEvaluator { /** * Evaluate the query, storing away the services that match. * * @param tag The attribute tag for the term. * @param op The term operator. * @param pattern the operand of the term. * @param invert True if the results of the comparison should be * inverted due to a not operator. * @param returns Hashtable for the returns. The returns are * structured exactly like the hashtable * returned from findServices(). * @return True if the term matched, false if not. */ boolean evaluate(AttributeString tag, char op, Object pattern, boolean invert, ParserRecord returns) throws ServiceLocationException; } /** * Parse a query and incrementally evaluate. * * @param urlLevel Hashtable of langlevel hashtables containing * registrations for the service type and scope. * @param query The query. Escapes have not yet been processed. * @param ret Vector for returned records. * @param locale Locale in which to interpret query strings. * @param ret ParserRecord in which to return the results. */ static void parseAndEvaluateQuery(String query, Parser.QueryEvaluator ev, Locale locale, ParserRecord ret) throws ServiceLocationException { // Create and initialize lexical analyzer. StreamTokenizer tk = new StreamTokenizer(new StringReader(query)); tk.resetSyntax(); // make all chars ordinary... tk.wordChars('\177','\177'); // treat controls as part of tokens tk.wordChars('\000', SPACE); tk.ordinaryChar(NOT); // 'NOT' operator tk.wordChars('"', '%'); tk.ordinaryChar(AND); // 'AND' operator tk.wordChars('\'', '\''); tk.ordinaryChar(OPAREN); // filter grouping tk.ordinaryChar(CPAREN); tk.ordinaryChar(STAR); // present operator tk.wordChars('+', '{'); tk.ordinaryChar(OR); // 'OR' operator tk.wordChars('}', '~'); tk.ordinaryChar(EQUAL); // comparision operator tk.ordinaryChar(LESS); // less operator tk.ordinaryChar(GREATER); // greater operator tk.ordinaryChar(APPROX); // approx operator // Begin parsing. try { ParserRecord rec = parseFilter(tk, ev, locale, false, true); // Throw exception if anything occurs after the // parsed expression. if (tk.nextToken() != StreamTokenizer.TT_EOF) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_char_closing", new Object[] {query}); } // Merge in returns. Use OR operator so all returned // values are merged in. mergeQueryReturns(ret, rec, OR); } catch (IOException ex) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_syn_err", new Object[] {query}); } } // // Routines for dealing with parse returns record. // // Merge source to target. The target has already // been precharged with ones that must match // if the op is AND. If it's OR, then simply // stuff them in. private static boolean mergeQueryReturns(ParserRecord target, ParserRecord source, char op) { Hashtable targetServices = target.services; Hashtable sourceServices = source.services; boolean eval; if (op == AND) { eval = mergeTablesWithAnd(targetServices, sourceServices); } else { eval = mergeTablesWithOr(targetServices, sourceServices); } Hashtable targetSigs = target.signatures; Hashtable sourceSigs = source.signatures; if (op == AND) { mergeTablesWithAnd(targetSigs, sourceSigs); } else { mergeTablesWithOr(targetSigs, sourceSigs); } return eval; } // Merge tables by removing anything from target that isn't in source. private static boolean mergeTablesWithAnd(Hashtable target, Hashtable source) { Enumeration en = target.keys(); // Remove any from target that aren't in source. while (en.hasMoreElements()) { Object tkey = en.nextElement(); if (source.get(tkey) == null) { target.remove(tkey); } } // If there's nothing left, return false to indicate no further // evaluation needed. if (target.size() <= 0) { return false; } return true; } // Merge tables by adding everything from source into target. private static boolean mergeTablesWithOr(Hashtable target, Hashtable source) { Enumeration en = source.keys(); while (en.hasMoreElements()) { Object skey = en.nextElement(); target.put(skey, source.get(skey)); } return true; } // // Parsing for various productions. // // Parse the filter production. private static ParserRecord parseFilter(StreamTokenizer tk, Parser.QueryEvaluator ev, Locale locale, boolean invert, boolean eval) throws ServiceLocationException, IOException { ParserRecord ret = null; int tok = tk.nextToken(); // Check for opening paren. if (tok != OPAREN) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_init_par", new Object[0]); } // Parse inside. tok = tk.nextToken(); // Check for a logical operator. if (tok == AND || tok == OR) { ret = parseFilterlist(tk, ev, locale, (char)tok, invert, eval); } else if (tok == NOT) { ret = parseFilter(tk, ev, locale, !invert, eval); } else if (tok == StreamTokenizer.TT_WORD) { tk.pushBack(); ret = parseItem(tk, ev, locale, invert, eval); } else { // Since we've covered the ASCII character set, the only other // thing that could be here is a nonASCII character. We push it // back and deal with it in parseItem(). tk.pushBack(); ret = parseItem(tk, ev, locale, invert, eval); } tok = tk.nextToken(); // Check for closing paren. if (tok != CPAREN) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_final_par", new Object[0]); } return ret; } // Parse a filterlist production. private static ParserRecord parseFilterlist(StreamTokenizer tk, Parser.QueryEvaluator ev, Locale locale, char op, boolean invert, boolean eval) throws ServiceLocationException, IOException { boolean match; ParserRecord mrex = null; // Parse through the list of filters. do { ParserRecord prex = null; if (op == AND) { prex = parseFilter(tk, ev, locale, invert, eval); } else { prex = parseFilter(tk, ev, locale, invert, eval); } // We need to start off with something. if (mrex == null) { mrex = prex; } else { // Merge in returns. eval = mergeQueryReturns(mrex, prex, op); } // Look for ending paren. int tok = tk.nextToken(); tk.pushBack(); if (tok == CPAREN) { return mrex; } } while (true); } // Parse item. private static ParserRecord parseItem(StreamTokenizer tk, Parser.QueryEvaluator ev, Locale locale, boolean invert, boolean eval) throws ServiceLocationException, IOException { ParserRecord prex = new ParserRecord(); AttributeString attr = parseAttr(tk, locale); char op = parseOp(tk); Object value = null; // If operator is PRESENT, then check whether // it's not really a wildcarded value. If the next // token isn't a closing paren, then it's // a wildcarded value. if (op == PRESENT) { int tok = tk.nextToken(); tk.pushBack(); // ...in any event... if ((char)tok != CPAREN) { // It's a wildcarded pattern... op = EQUAL; value = parseValue(tk, locale); // Need to convert to a wildcarded pattern. Regardless // of type, since wildcard makes the type be a // string. value = new AttributePattern(PRESENT + value.toString(), locale); } } else { value = parseValue(tk, locale); } // Check for inappropriate pattern. if (value instanceof AttributePattern && ((AttributePattern)value).isWildcarded() && op != EQUAL) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_wild_op", new Object[] {new Character(op)}); } // Check for inappropriate boolean. if ((value instanceof Boolean || value instanceof Opaque) && (op == GREATER || op == LESS)) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_bool_op", new Object[] {new Character(op)}); } // Check for wrong operator with keyword. if ((value == null || value.toString().length() <= 0) && op != PRESENT) { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_key_op", new Object[] {new Character(op)}); } if (eval) { /* * Try and evaluate the query. If the evaluation failed and the * value was an Integer or Boolean try again after converting the * value to a String. This is because the value in the query will * be converted to an Integer or Boolean in preference to a String * even though the query starts out as a String. Hence when an * attribute is registered with a String value that can equally be * parsed as a valid Integer or Boolean value the String will * almost always be parsed as an Integer or Boolean. This results * in the failing of the initial type check when performing the * query. By converting the value to a String there is another shot * at fulfulling the query. */ if (!ev.evaluate(attr, op, value, invert, prex) && !(value instanceof AttributeString)) { ev.evaluate(attr, op, new AttributeString( value.toString().trim(), locale), invert, prex); } } return prex; } // Parse attribute tag. private static AttributeString parseAttr(StreamTokenizer tk, Locale locale) throws ServiceLocationException, IOException { String str = parsePotentialNonASCII(tk); str = ServiceLocationAttribute.unescapeAttributeString(str, true); return new AttributeString(str, locale); } // Parse attribute operator. private static char parseOp(StreamTokenizer tk) throws ServiceLocationException, IOException { int tok = tk.nextToken(); // Identify operator switch (tok) { case EQUAL: // Is it present? tok = tk.nextToken(); if (tok == STAR) { return PRESENT; } else { tk.pushBack(); return EQUAL; } case APPROX: case GREATER: case LESS: // Need equals. if (tk.nextToken() != EQUAL) { break; } if (tok == APPROX) { tok = EQUAL; } return (char)tok; default: break; } throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_comp_op", new Object[0]); } // Parse expression value. private static Object parseValue(StreamTokenizer tk, Locale locale) throws ServiceLocationException, IOException { StringBuffer buf = new StringBuffer(); // Parse until the next closing paren. do { int tok = tk.nextToken(); if (tok == CPAREN) { tk.pushBack(); Object o = ServiceLocationAttribute.evaluate(buf.toString().trim()); if (o instanceof String) { o = new AttributePattern((String)o, locale); } else if (o instanceof byte[]) { o = new Opaque((byte[])o); } return o; } else if (tok != StreamTokenizer.TT_EOF) { if (tok == StreamTokenizer.TT_WORD) { buf.append(tk.sval); } else if (tok == StreamTokenizer.TT_NUMBER) { Assert.slpassert(false, "par_ntok", new Object[0]); } else { buf.append((char)tok); } } else { throw new ServiceLocationException( ServiceLocationException.PARSE_ERROR, "par_qend", new Object[0]); } } while (true); } // NonASCII characters may be in the string. StreamTokenizer // can't handle them as part of words, so we need to resort to // this loop to handle it. private static String parsePotentialNonASCII(StreamTokenizer tk) throws IOException { StringBuffer buf = new StringBuffer(); do { int tok = tk.nextToken(); if (tok == StreamTokenizer.TT_WORD) { buf.append(tk.sval); } else if (((char)tok >= NONASCII_LOWER) && ((char)tok <= NONASCII_UPPER)) { buf.append((char)tok); } else { tk.pushBack(); break; } } while (true); return buf.toString(); } }