/** * Evaluates the analyze-string function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item analyzeString(final byte[] val, final QueryContext ctx) throws QueryException { final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); if (p.matcher("").matches()) REGROUP.thrw(info); final String str = string(val); final Matcher m = p.matcher(str); final FElem root = new FElem(Q_ANALYZE, new Atts(FN, FNURI)); int s = 0; while (m.find()) { if (s != m.start()) nonmatch(str.substring(s, m.start()), root); match(m, str, root, 0); s = m.end(); } if (s != str.length()) nonmatch(str.substring(s), root); return root; }
/** * Evaluates the tokenize function. * * @param ctx query context * @return function result * @throws QueryException query exception */ private Value tokenize(final QueryContext ctx) throws QueryException { final byte[] val = checkEStr(expr[0], ctx); final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); if (p.matcher("").matches()) REGROUP.thrw(info); final TokenList tl = new TokenList(); final String str = string(val); if (!str.isEmpty()) { final Matcher m = p.matcher(str); int s = 0; while (m.find()) { tl.add(str.substring(s, m.start())); s = m.end(); } tl.add(str.substring(s, str.length())); } return StrSeq.get(tl); }
/** * Evaluates the replace function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item replace(final byte[] val, final QueryContext ctx) throws QueryException { final byte[] rep = checkStr(expr[2], ctx); for (int i = 0; i < rep.length; ++i) { if (rep[i] == '\\') { if (i + 1 == rep.length || rep[i + 1] != '\\' && rep[i + 1] != '$') FUNREPBS.thrw(info); ++i; } if (rep[i] == '$' && (i == 0 || rep[i - 1] != '\\') && (i + 1 == rep.length || !digit(rep[i + 1]))) FUNREPDOL.thrw(info); } final Pattern p = pattern(expr[1], expr.length == 4 ? expr[3] : null, ctx); if (p.pattern().isEmpty()) REGROUP.thrw(info); String r = string(rep); if ((p.flags() & Pattern.LITERAL) != 0) { r = SLASH.matcher(BSLASH.matcher(r).replaceAll("\\\\\\\\")).replaceAll("\\\\\\$"); } try { return Str.get(p.matcher(string(val)).replaceAll(r)); } catch (final Exception ex) { if (ex.getMessage().contains("No group")) REGROUP.thrw(info); throw REGPAT.thrw(info, ex); } }
/** * String pattern functions. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public final class FNPat extends StandardFunc { /** Pattern cache. */ private final TokenObjMap<Pattern> patterns = new TokenObjMap<Pattern>(); /** Slash pattern. */ private static final Pattern SLASH = Pattern.compile("\\$"); /** Slash pattern. */ private static final Pattern BSLASH = Pattern.compile("\\\\"); /** Root element for the analyze-string-result function. */ private static final QNm Q_ANALYZE = new QNm("fn:analyze-string-result", FNURI); /** Element for the analyze-string-result function. */ private static final QNm Q_MATCH = new QNm("fn:match", FNURI); /** Element for the analyze-string-result function. */ private static final QNm Q_NONMATCH = new QNm("fn:non-match", FNURI); /** Element for the analyze-string-result function. */ private static final QNm Q_MGROUP = new QNm("fn:group", FNURI); /** Attribute for the analyze-string-result function. */ private static final QNm Q_NR = new QNm("nr"); /** * Constructor. * * @param ii input info * @param f function definition * @param e arguments */ public FNPat(final InputInfo ii, final Function f, final Expr... e) { super(ii, f, e); } @Override public Iter iter(final QueryContext ctx) throws QueryException { switch (sig) { case TOKENIZE: return tokenize(ctx).iter(); default: return super.iter(ctx); } } @Override public Value value(final QueryContext ctx) throws QueryException { switch (sig) { case TOKENIZE: return tokenize(ctx); default: return super.value(ctx); } } @Override public Item item(final QueryContext ctx, final InputInfo ii) throws QueryException { switch (sig) { case MATCHES: return matches(checkEStr(expr[0], ctx), ctx); case REPLACE: return replace(checkEStr(expr[0], ctx), ctx); case ANALYZE_STRING: return analyzeString(checkEStr(expr[0], ctx), ctx); default: return super.item(ctx, ii); } } /** * Evaluates the match function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item matches(final byte[] val, final QueryContext ctx) throws QueryException { final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); return Bln.get(p.matcher(string(val)).find()); } /** * Evaluates the analyze-string function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item analyzeString(final byte[] val, final QueryContext ctx) throws QueryException { final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); if (p.matcher("").matches()) REGROUP.thrw(info); final String str = string(val); final Matcher m = p.matcher(str); final FElem root = new FElem(Q_ANALYZE, new Atts(FN, FNURI)); int s = 0; while (m.find()) { if (s != m.start()) nonmatch(str.substring(s, m.start()), root); match(m, str, root, 0); s = m.end(); } if (s != str.length()) nonmatch(str.substring(s), root); return root; } /** * Processes a match. * * @param m matcher * @param str string * @param par parent * @param g group number * @return next group number and position in string */ private static int[] match(final Matcher m, final String str, final FElem par, final int g) { final FElem nd = new FElem(g == 0 ? Q_MATCH : Q_MGROUP, new Atts(FN, FNURI)); if (g > 0) nd.add(Q_NR, token(g)); final int start = m.start(g), end = m.end(g), gc = m.groupCount(); int[] pos = {g + 1, start}; // group and position in string while (pos[0] <= gc && m.end(pos[0]) <= end) { final int st = m.start(pos[0]); if (st >= 0) { // group matched if (pos[1] < st) nd.add(str.substring(pos[1], st)); pos = match(m, str, nd, pos[0]); } else pos[0]++; // skip it } if (pos[1] < end) { nd.add(str.substring(pos[1], end)); pos[1] = end; } par.add(nd); return pos; } /** * Processes a non-match. * * @param text text * @param par root node */ private static void nonmatch(final String text, final FElem par) { par.add(new FElem(Q_NONMATCH, new Atts(FN, FNURI)).add(text)); } /** * Evaluates the replace function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item replace(final byte[] val, final QueryContext ctx) throws QueryException { final byte[] rep = checkStr(expr[2], ctx); for (int i = 0; i < rep.length; ++i) { if (rep[i] == '\\') { if (i + 1 == rep.length || rep[i + 1] != '\\' && rep[i + 1] != '$') FUNREPBS.thrw(info); ++i; } if (rep[i] == '$' && (i == 0 || rep[i - 1] != '\\') && (i + 1 == rep.length || !digit(rep[i + 1]))) FUNREPDOL.thrw(info); } final Pattern p = pattern(expr[1], expr.length == 4 ? expr[3] : null, ctx); if (p.pattern().isEmpty()) REGROUP.thrw(info); String r = string(rep); if ((p.flags() & Pattern.LITERAL) != 0) { r = SLASH.matcher(BSLASH.matcher(r).replaceAll("\\\\\\\\")).replaceAll("\\\\\\$"); } try { return Str.get(p.matcher(string(val)).replaceAll(r)); } catch (final Exception ex) { if (ex.getMessage().contains("No group")) REGROUP.thrw(info); throw REGPAT.thrw(info, ex); } } /** * Evaluates the tokenize function. * * @param ctx query context * @return function result * @throws QueryException query exception */ private Value tokenize(final QueryContext ctx) throws QueryException { final byte[] val = checkEStr(expr[0], ctx); final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); if (p.matcher("").matches()) REGROUP.thrw(info); final TokenList tl = new TokenList(); final String str = string(val); if (!str.isEmpty()) { final Matcher m = p.matcher(str); int s = 0; while (m.find()) { tl.add(str.substring(s, m.start())); s = m.end(); } tl.add(str.substring(s, str.length())); } return StrSeq.get(tl); } /** * Returns a regular expression pattern. * * @param pattern input pattern * @param modifier modifier item * @param ctx query context * @return pattern modifier * @throws QueryException query exception */ private Pattern pattern(final Expr pattern, final Expr modifier, final QueryContext ctx) throws QueryException { final byte[] pat = checkStr(pattern, ctx); final byte[] mod = modifier != null ? checkStr(modifier, ctx) : null; final TokenBuilder tb = new TokenBuilder(pat); if (mod != null) tb.add(0).add(mod); final byte[] key = tb.finish(); Pattern p = patterns.get(key); if (p == null) { p = RegExParser.parse(pat, mod, ctx.sc.xquery3(), info); patterns.add(key, p); } return p; } @Override public boolean xquery3() { return sig == ANALYZE_STRING; } @Override public boolean uses(final Use u) { return u == Use.X30 && xquery3() || u == Use.CNS && sig == ANALYZE_STRING || super.uses(u); } }
/** * Evaluates the match function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item matches(final byte[] val, final QueryContext ctx) throws QueryException { final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); return Bln.get(p.matcher(string(val)).find()); }
/** * Checks if the specified XQuery string is a library module. * * @param qu query string * @return result of check */ public static boolean isLibrary(final String qu) { return LIBMOD_PATTERN.matcher(removeComments(qu, 80)).matches(); }
/** * This class is an entry point for evaluating XQuery strings. * * @author BaseX Team 2005-16, BSD License * @author Christian Gruen */ public final class QueryProcessor extends Job implements Closeable { /** Pattern for detecting library modules. */ private static final Pattern LIBMOD_PATTERN = Pattern.compile( "^(xquery( version ['\"].*?['\"])?( encoding ['\"].*?['\"])? ?; ?)?module namespace.*"); /** Static context. */ public final StaticContext sc; /** Expression context. */ public final QueryContext qc; /** Query. */ private final String query; /** Parsed flag. */ private boolean parsed; /** * Default constructor. * * @param query query string * @param ctx database context */ public QueryProcessor(final String query, final Context ctx) { this(query, null, ctx); } /** * Default constructor. * * @param query query string * @param uri base uri (can be {@code null}) * @param ctx database context */ public QueryProcessor(final String query, final String uri, final Context ctx) { this.query = query; qc = pushJob(new QueryContext(ctx)); sc = new StaticContext(qc); sc.baseURI(uri); } /** * Parses the query. * * @throws QueryException query exception */ public void parse() throws QueryException { if (parsed) return; try { qc.parseMain(query, null, sc); } finally { parsed = true; updating = qc.updating; } } /** * Compiles the query. * * @throws QueryException query exception */ public void compile() throws QueryException { parse(); qc.compile(); } /** * Returns a memory-efficient result iterator. In most cases, the query will only be fully * evaluated if all items of this iterator are requested. * * @return result iterator * @throws QueryException query exception */ public Iter iter() throws QueryException { parse(); return qc.iter(); } /** * Evaluates the query and returns the resulting value. * * @return result value * @throws QueryException query exception */ public Value value() throws QueryException { parse(); return qc.iter().value(); } /** * This function is called by the GUI; use {@link #iter()} or {@link #value()} instead. Caches and * returns the result of the specified query. If all nodes are of the same database instance, the * returned value will be of type {@link DBNodes}. * * @param max maximum number of results to cache (negative: return all values) * @return result of query * @throws QueryException query exception */ public Value cache(final int max) throws QueryException { parse(); return qc.cache(max); } /** * Binds a value with the specified type to a global variable. If the value is an {@link Expr} * instance, it is directly assigned. Otherwise, it is first cast to the appropriate XQuery type. * If {@code "json"} is specified as type, the value is interpreted according to the rules * specified in {@link JsonMapConverter}. * * @param name name of variable * @param value value to be bound * @param type type (may be {@code null}) * @return self reference * @throws QueryException query exception */ public QueryProcessor bind(final String name, final Object value, final String type) throws QueryException { qc.bind(name, value, type, sc); return this; } /** * Binds a value to a global variable. * * @param name name of variable * @param value value to be bound * @return self reference * @throws QueryException query exception */ public QueryProcessor bind(final String name, final Object value) throws QueryException { return bind(name, value, null); } /** * Binds an XQuery value to a global variable. * * @param name name of variable * @param value value to be bound * @return self reference * @throws QueryException query exception */ public QueryProcessor bind(final String name, final Value value) throws QueryException { qc.bind(name, value, sc); return this; } /** * Binds the context value. * * @param value value to be bound * @return self reference * @throws QueryException query exception */ public QueryProcessor context(final Object value) throws QueryException { return context(value, null); } /** * Binds the context value. * * @param value XQuery value to be bound * @return self reference */ public QueryProcessor context(final Value value) { qc.context(value, sc); return this; } /** * Binds the HTTP context to the query processor. * * @param value HTTP context * @return self reference */ public QueryProcessor http(final Object value) { qc.http(value); return this; } /** * Binds the context value with a specified type, using the same rules as for {@link #bind binding * variables}. * * @param value value to be bound * @param type type (may be {@code null}) * @return self reference * @throws QueryException query exception */ public QueryProcessor context(final Object value, final String type) throws QueryException { qc.context(value, type, sc); return this; } /** * Declares a namespace. A namespace is undeclared if the {@code uri} is an empty string. The * default element namespaces is set if the {@code prefix} is empty. * * @param prefix namespace prefix * @param uri namespace uri * @return self reference * @throws QueryException query exception */ public QueryProcessor namespace(final String prefix, final String uri) throws QueryException { sc.namespace(prefix, uri); return this; } /** * Assigns a URI resolver. * * @param resolver resolver * @return self reference */ public QueryProcessor uriResolver(final UriResolver resolver) { sc.resolver = resolver; return this; } /** * Returns a serializer for the given output stream. Optional output declarations within the query * will be included in the serializer instance. * * @param os output stream * @return serializer instance * @throws IOException query exception * @throws QueryException query exception */ public Serializer getSerializer(final OutputStream os) throws IOException, QueryException { compile(); try { return Serializer.get(os, qc.serParams()).sc(sc); } catch (final QueryIOException ex) { throw ex.getCause(); } } /** * Adds a module reference. Only called from the test APIs. * * @param uri module uri * @param file file name */ public void module(final String uri, final String file) { qc.modDeclared.put(uri, file); } /** * Returns the query string. * * @return query */ public String query() { return query; } @Override public void close() { qc.close(); } @Override public void databases(final LockResult lr) { qc.databases(lr); } /** * Returns the number of performed updates after query execution, or {@code 0}. * * @return number of updates */ public int updates() { return updating ? qc.updates().size() : 0; } /** * Returns query information. * * @return query information */ public String info() { return qc.info(); } /** * Checks if the specified XQuery string is a library module. * * @param qu query string * @return result of check */ public static boolean isLibrary(final String qu) { return LIBMOD_PATTERN.matcher(removeComments(qu, 80)).matches(); } /** * Removes comments from the specified string and returns the first characters of a query. * * @param qu query string * @param max maximum length of string to return * @return result */ public static String removeComments(final String qu, final int max) { final StringBuilder sb = new StringBuilder(); int m = 0; boolean s = false; final int cl = qu.length(); for (int c = 0; c < cl && sb.length() < max; ++c) { final char ch = qu.charAt(c); if (ch == 0x0d) continue; if (ch == '(' && c + 1 < cl && qu.charAt(c + 1) == ':') { if (m == 0 && !s) { sb.append(' '); s = true; } ++m; ++c; } else if (m != 0 && ch == ':' && c + 1 < cl && qu.charAt(c + 1) == ')') { --m; ++c; } else if (m == 0) { if (ch > ' ') sb.append(ch); else if (!s) sb.append(' '); s = ch <= ' '; } } if (sb.length() >= max) sb.append("..."); return sb.toString().trim(); } /** * Returns a tree representation of the query plan. * * @return root node */ public FDoc plan() { return new FDoc().add(qc.plan()); } @Override public String shortInfo() { return PLEASE_WAIT_D; } @Override public String detailedInfo() { return PLEASE_WAIT_D; } @Override public String toString() { return query; } }