package org.basex.query; import static org.basex.query.QueryError.*; import static org.basex.query.QueryError.chop; import static org.basex.query.QueryText.*; import static org.basex.util.Token.*; import static org.basex.util.ft.FTFlag.*; import java.io.*; import java.math.*; import java.util.*; import java.util.Map.*; import org.basex.core.*; import org.basex.core.locks.*; import org.basex.io.*; import org.basex.io.serial.*; import org.basex.query.ann.*; import org.basex.query.expr.*; import org.basex.query.expr.CmpG.*; import org.basex.query.expr.CmpN.*; import org.basex.query.expr.CmpV.*; import org.basex.query.expr.Expr.*; import org.basex.query.expr.List; import org.basex.query.expr.constr.*; import org.basex.query.expr.ft.*; import org.basex.query.expr.gflwor.*; import org.basex.query.expr.gflwor.GFLWOR.*; import org.basex.query.expr.gflwor.GroupBy.*; import org.basex.query.expr.gflwor.OrderBy.*; import org.basex.query.expr.path.*; import org.basex.query.expr.path.Test.*; import org.basex.query.func.*; import org.basex.query.func.fn.*; import org.basex.query.scope.*; import org.basex.query.up.expr.*; import org.basex.query.util.*; import org.basex.query.util.collation.*; import org.basex.query.util.format.*; import org.basex.query.util.list.*; import org.basex.query.util.parse.*; import org.basex.query.value.*; import org.basex.query.value.item.*; import org.basex.query.value.seq.*; import org.basex.query.value.type.*; import org.basex.query.value.type.SeqType.*; import org.basex.query.var.*; import org.basex.util.*; import org.basex.util.ft.*; import org.basex.util.hash.*; import org.basex.util.options.*; /** * Parser for XQuery expressions. * * @author BaseX Team 2005-17, BSD License * @author Christian Gruen */ public class QueryParser extends InputParser { /** QName check: URI is mandatory. */ private static final byte[] URICHECK = {}; /** QName check: skip namespace check. */ private static final byte[] SKIPCHECK = {}; /** Reserved function names. */ private static final TokenSet KEYWORDS = new TokenSet(); /** Decimal declarations. */ private static final byte[][] DECFORMATS = tokens( DF_DEC, DF_DIG, DF_GRP, DF_EXP, DF_INF, DF_MIN, DF_NAN, DF_PAT, DF_PC, DF_PM, DF_ZD ); static { final byte[][] keys = { SeqType.ANY_FUN.string(), token(ARRAY), NodeType.ATT.string(), NodeType.COM.string(), NodeType.DOC.string(), NodeType.ELM.string(), token(EMPTY_SEQUENCE), token(IF), AtomType.ITEM.string(), token(MAP), NodeType.NSP.string(), NodeType.NOD.string(), NodeType.PI.string(), token(SCHEMA_ATTRIBUTE), token(SCHEMA_ELEMENT), token(SWITCH), NodeType.TXT.string(), token(TYPESWITCH) }; for(final byte[] key : keys) KEYWORDS.add(key); } /** Imported modules. */ final TokenSet imports = new TokenSet(); /** Modules loaded by the current file. */ public final TokenSet modules = new TokenSet(); /** List of modules to be parsed. */ public final ArrayList<ModInfo> mods = new ArrayList<>(); /** Name of current module. */ public QNm module; /** Parsed variables. */ public final TokenObjMap<StaticVar> vars = new TokenObjMap<>(); /** Parsed functions. */ public final TokenObjMap<StaticFunc> funcs = new TokenObjMap<>(); /** Namespaces. */ public final TokenMap namespaces = new TokenMap(); /** Query context. */ public final QueryContext qc; /** Static context. */ public final StaticContext sc; /** Temporary token cache. */ private final TokenBuilder tok = new TokenBuilder(); /** Current XQDoc string. */ private final StringBuilder currDoc = new StringBuilder(); /** XQDoc string of module. */ private String doc = ""; /** Alternative error code. */ private QueryError alter; /** Function name of alternative error. */ private QNm alterFunc; /** Alternative position. */ private int alterPos; /** Declared flags. */ private final HashSet<String> decl = new HashSet<>(); /** QName cache. */ private final QNmCache qnames = new QNmCache(); /** Local variable. */ private final LocalVars localVars = new LocalVars(this); /** * Constructor. * @param query query string * @param uri base URI (can be {@code null}; only passed on if not bound to static context yet) * @param qctx query context * @param sctx static context (can be {@code null}) * @throws QueryException query exception */ public QueryParser(final String query, final String uri, final QueryContext qctx, final StaticContext sctx) throws QueryException { super(query); qc = qctx; sc = sctx != null ? sctx : new StaticContext(qctx); // set path to query file final MainOptions opts = qctx.context.options; if(uri != null) sc.baseURI(uri); // bind external variables for(final Entry<String, String> entry : opts.toMap(MainOptions.BINDINGS).entrySet()) { final String key = entry.getKey(); final Atm value = new Atm(entry.getValue()); if(key.isEmpty()) qctx.context(value, sc); else qctx.bind(key, value, sc); } } /** * Parses a main module. * Parses the "MainModule" rule. * Parses the "Setter" rule. * Parses the "QueryBody (= Expr)" rule. * @return resulting root expression * @throws QueryException query exception */ public final MainModule parseMain() throws QueryException { init(); try { versionDecl(); final int i = pos; if(wsConsumeWs(MODULE, NAMESPACE, null)) throw error(MAINMOD); pos = i; prolog1(); importModules(); prolog2(); localVars.pushContext(null); final Expr expr = expr(); if(expr == null) throw alter == null ? error(EXPREMPTY) : error(); final VarScope vs = localVars.popContext(); final MainModule mm = new MainModule(vs, expr, null, doc, null, funcs, vars, imports); finish(mm); check(mm); return mm; } catch(final QueryException ex) { mark(); ex.pos(this); throw ex; } } /** * Parses a library module. * Parses the "ModuleDecl" rule. * @param check check functions and variables * @return name of the module * @throws QueryException query exception */ public final LibraryModule parseLibrary(final boolean check) throws QueryException { init(); try { versionDecl(); wsCheck(MODULE); wsCheck(NAMESPACE); skipWs(); final byte[] pref = ncName(NONAME_X); wsCheck(IS); final byte[] uri = stringLiteral(); if(uri.length == 0) throw error(NSMODURI); module = new QNm(pref, uri); sc.ns.add(pref, uri, info()); namespaces.put(pref, uri); wsCheck(";"); // get absolute path final IO baseO = sc.baseIO(); final byte[] path = token(baseO == null ? "" : baseO.path()); qc.modParsed.put(path, uri); qc.modStack.push(path); prolog1(); importModules(); prolog2(); finish(null); if(check) check(null); qc.modStack.pop(); return new LibraryModule(module, doc, funcs, vars, imports, sc); } catch(final QueryException ex) { mark(); ex.pos(this); throw ex; } } /** * Initializes the parsing process. * @throws QueryException query exception */ private void init() throws QueryException { final IO baseIO = sc.baseIO(); file = baseIO == null ? null : baseIO.path(); if(!more()) throw error(QUERYEMPTY); // checks if the query string contains invalid characters for(int i = 0; i < length;) { // only retrieve code points for large character codes (faster) int cp = input.charAt(i); final boolean hs = cp >= Character.MIN_HIGH_SURROGATE; if(hs) cp = input.codePointAt(i); if(!XMLToken.valid(cp)) { pos = i; throw error(MODLEINV_X, cp); } i += hs ? Character.charCount(cp) : 1; } } /** * Finishes the parsing step. * @param mm main module; {@code null} for library modules * @throws QueryException query exception */ private void finish(final MainModule mm) throws QueryException { if(more()) { if(alter != null) throw error(); final String rest = rest(); pos++; if(mm == null) throw error(MODEXPR, rest); throw error(QUERYEND_X, rest); } // completes the parsing step qnames.assignURI(this, 0); if(sc.elemNS != null) sc.ns.add(EMPTY, sc.elemNS, null); } /** * Checks function calls, variable references and updating semantics. * @param mm main module; {@code null} for library modules * @throws QueryException query exception */ private void check(final MainModule mm) throws QueryException { // check function calls and variable references qc.funcs.check(qc); qc.vars.check(); // check updating semantics (skip if updates and values can be mixed) if(qc.updating && !sc.mixUpdates) { qc.funcs.checkUp(); qc.vars.checkUp(); if(mm != null) mm.expr.checkUp(); } } /** * Parses the "VersionDecl" rule. * @throws QueryException query exception */ private void versionDecl() throws QueryException { final int i = pos; if(!wsConsumeWs(XQUERY)) return; final boolean version = wsConsumeWs(VERSION); if(version) { // parse xquery version final String ver = string(stringLiteral()); if(!ver.equals(XQ10) && !Strings.eq(ver, XQ11, XQ30, XQ31)) throw error(XQUERYVER_X, ver); } // parse xquery encoding (ignored, as input always comes in as string) if(wsConsumeWs(ENCODING)) { final String enc = string(stringLiteral()); if(!Strings.supported(enc)) throw error(XQUERYENC2_X, enc); } else if(!version) { pos = i; return; } wsCheck(";"); } /** * Parses the "Prolog" rule. * Parses the "Setter" rule. * @throws QueryException query exception */ private void prolog1() throws QueryException { while(true) { final int i = pos; if(wsConsumeWs(DECLARE)) { if(wsConsumeWs(DEFAULT)) { if(!defaultNamespaceDecl() && !defaultCollationDecl() && !emptyOrderDecl() && !decimalFormatDecl(true)) throw error(DECLINCOMPLETE); } else if(wsConsumeWs(BOUNDARY_SPACE)) { boundarySpaceDecl(); } else if(wsConsumeWs(BASE_URI)) { baseURIDecl(); } else if(wsConsumeWs(CONSTRUCTION)) { constructionDecl(); } else if(wsConsumeWs(ORDERING)) { orderingModeDecl(); } else if(wsConsumeWs(REVALIDATION)) { revalidationDecl(); } else if(wsConsumeWs(COPY_NAMESPACES)) { copyNamespacesDecl(); } else if(wsConsumeWs(DECIMAL_FORMAT)) { decimalFormatDecl(false); } else if(wsConsumeWs(NAMESPACE)) { namespaceDecl(); } else if(wsConsumeWs(FT_OPTION)) { // subsequent assignment required to enable duplicate checks final FTOpt fto = new FTOpt(); while(ftMatchOption(fto)); qc.ftOpt().assign(fto); } else { pos = i; return; } } else if(wsConsumeWs(IMPORT)) { if(wsConsumeWs(SCHEMA)) { schemaImport(); } else if(wsConsumeWs(MODULE)) { moduleImport(); } else { pos = i; return; } } else { return; } currDoc.setLength(0); skipWs(); check(';'); } } /** * Parses the "Prolog" rule. * @throws QueryException query exception */ private void prolog2() throws QueryException { while(true) { final int i = pos; if(!wsConsumeWs(DECLARE)) break; if(wsConsumeWs(CONTEXT)) { contextItemDecl(); } else if(wsConsumeWs(OPTION)) { optionDecl(); } else if(wsConsumeWs(DEFAULT)) { throw error(PROLOGORDER); } else { final AnnList anns = annotations(true); if(wsConsumeWs(VARIABLE)) { // variables cannot be updating if(anns.contains(Annotation.UPDATING)) throw error(UPDATINGVAR); varDecl(anns.check(true)); } else if(wsConsumeWs(FUNCTION)) { functionDecl(anns.check(false)); } else if(!anns.isEmpty()) { throw error(VARFUNC); } else { pos = i; break; } } currDoc.setLength(0); skipWs(); check(';'); } } /** * Parses the "Annotation" rule. * @param updating also check for updating keyword * @return annotations * @throws QueryException query exception */ private AnnList annotations(final boolean updating) throws QueryException { final AnnList anns = new AnnList(); while(true) { final Ann ann; if(updating && wsConsumeWs(UPDATING)) { ann = new Ann(info(), Annotation.UPDATING); } else if(consume('%')) { skipWs(); final InputInfo ii = info(); final QNm name = eQName(QNAME_X, XQ_URI); final ItemList items = new ItemList(); if(wsConsumeWs(PAREN1)) { do { final Expr ex = literal(); if(!(ex instanceof Item)) throw error(ANNVALUE); items.add((Item) ex); } while(wsConsumeWs(COMMA)); wsCheck(PAREN2); } skipWs(); final Annotation sig = Annotation.get(name); // check if annotation is a pre-defined one if(sig == null) { // reject unknown annotations with pre-defined namespaces, ignore others final byte[] uri = name.uri(); if(NSGlobal.prefix(uri).length != 0 && !eq(uri, LOCAL_URI, ERROR_URI)) { throw (NSGlobal.reserved(uri) ? ANNWHICH_X_X : BASX_ANNOT_X_X).get( ii, '%', name.string()); } ann = new Ann(ii, name, items.finish()); } else { // check if annotation is specified more than once if(sig.single && anns.contains(sig)) throw BASX_TWICE_X_X.get(ii, '%', sig.id()); final long arity = items.size(); if(arity < sig.minMax[0] || arity > sig.minMax[1]) throw BASX_ANNNUM_X_X.get(ii, sig, arguments(arity)); final int al = sig.args.length; for(int a = 0; a < arity; a++) { final SeqType st = sig.args[Math.min(al - 1, a)]; final Item it = items.get(a); if(!st.instance(it)) throw BASX_ANNTYPE_X_X_X.get(ii, sig, st, it.seqType()); } ann = new Ann(ii, sig, items.finish()); } } else { break; } anns.add(ann); if(ann.sig == Annotation.UPDATING) qc.updating(); } skipWs(); return anns; } /** * Parses the "NamespaceDecl" rule. * @throws QueryException query exception */ private void namespaceDecl() throws QueryException { final byte[] pref = ncName(NONAME_X); wsCheck(IS); final byte[] uri = stringLiteral(); if(sc.ns.staticURI(pref) != null) throw error(DUPLNSDECL_X, pref); sc.ns.add(pref, uri, info()); namespaces.put(pref, uri); } /** * Parses the "RevalidationDecl" rule. * @throws QueryException query exception */ private void revalidationDecl() throws QueryException { if(!decl.add(REVALIDATION)) throw error(DUPLREVAL); if(wsConsumeWs(STRICT) || wsConsumeWs(LAX)) throw error(NOREVAL); wsCheck(SKIP); } /** * Parses the "BoundarySpaceDecl" rule. * @throws QueryException query exception */ private void boundarySpaceDecl() throws QueryException { if(!decl.add(BOUNDARY_SPACE)) throw error(DUPLBOUND); final boolean spaces = wsConsumeWs(PRESERVE); if(!spaces) wsCheck(STRIP); sc.spaces = spaces; } /** * Parses the "DefaultNamespaceDecl" rule. * @return true if declaration was found * @throws QueryException query exception */ private boolean defaultNamespaceDecl() throws QueryException { final boolean elem = wsConsumeWs(ELEMENT); if(!elem && !wsConsumeWs(FUNCTION)) return false; wsCheck(NAMESPACE); final byte[] uri = stringLiteral(); if(eq(XML_URI, uri)) throw error(BINDXMLURI_X_X, uri, XML); if(eq(XMLNS_URI, uri)) throw error(BINDXMLURI_X_X, uri, XMLNS); if(elem) { if(!decl.add(ELEMENT)) throw error(DUPLNS); sc.elemNS = uri.length == 0 ? null : uri; } else { if(!decl.add(FUNCTION)) throw error(DUPLNS); sc.funcNS = uri.length == 0 ? null : uri; } return true; } /** * Parses the "OptionDecl" rule. * @throws QueryException query exception */ private void optionDecl() throws QueryException { skipWs(); final QNm qnm = eQName(QNAME_X, XQ_URI); final byte[] val = stringLiteral(); final String name = string(qnm.local()); if(eq(qnm.uri(), OUTPUT_URI)) { // output declaration if(module != null) throw error(OPTDECL_X, qnm.string()); final SerializerOptions sopts = qc.serParams(); if(!decl.add("S " + name)) throw error(OUTDUPL_X, name); sopts.parse(name, val, sc, info()); } else if(eq(qnm.uri(), DB_URI)) { // project-specific declaration if(module != null) throw error(BASX_OPTDECL_X, qnm.local()); final String ukey = name.toUpperCase(Locale.ENGLISH); final Option<?> opt = qc.context.options.option(ukey); if(opt == null) throw error(BASX_OPTIONS_X, ukey); // cache old value (to be reset after query evaluation) qc.staticOpts.put(opt, qc.context.options.get(opt)); qc.tempOpts.add(name).add(string(val)); } else if(eq(qnm.uri(), QUERY_URI)) { // query-specific options switch(name) { case READ_LOCK: for(final byte[] lock : split(val, ',')) qc.readLocks.add(Locking.USER_PREFIX + string(lock).trim()); break; case WRITE_LOCK: for(final byte[] lock : split(val, ',')) qc.writeLocks.add(Locking.USER_PREFIX + string(lock).trim()); break; default: throw error(BASX_OPTIONS_X, name); } } // ignore unknown options } /** * Parses the "OrderingModeDecl" rule. * @throws QueryException query exception */ private void orderingModeDecl() throws QueryException { if(!decl.add(ORDERING)) throw error(DUPLORD); sc.ordered = wsConsumeWs(ORDERED); if(!sc.ordered) wsCheck(UNORDERED); } /** * Parses the "emptyOrderDecl" rule. * @return true if declaration was found * @throws QueryException query exception */ private boolean emptyOrderDecl() throws QueryException { if(!wsConsumeWs(ORDER)) return false; wsCheck(EMPTYORD); if(!decl.add(EMPTYORD)) throw error(DUPLORDEMP); sc.orderGreatest = wsConsumeWs(GREATEST); if(!sc.orderGreatest) wsCheck(LEAST); return true; } /** * Parses the "copyNamespacesDecl" rule. * Parses the "PreserveMode" rule. * Parses the "InheritMode" rule. * @throws QueryException query exception */ private void copyNamespacesDecl() throws QueryException { if(!decl.add(COPY_NAMESPACES)) throw error(DUPLCOPYNS); sc.preserveNS = wsConsumeWs(PRESERVE); if(!sc.preserveNS) wsCheck(NO_PRESERVE); wsCheck(COMMA); sc.inheritNS = wsConsumeWs(INHERIT); if(!sc.inheritNS) wsCheck(NO_INHERIT); } /** * Parses the "DecimalFormatDecl" rule. * @param def default flag * @return true if declaration was found * @throws QueryException query exception */ private boolean decimalFormatDecl(final boolean def) throws QueryException { if(def && !wsConsumeWs(DECIMAL_FORMAT)) return false; // use empty name for default declaration final QNm name = def ? new QNm() : eQName(QNAME_X, null); // check if format has already been declared if(sc.decFormats.get(name.id()) != null) throw error(DECDUPL); // create new format final TokenMap map = new TokenMap(); // collect all property declarations int n; do { n = map.size(); skipWs(); final byte[] prop = ncName(null); for(final byte[] s : DECFORMATS) { if(!eq(prop, s)) continue; if(map.get(s) != null) throw error(DECDUPLPROP_X, s); wsCheck(IS); map.put(s, stringLiteral()); break; } if(map.isEmpty()) throw error(NODECLFORM_X, prop); } while(n != map.size()); // completes the format declaration sc.decFormats.put(name.id(), new DecFormatter(map, info())); return true; } /** * Parses the "DefaultCollationDecl" rule. * @return query expression * @throws QueryException query exception */ private boolean defaultCollationDecl() throws QueryException { if(!wsConsumeWs(COLLATION)) return false; if(!decl.add(COLLATION)) throw error(DUPLCOLL); sc.collation = Collation.get(stringLiteral(), qc, sc, info(), WHICHDEFCOLL_X); return true; } /** * Parses the "BaseURIDecl" rule. * @throws QueryException query exception */ private void baseURIDecl() throws QueryException { if(!decl.add(BASE_URI)) throw error(DUPLBASE); sc.baseURI(string(stringLiteral())); } /** * Parses the "SchemaImport" rule. * Parses the "SchemaPrefix" rule. * @throws QueryException query exception */ private void schemaImport() throws QueryException { byte[] pref = null; if(wsConsumeWs(NAMESPACE)) { pref = ncName(NONAME_X); if(eq(pref, XML, XMLNS)) throw error(BINDXML_X, pref); wsCheck(IS); } else if(wsConsumeWs(DEFAULT)) { wsCheck(ELEMENT); wsCheck(NAMESPACE); } byte[] ns = stringLiteral(); if(pref != null && ns.length == 0) throw error(NSEMPTY); if(!Uri.uri(ns).isValid()) throw error(INVURI_X, ns); if(wsConsumeWs(AT)) { do { ns = stringLiteral(); if(!Uri.uri(ns).isValid()) throw error(INVURI_X, ns); } while(wsConsumeWs(COMMA)); } throw error(IMPLSCHEMA); } /** * Parses the "ModuleImport" rule. * @throws QueryException query exception */ private void moduleImport() throws QueryException { byte[] pref = EMPTY; if(wsConsumeWs(NAMESPACE)) { pref = ncName(NONAME_X); wsCheck(IS); } final byte[] uri = trim(stringLiteral()); if(uri.length == 0) throw error(NSMODURI); if(!Uri.uri(uri).isValid()) throw error(INVURI_X, uri); if(modules.contains(uri)) throw error(DUPLMODULE_X, uri); modules.add(uri); // add non-default namespace if(pref != EMPTY) { if(sc.ns.staticURI(pref) != null) throw error(DUPLNSDECL_X, pref); sc.ns.add(pref, uri, info()); namespaces.put(pref, uri); } final ModInfo mi = new ModInfo(); mi.info = info(); mi.uri = uri; mods.add(mi); // check modules at specified locations if(wsConsumeWs(AT)) { do mi.paths.add(stringLiteral()); while(wsConsumeWs(COMMA)); } else { // check module files that have been pre-declared by a test API final byte[] path = qc.modDeclared.get(uri); if(path != null) mi.paths.add(path); } } /** * Imports all modules parsed in the prolog. * @throws QueryException query exception */ private void importModules() throws QueryException { for(final ModInfo mi : mods) importModule(mi); } /** * Imports a single module. * @param mi module import * @throws QueryException query exception */ private void importModule(final ModInfo mi) throws QueryException { final byte[] uri = mi.uri; if(mi.paths.isEmpty()) { // no paths specified: skip statically available modules for(final byte[] u : Function.URIS) if(eq(uri, u)) return; // try to resolve module uri if(qc.resources.modules().addImport(string(uri), mi.info, this)) return; // module not found throw WHICHMODULE_X.get(mi.info, uri); } // parse supplied paths for(final byte[] path : mi.paths) module(string(path), string(uri), mi.info); } /** * Parses the specified module, checking function and variable references at the end. * @param path file path * @param uri base URI of module * @param info input info * @throws QueryException query exception */ public final void module(final String path, final String uri, final InputInfo info) throws QueryException { // get absolute path final IO io = sc.resolve(path, uri); final byte[] tPath = token(io.path()); // check if module has already been parsed final byte[] tUri = token(uri), pUri = qc.modParsed.get(tPath); if(pUri != null) { if(!eq(tUri, pUri)) throw WRONGMODULE_X_X_X.get(info, io.name(), uri, pUri); return; } qc.modParsed.put(tPath, tUri); imports.put(tUri); // read module final String qu; try { qu = string(io.read()); } catch(final IOException ex) { Util.debug(ex); throw error(WHICHMODFILE_X, io); } qc.modStack.push(tPath); final QueryParser qp = new QueryParser(qu, io.path(), qc, null); final LibraryModule lib = qp.parseLibrary(false); final byte[] muri = lib.name.uri(); // check if import and declaration uri match if(!uri.equals(string(muri))) throw WRONGMODULE_X_X_X.get(info, io.name(), uri, muri); // check if context value declaration types are compatible to each other final StaticContext sctx = qp.sc; if(sctx.contextType != null) { if(sc.contextType == null) { sc.contextType = sctx.contextType; } else if(!sctx.contextType.eq(sc.contextType)) { throw error(CITYPES_X_X, sctx.contextType, sc.contextType); } } qc.modStack.pop(); } /** * Parses the "ContextItemDecl" rule. * @throws QueryException query exception */ private void contextItemDecl() throws QueryException { wsCheck(ITEMM); if(!decl.add(ITEMM)) throw error(DUPLITEM); if(wsConsumeWs(AS)) { final SeqType declType = itemType(); if(sc.contextType == null) { sc.contextType = declType; } else if(!sc.contextType.eq(declType)) { throw error(CITYPES_X_X, sc.contextType, declType); } } if(!wsConsumeWs(EXTERNAL)) wsCheck(ASSIGN); else if(!wsConsumeWs(ASSIGN)) return; localVars.pushContext(null); final Expr e = check(single(), NOVARDECL); final SeqType declType = sc.contextType != null ? sc.contextType : SeqType.ITEM; final VarScope vs = localVars.popContext(); qc.ctxItem = MainModule.get(vs, e, declType, currDoc.toString(), info()); if(module != null) throw error(DECITEM); if(!sc.mixUpdates && e.has(Flag.UPD)) throw error(UPCTX, e); } /** * Parses the "VarDecl" rule. * @param anns annotations * @throws QueryException query exception */ private void varDecl(final AnnList anns) throws QueryException { final Var var = newVar(); if(module != null && !eq(var.name.uri(), module.uri())) throw error(MODULENS_X, var); localVars.pushContext(null); final boolean ext = wsConsumeWs(EXTERNAL); final Expr bind; if(ext) { bind = wsConsumeWs(ASSIGN) ? check(single(), NOVARDECL) : null; } else { wsCheck(ASSIGN); bind = check(single(), NOVARDECL); } final VarScope vs = localVars.popContext(); final StaticVar sv = qc.vars.declare(var, anns, bind, ext, currDoc.toString(), vs); vars.put(sv.id(), sv); } /** * Parses an optional SeqType declaration. * @return type if preceded by {@code as} or {@code null} * @throws QueryException query exception */ private SeqType optAsType() throws QueryException { return wsConsumeWs(AS) ? sequenceType() : null; } /** * Parses the "ConstructionDecl" rule. * @throws QueryException query exception */ private void constructionDecl() throws QueryException { if(!decl.add(CONSTRUCTION)) throw error(DUPLCONS); sc.strip = wsConsumeWs(STRIP); if(!sc.strip) wsCheck(PRESERVE); } /** * Parses the "FunctionDecl" rule. * @param anns annotations * @throws QueryException query exception */ private void functionDecl(final AnnList anns) throws QueryException { final InputInfo ii = info(); final QNm name = eQName(FUNCNAME, sc.funcNS); if(keyword(name)) throw error(RESERVED_X, name.local()); wsCheck(PAREN1); if(module != null && !eq(name.uri(), module.uri())) throw error(MODULENS_X, name); localVars.pushContext(null); final Var[] args = paramList(); wsCheck(PAREN2); final SeqType type = optAsType(); final Expr expr = wsConsumeWs(EXTERNAL) ? null : enclosedExpr(); final VarScope vs = localVars.popContext(); final StaticFunc func = qc.funcs.declare( anns, name, args, type, expr, currDoc.toString(), vs, ii); funcs.put(func.id(), func); } /** * Checks if the specified name equals reserved function names. * @param name name to be checked * @return result of check */ private static boolean keyword(final QNm name) { return !name.hasPrefix() && KEYWORDS.contains(name.string()); } /** * Parses a ParamList. * @return declared variables * @throws QueryException query exception */ private Var[] paramList() throws QueryException { Var[] args = { }; while(true) { skipWs(); if(curr() != '$' && args.length == 0) break; final InputInfo ii = info(); final Var var = localVars.add(new Var(varName(), optAsType(), true, qc, sc, ii)); for(final Var v : args) { if(v.name.eq(var.name)) throw error(FUNCDUPL_X, var); } args = Array.add(args, var); if(!consume(',')) break; } return args; } /** * Parses the "EnclosedExpr" rule. * @return query expression * @throws QueryException query exception */ private Expr enclosedExpr() throws QueryException { wsCheck(CURLY1); final Expr e = expr(); wsCheck(CURLY2); return e == null ? Empty.SEQ : e; } /** * Parses the "Expr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr expr() throws QueryException { final Expr e = single(); if(e == null) { if(more()) return null; throw alter == null ? error(NOEXPR) : error(); } if(!wsConsume(COMMA)) return e; final ExprList el = new ExprList(e); do add(el, single()); while(wsConsume(COMMA)); return new List(info(), el.finish()); } /** * Parses the "ExprSingle" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr single() throws QueryException { alter = null; Expr e = flwor(); if(e == null) e = quantified(); if(e == null) e = switchh(); if(e == null) e = typeswitch(); if(e == null) e = iff(); if(e == null) e = tryCatch(); if(e == null) e = insert(); if(e == null) e = delete(); if(e == null) e = rename(); if(e == null) e = replace(); if(e == null) e = updatingFunctionCall(); if(e == null) e = copyModify(); if(e == null) e = or(); return e; } /** * Parses the "FLWORExpr" rule. * Parses the "WhereClause" rule. * Parses the "OrderByClause" rule. * Parses the "OrderSpecList" rule. * Parses the "GroupByClause" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr flwor() throws QueryException { final int s = localVars.openScope(); final LinkedList<Clause> clauses = initialClause(null); if(clauses == null) return null; final TokenObjMap<Var> curr = new TokenObjMap<>(); for(final Clause fl : clauses) for(final Var v : fl.vars()) curr.put(v.name.id(), v); int size; do { do { size = clauses.size(); initialClause(clauses); for(final Clause c : clauses) { for(final Var v : c.vars()) curr.put(v.name.id(), v); } } while(size < clauses.size()); if(wsConsumeWs(WHERE)) { alterPos = pos; clauses.add(new Where(check(single(), NOWHERE), info())); } if(wsConsumeWs(GROUP)) { wsCheck(BY); skipWs(); alterPos = pos; final Spec[] specs = groupSpecs(clauses); // find all non-grouping variables that aren't shadowed final ArrayList<VarRef> ng = new ArrayList<>(); for(final Spec spec : specs) curr.put(spec.var.name.id(), spec.var); vars: for(final Var v : curr.values()) { for(final Spec spec : specs) if(spec.var.is(v)) continue vars; ng.add(new VarRef(specs[0].info, v)); } // add new copies for all non-grouping variables final Var[] ngrp = new Var[ng.size()]; for(int i = ng.size(); --i >= 0;) { final VarRef ref = ng.get(i); // if one groups variables such as $x as xs:integer, then the resulting // sequence isn't compatible with the type and can't be assigned final Var nv = localVars.add(new Var(ref.var.name, null, false, qc, sc, ref.var.info)); ngrp[i] = nv; curr.put(nv.name.id(), nv); } final VarRef[] pre = new VarRef[ng.size()]; clauses.add(new GroupBy(specs, ng.toArray(pre), ngrp, specs[0].info)); } final boolean stable = wsConsumeWs(STABLE); if(stable) wsCheck(ORDER); if(stable || wsConsumeWs(ORDER)) { wsCheck(BY); alterPos = pos; Key[] ob = null; do { final Key key = orderSpec(); ob = ob == null ? new Key[] { key } : Array.add(ob, key); } while(wsConsume(COMMA)); final VarRef[] vs = new VarRef[curr.size()]; int i = 0; for(final Var var : curr.values()) vs[i++] = new VarRef(ob[0].info, var); clauses.add(new OrderBy(vs, ob, ob[0].info)); } if(wsConsumeWs(COUNT, DOLLAR, NOCOUNT)) { final Var var = localVars.add(newVar(SeqType.ITR)); curr.put(var.name.id(), var); clauses.add(new Count(var)); } } while(size < clauses.size()); if(!wsConsumeWs(RETURN)) throw alter == null ? error(FLWORRETURN) : error(); final Expr ret = check(single(), NORETURN); localVars.closeScope(s); return new GFLWOR(clauses.get(0).info, clauses, ret); } /** * Parses the "InitialClause" rule. * @param clauses FLWOR clauses * @return query expression * @throws QueryException query exception */ private LinkedList<Clause> initialClause(final LinkedList<Clause> clauses) throws QueryException { LinkedList<Clause> cls = clauses; // WindowClause final boolean slide = wsConsumeWs(FOR, SLIDING, NOWINDOW); if(slide || wsConsumeWs(FOR, TUMBLING, NOWINDOW)) { if(cls == null) cls = new LinkedList<>(); cls.add(windowClause(slide)); } else { // ForClause / LetClause final boolean let = wsConsumeWs(LET, SCORE, NOLET) || wsConsumeWs(LET, DOLLAR, NOLET); if(let || wsConsumeWs(FOR, DOLLAR, NOFOR)) { if(cls == null) cls = new LinkedList<>(); if(let) letClause(cls); else forClause(cls); } } return cls; } /** * Parses the "ForClause" rule. * Parses the "PositionalVar" rule. * @param cls list of clauses * @throws QueryException parse exception */ private void forClause(final LinkedList<Clause> cls) throws QueryException { do { final Var var = newVar(); final boolean emp = wsConsume(ALLOWING); if(emp) wsCheck(EMPTYORD); final Var at = wsConsumeWs(AT) ? newVar(SeqType.ITR) : null; final Var score = wsConsumeWs(SCORE) ? newVar(SeqType.DBL) : null; // check for duplicate variable names if(at != null) { if(var.name.eq(at.name)) throw error(DUPLVAR_X, at); if(score != null && at.name.eq(score.name)) throw error(DUPLVAR_X, score); } if(score != null && var.name.eq(score.name)) throw error(DUPLVAR_X, score); wsCheck(IN); final Expr expr = check(single(), NOVARDECL); // declare late because otherwise it would shadow the wrong variables cls.add(new For(localVars.add(var), localVars.add(at), localVars.add(score), expr, emp)); } while(wsConsumeWs(COMMA)); } /** * Parses the "LetClause" rule. * Parses the "FTScoreVar" rule. * @param cls list of clauses * @throws QueryException parse exception */ private void letClause(final LinkedList<Clause> cls) throws QueryException { do { final boolean score = wsConsumeWs(SCORE); final Var var = score ? newVar(SeqType.DBL) : newVar(); wsCheck(ASSIGN); final Expr e = check(single(), NOVARDECL); cls.add(new Let(localVars.add(var), e, score)); } while(wsConsume(COMMA)); } /** * Parses the "TumblingWindowClause" rule. * Parses the "SlidingWindowClause" rule. * @param slide sliding window flag * @return the window clause * @throws QueryException parse exception */ private Window windowClause(final boolean slide) throws QueryException { wsCheck(slide ? SLIDING : TUMBLING); wsCheck(WINDOW); skipWs(); final Var var = newVar(); wsCheck(IN); final Expr e = check(single(), NOVARDECL); // WindowStartCondition wsCheck(START); final Condition start = windowCond(true); // WindowEndCondition Condition end = null; final boolean only = wsConsume(ONLY), check = slide || only; if(check || wsConsume(END)) { if(check) wsCheck(END); end = windowCond(false); } return new Window(slide, localVars.add(var), e, start, only, end); } /** * Parses the "WindowVars" rule. * @param start start condition flag * @return an array containing the current, positional, previous and next variable name * @throws QueryException parse exception */ private Condition windowCond(final boolean start) throws QueryException { skipWs(); final InputInfo ii = info(); final Var var = curr('$') ? newVar(SeqType.ITEM_ZM) : null; final Var at = wsConsumeWs(AT) ? newVar(SeqType.ITEM_ZM) : null; final Var prv = wsConsumeWs(PREVIOUS) ? newVar(SeqType.ITEM_ZM) : null; final Var nxt = wsConsumeWs(NEXT) ? newVar(SeqType.ITEM_ZM) : null; wsCheck(WHEN); return new Condition(start, localVars.add(var), localVars.add(at), localVars.add(prv), localVars.add(nxt), check(single(), NOEXPR), ii); } /** * Parses the "OrderSpec" rule. * Parses the "OrderModifier" rule. * * Empty order specs are ignored, {@code order} is then returned unchanged. * @return new order key * @throws QueryException query exception */ private Key orderSpec() throws QueryException { final Expr e = check(single(), ORDERBY); boolean desc = false; if(!wsConsumeWs(ASCENDING)) desc = wsConsumeWs(DESCENDING); boolean least = !sc.orderGreatest; if(wsConsumeWs(EMPTYORD)) { least = !wsConsumeWs(GREATEST); if(least) wsCheck(LEAST); } final Collation coll = wsConsumeWs(COLLATION) ? Collation.get(stringLiteral(), qc, sc, info(), FLWORCOLL_X) : sc.collation; return new Key(info(), e, desc, least, coll); } /** * Parses the "GroupingSpec" rule. * @param cl preceding clauses * @return new group specification * @throws QueryException query exception */ private Spec[] groupSpecs(final LinkedList<Clause> cl) throws QueryException { Spec[] specs = null; do { final Var var = newVar(); final Expr by; if(var.type != null || wsConsume(ASSIGN)) { if(var.type != null) wsCheck(ASSIGN); by = check(single(), NOVARDECL); } else { final VarRef vr = localVars.resolveLocal(var.name, var.info); // the grouping variable has to be declared by the same FLWOR expression boolean dec = false; if(vr != null) { // check preceding clauses for(final Clause f : cl) { if(f.declares(vr.var)) { dec = true; break; } } // check other grouping variables if(!dec && specs != null) { for(final Spec spec : specs) { if(spec.var.is(vr.var)) { dec = true; break; } } } } if(!dec) throw error(GVARNOTDEFINED_X, var); by = vr; } final Collation coll = wsConsumeWs(COLLATION) ? Collation.get(stringLiteral(), qc, sc, info(), FLWORCOLL_X) : sc.collation; final Spec spec = new Spec(var.info, localVars.add(var), by, coll); if(specs == null) { specs = new Spec[] { spec }; } else { for(int i = specs.length; --i >= 0;) { if(specs[i].var.name.eq(spec.var.name)) { specs[i].occluded = true; break; } } specs = Array.add(specs, spec); } } while(wsConsumeWs(COMMA)); return specs; } /** * Parses the "QuantifiedExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr quantified() throws QueryException { final boolean some = wsConsumeWs(SOME, DOLLAR, NOSOME); if(!some && !wsConsumeWs(EVERY, DOLLAR, NOSOME)) return null; final int s = localVars.openScope(); For[] fl = { }; do { final Var var = newVar(); wsCheck(IN); final Expr e = check(single(), NOSOME); fl = Array.add(fl, new For(localVars.add(var), null, null, e, false)); } while(wsConsumeWs(COMMA)); wsCheck(SATISFIES); final Expr e = check(single(), NOSOME); localVars.closeScope(s); return new Quantifier(info(), fl, e, !some, sc); } /** * Parses the "SwitchExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr switchh() throws QueryException { if(!wsConsumeWs(SWITCH, PAREN1, TYPEPAR)) return null; final InputInfo ii = info(); wsCheck(PAREN1); final Expr cond = check(expr(), NOSWITCH); SwitchCase[] cases = { }; wsCheck(PAREN2); // collect all cases ExprList exprs; do { exprs = new ExprList(null); while(wsConsumeWs(CASE)) add(exprs, single()); if(exprs.size() == 1) { // add default case if(cases.length == 0) throw error(WRONGCHAR_X_X, CASE, found()); wsCheck(DEFAULT); } wsCheck(RETURN); exprs.set(0, check(single(), NOSWITCH)); cases = Array.add(cases, new SwitchCase(info(), exprs.finish())); } while(exprs.size() != 1); return new Switch(ii, cond, cases); } /** * Parses the "TypeswitchExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr typeswitch() throws QueryException { if(!wsConsumeWs(TYPESWITCH, PAREN1, TYPEPAR)) return null; final InputInfo ii = info(); wsCheck(PAREN1); final Expr ts = check(expr(), NOTYPESWITCH); wsCheck(PAREN2); TypeCase[] cases = { }; final ArrayList<SeqType> types = new ArrayList<>(); final int s = localVars.openScope(); boolean cs; do { types.clear(); cs = wsConsumeWs(CASE); if(!cs) { wsCheck(DEFAULT); skipWs(); } Var var = null; if(curr('$')) { var = localVars.add(newVar(SeqType.ITEM_ZM)); if(cs) wsCheck(AS); } if(cs) { do types.add(sequenceType()); while(wsConsume(PIPE)); } wsCheck(RETURN); final Expr ret = check(single(), NOTYPESWITCH); final TypeCase tc = new TypeCase(info(), var, types.toArray(new SeqType[types.size()]), ret); cases = Array.add(cases, tc); localVars.closeScope(s); } while(cs); if(cases.length == 1) throw error(NOTYPESWITCH); return new TypeSwitch(ii, ts, cases); } /** * Parses the "IfExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr iff() throws QueryException { if(!wsConsumeWs(IF, PAREN1, IFPAR)) return null; final InputInfo ii = info(); wsCheck(PAREN1); final Expr iff = check(expr(), NOIF); wsCheck(PAREN2); if(!wsConsumeWs(THEN)) throw error(NOIF); final Expr thn = check(single(), NOIF); if(!wsConsumeWs(ELSE)) throw error(NOIF); final Expr els = check(single(), NOIF); return new If(ii, iff, thn, els); } /** * Parses the "OrExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr or() throws QueryException { final Expr e = and(); if(!wsConsumeWs(OR)) return e; final InputInfo ii = info(); final ExprList el = new ExprList(2).add(e); do add(el, and()); while(wsConsumeWs(OR)); return new Or(ii, el.finish()); } /** * Parses the "AndExpr" rule. * @return query expression * @throws QueryException query exception */ private Expr and() throws QueryException { final Expr e = update(); if(!wsConsumeWs(AND)) return e; final InputInfo ii = info(); final ExprList el = new ExprList(2).add(e); do add(el, update()); while(wsConsumeWs(AND)); return new And(ii, el.finish()); } /** * Parses the "UpdateExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr update() throws QueryException { Expr e = comparison(); if(e != null) { while(wsConsumeWs(UPDATE)) { qc.updating(); e = new TransformWith(info(), e, curr('{') ? enclosedExpr() : check(single(), UPDATEEXPR)); } } return e; } /** * Parses the "ComparisonExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr comparison() throws QueryException { final Expr e = ftContains(); if(e != null) { for(final OpV c : OpV.VALUES) if(wsConsumeWs(c.name)) return new CmpV(e, check(ftContains(), CMPEXPR), c, sc.collation, sc, info()); for(final OpN c : OpN.VALUES) if(wsConsumeWs(c.name)) return new CmpN(e, check(ftContains(), CMPEXPR), c, info()); for(final OpG c : OpG.VALUES) if(wsConsumeWs(c.name)) return new CmpG(e, check(ftContains(), CMPEXPR), c, sc.collation, sc, info()); } return e; } /** * Parses the "FTContainsExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr ftContains() throws QueryException { final Expr e = stringConcat(); final int i = pos; if(!wsConsumeWs(CONTAINS) || !wsConsumeWs(TEXT)) { pos = i; return e; } final FTExpr select = ftSelection(false); if(wsConsumeWs(WITHOUT)) { wsCheck(CONTENT); union(); throw error(FTIGNORE); } return new FTContains(e, select, info()); } /** * Parses the "StringConcatExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr stringConcat() throws QueryException { final Expr e = range(); if(e == null || !consume(CONCAT)) return e; final ExprList el = new ExprList(e); do add(el, range()); while(wsConsume(CONCAT)); return Function.CONCAT.get(sc, info(), el.finish()); } /** * Parses the "RangeExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr range() throws QueryException { final Expr e = additive(); if(!wsConsumeWs(TO)) return e; return new Range(info(), e, check(additive(), INCOMPLETE)); } /** * Parses the "AdditiveExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr additive() throws QueryException { Expr e = multiplicative(); while(e != null) { final Calc c = consume('+') ? Calc.PLUS : consume('-') ? Calc.MINUS : null; if(c == null) break; e = new Arith(info(), e, check(multiplicative(), CALCEXPR), c); } return e; } /** * Parses the "MultiplicativeExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr multiplicative() throws QueryException { Expr e = union(); while(e != null) { final Calc c = consume('*') ? Calc.MULT : wsConsumeWs(DIV) ? Calc.DIV : wsConsumeWs(IDIV) ? Calc.IDIV : wsConsumeWs(MOD) ? Calc.MOD : null; if(c == null) break; e = new Arith(info(), e, check(union(), CALCEXPR), c); } return e; } /** * Parses the "UnionExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr union() throws QueryException { final Expr e = intersect(); if(e == null || !isUnion()) return e; final ExprList el = new ExprList(e); do add(el, intersect()); while(isUnion()); return new Union(info(), el.finish()); } /** * Checks if a union operator is found. * @return result of check * @throws QueryException query exception */ private boolean isUnion() throws QueryException { if(wsConsumeWs(UNION)) return true; final int i = pos; if(consume(PIPE) && !consume(PIPE)) return true; pos = i; return false; } /** * Parses the "IntersectExceptExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr intersect() throws QueryException { Expr e = instanceoff(); boolean lastIs = false; ExprList el = null; while(true) { final boolean is = wsConsumeWs(INTERSECT); if(!is && !wsConsumeWs(EXCEPT)) break; if((is != lastIs) && el != null) { e = intersectExcept(lastIs, el); el = null; } lastIs = is; if(el == null) el = new ExprList(e); add(el, instanceoff()); } return el != null ? intersectExcept(lastIs, el) : e; } /** * Parses the "IntersectExceptExpr" rule. * @param intersect intersect flag * @param el expression list * @return expression */ private Expr intersectExcept(final boolean intersect, final ExprList el) { return intersect ? new InterSect(info(), el.finish()) : new Except(info(), el.finish()); } /** * Parses the "InstanceofExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr instanceoff() throws QueryException { final Expr e = treat(); if(!wsConsumeWs(INSTANCE)) return e; wsCheck(OF); return new Instance(info(), e, sequenceType()); } /** * Parses the "TreatExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr treat() throws QueryException { final Expr e = castable(); if(!wsConsumeWs(TREAT)) return e; wsCheck(AS); return new Treat(info(), e, sequenceType()); } /** * Parses the "CastableExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr castable() throws QueryException { final Expr e = cast(); if(!wsConsumeWs(CASTABLE)) return e; wsCheck(AS); return new Castable(sc, info(), e, simpleType()); } /** * Parses the "CastExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr cast() throws QueryException { final Expr e = transformWith(); if(!wsConsumeWs(CAST)) return e; wsCheck(AS); return new Cast(sc, info(), e, simpleType()); } /** * Parses the "TransformWithExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr transformWith() throws QueryException { final Expr e = arrow(); final int p = pos; if(e != null && wsConsume(TRANSFORM) && wsConsume(WITH)) { qc.updating(); return new TransformWith(info(), e, enclosedExpr()); } pos = p; return e; } /** * Parses the "ArrowExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr arrow() throws QueryException { Expr e = unary(); if(e != null) { while(wsConsume(ARROW)) { skipWs(); final Expr ex; if(curr('(')) { ex = parenthesized(); } else if(curr('$')) { final InputInfo ii = info(); ex = localVars.resolve(varName(), ii); } else { ex = eQName(ARROWSPEC, sc.funcNS); } final InputInfo ii = info(); wsCheck(PAREN1); if(ex instanceof QNm) { final QNm name = (QNm) ex; if(keyword(name)) throw error(RESERVED_X, name.local()); e = function(name, ii, new ExprList(e)); } else { final ExprList argList = new ExprList(e); final int[] holes = argumentList(argList, e); final Expr[] args = argList.finish(); if(holes == null) { e = new DynFuncCall(ii, sc, ex, args); } else { e = new PartFunc(sc, ii, ex, args, holes); } } } } return e; } /** * Parses the "UnaryExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr unary() throws QueryException { boolean minus = false; boolean found = false; do { skipWs(); if(consume('-')) { minus ^= true; found = true; } else if(consume('+')) { found = true; } else { final Expr e = value(); return found ? new Unary(info(), check(e, EVALUNARY), minus) : e; } } while(true); } /** * Parses the "ValueExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr value() throws QueryException { validate(); final Expr e = extension(); return e == null ? map() : e; } /** * Parses the "ValidateExpr" rule. * @throws QueryException query exception */ private void validate() throws QueryException { final int i = pos; if(!wsConsumeWs(VALIDATE)) return; if(consume(TYPE)) { final InputInfo info = info(); qnames.add(eQName(QNAME_X, SKIPCHECK), info); } consume(STRICT); consume(LAX); skipWs(); if(curr('{')) { enclosedExpr(); throw error(IMPLVAL); } pos = i; } /** * Parses the "ExtensionExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr extension() throws QueryException { final Pragma[] pragmas = pragma(); if(pragmas == null) return null; wsCheck(CURLY1); final Expr expr = check(expr(), NOPRAGMA); wsCheck(CURLY2); return pragmas.length == 0 ? expr : new Extension(info(), pragmas, expr); } /** * Parses the "Pragma" rule. * @return array of pragmas or {@code null} * @throws QueryException query exception */ private Pragma[] pragma() throws QueryException { if(!wsConsumeWs(PRAGMA)) return null; final ArrayList<Pragma> el = new ArrayList<>(); do { final QNm name = eQName(QNAME_X, URICHECK); char c = curr(); if(c != '#' && !ws(c)) throw error(PRAGMAINV); tok.reset(); while(c != '#' || next() != ')') { if(c == 0) throw error(PRAGMAINV); tok.add(consume()); c = curr(); } final byte[] value = tok.trim().toArray(); if(eq(name.prefix(), DB_PREFIX)) { // project-specific declaration final String key = string(uc(name.local())); final Option<?> opt = qc.context.options.option(key); if(opt == null) throw error(BASX_OPTIONS_X, key); el.add(new DBPragma(name, opt, value)); } else if(eq(name.prefix(), BASEX_PREFIX)) { // project-specific declaration el.add(new BaseXPragma(name, value)); } pos += 2; } while(wsConsumeWs(PRAGMA)); return el.toArray(new Pragma[el.size()]); } /** * Parses the "MapExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr map() throws QueryException { final Expr e = path(); if(e != null && next() != '=' && wsConsumeWs(EXCL)) { final ExprList el = new ExprList(e); do add(el, path()); while(next() != '=' && wsConsumeWs(EXCL)); return SimpleMap.get(info(), el.finish()); } return e; } /** * Parses the "PathExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr path() throws QueryException { checkInit(); final ExprList el; Expr root = null; if(consume('/')) { root = new Root(info()); el = new ExprList(); final Expr ex; if(consume('/')) { // two slashes: absolute descendant path checkAxis(Axis.DESC); add(el, Step.get(info(), Axis.DESCORSELF, KindTest.NOD)); mark(); ex = step(true); } else { // one slash: absolute child path checkAxis(Axis.CHILD); mark(); ex = step(false); // no more steps: return root expression if(ex == null) return root; } add(el, ex); relativePath(el); } else { // relative path (no preceding slash) mark(); final Expr ex = step(false); if(ex == null) return null; // return expression if no slash follows if(curr() != '/' && !(ex instanceof Step)) return ex; el = new ExprList(); if(ex instanceof Step) add(el, ex); else root = ex; relativePath(el); } return Path.get(info(), root, el.finish()); } /** * Parses the "RelativePathExpr" rule. * @param el expression list * @throws QueryException query exception */ private void relativePath(final ExprList el) throws QueryException { while(true) { if(consume('/')) { if(consume('/')) { add(el, Step.get(info(), Axis.DESCORSELF, KindTest.NOD)); checkAxis(Axis.DESC); } else { checkAxis(Axis.CHILD); } } else { return; } mark(); add(el, step(true)); } } // methods for query suggestions /** * Performs an optional check init. */ void checkInit() { } /** * Performs an optional axis check. * @param axis axis */ @SuppressWarnings("unused") void checkAxis(final Axis axis) { } /** * Performs an optional test check. * @param test node test * @param attr attribute flag */ @SuppressWarnings("unused") void checkTest(final Test test, final boolean attr) { } /** * Checks a predicate. * @param open open flag */ @SuppressWarnings("unused") void checkPred(final boolean open) { } /** * Parses the "StepExpr" rule. * @param error show error if nothing is found * @return query expression or {@code null} * @throws QueryException query exception */ private Expr step(final boolean error) throws QueryException { final Expr e = postfix(); return e != null ? e : axisStep(error); } /** * Parses the "AxisStep" rule. * @param error show error if nothing is found * @return step or {@code null} * @throws QueryException query exception */ private Step axisStep(final boolean error) throws QueryException { Axis axis = null; Test test = null; if(wsConsume(DOT2)) { axis = Axis.PARENT; test = KindTest.NOD; checkTest(test, false); } else if(consume('@')) { axis = Axis.ATTR; test = nodeTest(true, true); checkTest(test, true); if(test == null) { --pos; throw error(NOATTNAME); } } else { for(final Axis ax : Axis.VALUES) { final int i = pos; if(!wsConsumeWs(ax.name)) continue; if(wsConsumeWs(COLS)) { alterPos = pos; axis = ax; final boolean attr = ax == Axis.ATTR; test = nodeTest(attr, true); checkTest(test, attr); if(test == null) throw error(AXISMISS_X, axis); break; } pos = i; } if(axis == null) { axis = Axis.CHILD; test = nodeTest(false, true); if(test == KindTest.NSP) throw error(NSAXIS); if(test != null && test.type == NodeType.ATT) axis = Axis.ATTR; checkTest(test, axis == Axis.ATTR); } if(test == null) { if(error) throw error(STEPMISS_X, found()); return null; } } final ExprList el = new ExprList(); while(wsConsume(SQUARE1)) { checkPred(true); add(el, expr()); wsCheck(SQUARE2); checkPred(false); } return Step.get(info(), axis, test, el.finish()); } /** * Parses the "NodeTest" rule. * Parses the "NameTest" rule. * Parses the "KindTest" rule. * @param att attribute flag * @param all check all tests, or only names * @return test or {@code null} * @throws QueryException query exception */ private Test nodeTest(final boolean att, final boolean all) throws QueryException { int p = pos; if(consume('*')) { p = pos; if(consume(':')) { // name test: *:name if(!consume('*')) return new NameTest(new QNm(ncName(QNAME_X)), Kind.NAME, att, sc.elemNS); } // name test: * pos = p; return new NameTest(att); } if(consume(EQNAME)) { final byte[] uri = bracedURILiteral(); if(consume('*')) { // name test: Q{uri}* final QNm nm = new QNm(COLON, uri); return new NameTest(nm, Kind.URI, att, sc.elemNS); } } pos = p; final InputInfo info = info(); QNm name = eQName(null, SKIPCHECK); if(name != null) { p = pos; if(all && wsConsumeWs(PAREN1)) { final NodeType type = NodeType.find(name); if(type != null) return kindTest(type); } else { pos = p; if(!name.hasPrefix() && consume(":*")) { // name test: prefix:* name = new QNm(concat(name.string(), COLON)); qnames.add(name, !att, info); return new NameTest(name, Kind.URI, att, sc.elemNS); } // name test: prefix:name, name, Q{uri}name qnames.add(name, !att, info); return new NameTest(name, Kind.URI_NAME, att, sc.elemNS); } } pos = p; return null; } /** * Parses the "PostfixExpr" rule. * @return postfix expression or {@code null} * @throws QueryException query exception */ private Expr postfix() throws QueryException { Expr e = primary(), old; if(e != null) { do { old = e; if(wsConsume(SQUARE1)) { // parses the "Predicate" rule final ExprList el = new ExprList(); do { add(el, expr()); wsCheck(SQUARE2); } while(wsConsume(SQUARE1)); e = new CachedFilter(info(), e, el.finish()); } else if(consume(PAREN1)) { if(e instanceof Value && !(e instanceof FItem)) throw error(NOPAREN_X_X, e); // parses the "ArgumentList" rule final InputInfo ii = info(); final ExprList argList = new ExprList(); final int[] holes = argumentList(argList, e); final Expr[] args = argList.finish(); if(holes == null) { e = new DynFuncCall(ii, sc, e, args); } else { e = new PartFunc(sc, ii, e, args, holes); } } else if(consume(QUESTION)) { // parses the "Lookup" rule e = new Lookup(info(), keySpecifier(), e); } } while(e != old); } return e; } /** * Parses the "PrimaryExpr" rule. * Parses the "VarRef" rule. * Parses the "ContextItem" rule. * Parses the "Literal" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr primary() throws QueryException { skipWs(); final char c = curr(); // variables if(c == '$') { final InputInfo ii = info(); return localVars.resolve(varName(), ii); } // parentheses if(c == '(' && next() != '#') return parenthesized(); // direct constructor if(c == '<') return dirConstructor(); // string constructor if(c == '`') return stringConstructor(); // function item Expr e = functionItem(); if(e != null) return e; // function call e = functionCall(); if(e != null) return e; // computed constructors e = compConstructor(); if(e != null) return e; // ordered expression final int p = pos; if(wsConsumeWs(ORDERED) || wsConsumeWs(UNORDERED)) { if(curr('{')) return enclosedExpr(); pos = p; } // map constructor if(wsConsumeWs(MAP, CURLY1, INCOMPLETE)) return new CMap(info(), keyValues()); // square array constructor if(wsConsumeWs(SQUARE1)) return new CArray(info(), false, values()); // curly array constructor if(wsConsumeWs(ARRAY, CURLY1, INCOMPLETE)) { wsCheck(CURLY1); final Expr a = expr(); wsCheck(CURLY2); return a == null ? new CArray(info(), true) : new CArray(info(), true, a); } // unary lookup final int ip = pos; if(consume(QUESTION)) { if(!wsConsume(COMMA) && !consume(PAREN2)) return new Lookup(info(), keySpecifier()); pos = ip; } // context value if(c == '.') { if(next() == '.') return null; if(!digit(next())) { consume(); return new ContextValue(info()); } } // literals return literal(); } /** * Parses the "KeySpecifier" rule. * @return specifier expression ({@code null} means wildcard) * @throws QueryException query exception */ private Expr keySpecifier() throws QueryException { skipWs(); final int c = curr(); if(c == '*') { consume(); return Str.WC; } return c == '(' ? parenthesized() : digit(c) ? numericLiteral(true) : Str.get(ncName(KEYSPEC)); } /** * Parses keys and values of maps. * @return map literals * @throws QueryException query exception */ private Expr[] keyValues() throws QueryException { wsCheck(CURLY1); final ExprList el = new ExprList(); if(!wsConsume(CURLY2)) { do { add(el, check(single(), INVMAPKEY)); if(!wsConsume(COL)) throw error(WRONGCHAR_X_X, COL, found()); add(el, check(single(), INVMAPVAL)); } while(wsConsume(COMMA)); wsCheck(CURLY2); } return el.finish(); } /** * Parses values of arrays. * @return array literals * @throws QueryException query exception */ private Expr[] values() throws QueryException { final ExprList el = new ExprList(); if(!wsConsume(SQUARE2)) { do { add(el, check(single(), INVMAPVAL)); } while(wsConsume(COMMA)); wsCheck(SQUARE2); } return el.finish(); } /** * Parses the "FunctionItemExpr" rule. * Parses the "NamedFunctionRef" rule. * Parses the "LiteralFunctionItem" rule. * Parses the "InlineFunction" rule. * @return function item or {@code null} * @throws QueryException query exception */ private Expr functionItem() throws QueryException { skipWs(); final int ip = pos; // parse annotations; will only be visited for XQuery 3.0 expressions final AnnList anns = annotations(false).check(false); // inline function if(wsConsume(FUNCTION) && wsConsume(PAREN1)) { if(anns.contains(Annotation.PRIVATE) || anns.contains(Annotation.PUBLIC)) throw error(NOVISALLOWED); final HashMap<Var, Expr> global = new HashMap<>(); localVars.pushContext(global); final Var[] args = paramList(); wsCheck(PAREN2); final SeqType type = optAsType(); final Expr body = enclosedExpr(); final VarScope vs = localVars.popContext(); return new Closure(info(), type, args, body, anns, global, vs); } // annotations not allowed here if(!anns.isEmpty()) throw error(NOANN); // named function reference pos = ip; final QNm name = eQName(null, sc.funcNS); if(name != null && wsConsumeWs("#")) { if(keyword(name)) throw error(RESERVED_X, name.local()); final Expr ex = numericLiteral(true); if(!(ex instanceof Int)) return ex; final int card = (int) ((ANum) ex).itr(); final Expr lit = Functions.getLiteral(name, card, qc, sc, info(), false); return lit != null ? lit : unknownLit(name, card, info()); } pos = ip; return null; } /** * Creates and registers a function literal. * @param name function name * @param card cardinality * @param ii input info * @return the literal * @throws QueryException query exception */ private Closure unknownLit(final QNm name, final int card, final InputInfo ii) throws QueryException { final Closure lit = Closure.unknownLit(name, card, qc, sc, ii); qc.funcs.registerFuncLit(lit); return lit; } /** * Parses the "Literal" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr literal() throws QueryException { final char c = curr(); // literals if(digit(c) || c == '.') return numericLiteral(false); // strings return quote(c) ? Str.get(stringLiteral()) : null; } /** * Parses the "NumericLiteral" rule. * Parses the "DecimalLiteral" rule. * Parses the "IntegerLiteral" rule. * @param itr integer flag * @return numeric literal * @throws QueryException query exception */ private Expr numericLiteral(final boolean itr) throws QueryException { tok.reset(); while(digit(curr())) tok.add(consume()); final boolean dot = consume('.'); if(dot) { // decimal literal tok.add('.'); if(itr) throw error(NUMBERITR, tok); while(digit(curr())) tok.add(consume()); } if(XMLToken.isNCStartChar(curr())) { if(!consume('e') && !consume('E')) throw error(NUMBERWS); // double literal tok.add('e'); if(curr('+') || curr('-')) tok.add(consume()); final int s = tok.size(); while(digit(curr())) tok.add(consume()); if(s == tok.size()) throw error(NUMBER_X, tok); if(XMLToken.isNCStartChar(curr())) throw error(NUMBERWS); return Dbl.get(tok.toArray(), info()); } final byte[] tmp = tok.toArray(); final int tl = tmp.length; if(tl == 0) throw error(NUMBER_X, tmp); if(dot) { if(tl == 1 && tmp[0] == '.') throw error(NUMBER_X, tmp); return Dec.get(new BigDecimal(string(trim(tmp)))); } final long l = toLong(tmp); if(l != Long.MIN_VALUE) return Int.get(l); final InputInfo ii = info(); return FnError.get(RANGE_X.get(ii, chop(tok, ii)), SeqType.ITR, sc); } /** * Parses the "StringLiteral" rule. * @return query expression * @throws QueryException query exception */ private byte[] stringLiteral() throws QueryException { skipWs(); final char del = curr(); if(!quote(del)) throw error(NOQUOTE_X, found()); consume(); tok.reset(); while(true) { while(!consume(del)) { if(!more()) throw error(NOQUOTE_X, found()); entity(tok); } if(!consume(del)) break; tok.add(del); } return tok.toArray(); } /** * Parses the "BracedURILiteral" rule without the "Q{" prefix. * @return query expression * @throws QueryException query exception */ private byte[] bracedURILiteral() throws QueryException { tok.reset(); while(!consume('}')) { if(!more()) throw error(WRONGCHAR_X_X, CURLY2, found()); entity(tok); } return tok.toArray(); } /** * Parses the "VarName" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private QNm varName() throws QueryException { check('$'); skipWs(); return eQName(NOVARNAME, null); } /** * Parses a variable with an optional type declaration. * @return variable * @throws QueryException query exception */ private Var newVar() throws QueryException { return newVar(null); } /** * Parses a variable. * @param type type (if {@code null}, optional type will be parsed) * @return variable * @throws QueryException query exception */ private Var newVar(final SeqType type) throws QueryException { final InputInfo ii = info(); final QNm name = varName(); final SeqType st = type != null ? type : optAsType(); return new Var(name, st, false, qc, sc, ii); } /** * Parses the "ParenthesizedExpr" rule. * @return query expression * @throws QueryException query exception */ private Expr parenthesized() throws QueryException { check('('); final Expr e = expr(); wsCheck(PAREN2); return e == null ? Empty.SEQ : e; } /** * Parses the "FunctionCall" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr functionCall() throws QueryException { final int i = pos; final QNm name = eQName(null, sc.funcNS); if(name != null && !keyword(name)) { final InputInfo ii = info(); if(wsConsume(PAREN1)) { final Expr ret = function(name, ii, new ExprList()); if(ret != null) return ret; } } pos = i; return null; } /** * Returns a function. * @param name function name * @param ii input info * @param argList list of arguments * @return function or {@code null} * @throws QueryException query exception */ private Expr function(final QNm name, final InputInfo ii, final ExprList argList) throws QueryException { final int[] holes = argumentList(argList, name.string()); final Expr[] args = argList.finish(); alter = WHICHFUNC_X; alterFunc = name; alterPos = pos; final Expr ret; if(holes != null) { final int card = args.length + holes.length; final Expr lit = Functions.getLiteral(name, card, qc, sc, ii, false); final Expr f = lit != null ? lit : unknownLit(name, card, ii); ret = new PartFunc(sc, ii, f, args, holes); } else { final TypedFunc f = Functions.get(name, args, qc, sc, ii); ret = f != null ? f.fun : null; } if(ret != null) alter = null; return ret; } /** * Parses the "ArgumentList" rule without the opening parenthesis. * @param args list to put the argument expressions into * @param name name of the function (item); only required for error messages * @return array of arguments, place-holders '?' are represented as {@code null} entries * @throws QueryException query exception */ private int[] argumentList(final ExprList args, final Object name) throws QueryException { int[] holes = null; if(!wsConsume(PAREN2)) { int i = args.size(); do { final Expr e = single(); if(e != null) { args.add(e); } else if(wsConsume(QUESTION)) { holes = holes == null ? new int[] { i } : Array.add(holes, i); } else { throw funcMiss(name); } i++; } while(wsConsume(COMMA)); if(!wsConsume(PAREN2)) throw funcMiss(name); } return holes; } /** * Parses the "StringConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr stringConstructor() throws QueryException { check('`'); check('`'); check('['); final ExprList el = new ExprList(); final TokenBuilder tb = new TokenBuilder(); while(more()) { final int p = pos; if(consume(']') && consume('`') && consume('`')) { if(!tb.isEmpty()) el.add(Str.get(tb.next())); return Function.CONCAT.get(sc, info(), el.finish()); } pos = p; if(consume('`') && consume('{')) { if(!tb.isEmpty()) el.add(Str.get(tb.next())); final Expr e = expr(); if(e != null) el.add(Function.STRING_JOIN.get(sc, info(), e, Str.get(" "))); skipWs(); check('}'); check('`'); } else { pos = p; tb.add(consume()); } } throw error(INCOMPLETE); } /** * Parses the "DirectConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr dirConstructor() throws QueryException { check('<'); return consume('!') ? dirComment() : consume('?') ? dirPI() : dirElement(); } /** * Parses the "DirElemConstructor" rule. * Parses the "DirAttributeList" rules. * @return query expression * @throws QueryException query exception */ private Expr dirElement() throws QueryException { // cache namespace information final int s = sc.ns.size(); final byte[] nse = sc.elemNS; final int npos = qnames.size(); final InputInfo info = info(); final QNm name = new QNm(qName(ELEMNAME_X)); qnames.add(name, info); consumeWS(); final Atts ns = new Atts(); final ExprList cont = new ExprList(); // parse attributes... boolean xmlDecl = false; // xml prefix explicitly declared? ArrayList<QNm> atts = null; while(true) { final byte[] atn = qName(null); if(atn.length == 0) break; final ExprList attv = new ExprList(); consumeWS(); check('='); consumeWS(); final char delim = consume(); if(!quote(delim)) throw error(NOQUOTE_X, found()); final TokenBuilder tb = new TokenBuilder(); boolean simple = true; do { while(!consume(delim)) { final char ch = curr(); if(ch == '{') { if(next() == '{') { tb.add(consume()); consume(); } else { final byte[] text = tb.next(); if(text.length == 0) { add(attv, enclosedExpr()); simple = false; } else { add(attv, Str.get(text)); } } } else if(ch == '}') { consume(); check('}'); tb.add('}'); } else if(ch == '<' || ch == 0) { throw error(NOQUOTE_X, found()); } else if(ch == '\n' || ch == '\t') { tb.add(' '); consume(); } else if(ch == '\r') { if(next() != '\n') tb.add(' '); consume(); } else { entity(tb); } } if(!consume(delim)) break; tb.add(delim); } while(true); if(!tb.isEmpty()) add(attv, Str.get(tb.finish())); // parse namespace declarations final boolean pr = startsWith(atn, XMLNSC); if(pr || eq(atn, XMLNS)) { if(!simple) throw error(NSCONS); final byte[] pref = pr ? local(atn) : EMPTY; final byte[] uri = attv.isEmpty() ? EMPTY : ((Str) attv.get(0)).string(); if(eq(pref, XML) && eq(uri, XML_URI)) { if(xmlDecl) throw error(DUPLNSDEF_X, XML); xmlDecl = true; } else { if(!Uri.uri(uri).isValid()) throw error(INVURI_X, uri); if(pr) { if(uri.length == 0) throw error(NSEMPTYURI); if(eq(pref, XML, XMLNS)) throw error(BINDXML_X, pref); if(eq(uri, XML_URI)) throw error(BINDXMLURI_X_X, uri, XML); if(eq(uri, XMLNS_URI)) throw error(BINDXMLURI_X_X, uri, XMLNS); sc.ns.add(pref, uri); } else { if(eq(uri, XML_URI)) throw error(XMLNSDEF_X, uri); sc.elemNS = uri; } if(ns.contains(pref)) throw error(DUPLNSDEF_X, pref); ns.add(pref, uri); } } else { final QNm attn = new QNm(atn); if(atts == null) atts = new ArrayList<>(1); atts.add(attn); qnames.add(attn, false, info()); add(cont, new CAttr(sc, info(), false, attn, attv.finish())); } if(!consumeWS()) break; } if(consume('/')) { check('>'); } else { check('>'); while(curr() != '<' || next() != '/') { final Expr e = dirElemContent(name.string()); if(e != null) add(cont, e); } pos += 2; final byte[] close = qName(ELEMNAME_X); consumeWS(); check('>'); if(!eq(name.string(), close)) throw error(TAGWRONG_X_X, name.string(), close); } qnames.assignURI(this, npos); // check for duplicate attribute names if(atts != null) { final int as = atts.size(); for(int a = 0; a < as - 1; a++) { for(int b = a + 1; b < as; b++) { if(atts.get(a).eq(atts.get(b))) throw error(ATTDUPL_X, atts.get(a)); } } } sc.ns.size(s); sc.elemNS = nse; return new CElem(sc, info(), name, ns, cont.finish()); } /** * Parses the "DirElemContent" rule. * @param name name of opening element * @return query expression * @throws QueryException query exception */ private Expr dirElemContent(final byte[] name) throws QueryException { final TokenBuilder tb = new TokenBuilder(); boolean strip = true; do { final char c = curr(); if(c == '<') { if(wsConsume(CDATA)) { tb.add(cDataSection()); strip = false; } else { final Str txt = text(tb, strip); return txt != null ? txt : next() == '/' ? null : dirConstructor(); } } else if(c == '{') { if(next() == '{') { tb.add(consume()); consume(); } else { final Str txt = text(tb, strip); return txt != null ? txt : enclosedExpr(); } } else if(c == '}') { consume(); check('}'); tb.add('}'); } else if(c != 0) { strip &= !entity(tb); } else { throw error(NOCLOSING_X, name); } } while(true); } /** * Returns a string item. * @param tb token builder * @param strip strip flag * @return string item or {@code null} */ private Str text(final TokenBuilder tb, final boolean strip) { final byte[] t = tb.toArray(); return t.length == 0 || strip && !sc.spaces && ws(t) ? null : Str.get(t); } /** * Parses the "DirCommentConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr dirComment() throws QueryException { check('-'); check('-'); final TokenBuilder tb = new TokenBuilder(); do { final char ch = consumeContent(); if(ch == '-' && consume('-')) { check('>'); return new CComm(sc, info(), Str.get(tb.finish())); } tb.add(ch); } while(true); } /** * Parses the "DirPIConstructor" rule. * Parses the "DirPIContents" rule. * @return query expression * @throws QueryException query exception */ private Expr dirPI() throws QueryException { final byte[] str = ncName(INVALPI); if(eq(lc(str), XML)) throw error(PIXML_X, str); final boolean space = skipWs(); final TokenBuilder tb = new TokenBuilder(); do { final char ch = consumeContent(); if(ch == '?' && consume('>')) { return new CPI(sc, info(), Str.get(str), Str.get(tb.finish())); } if(!space) throw error(PIWRONG); tb.add(ch); } while(true); } /** * Parses the "CDataSection" rule. * @return CData * @throws QueryException query exception */ private byte[] cDataSection() throws QueryException { final TokenBuilder tb = new TokenBuilder(); while(true) { final char ch = consumeContent(); if(ch == ']' && curr(']') && next() == '>') { pos += 2; return tb.finish(); } tb.add(ch); } } /** * Parses the "ComputedConstructor" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr compConstructor() throws QueryException { final int i = pos; if(wsConsumeWs(DOCUMENT)) return consume(compDoc(), i); if(wsConsumeWs(ELEMENT)) return consume(compElement(), i); if(wsConsumeWs(ATTRIBUTE)) return consume(compAttribute(), i); if(wsConsumeWs(NAMESPACE)) return consume(compNamespace(), i); if(wsConsumeWs(TEXT)) return consume(compText(), i); if(wsConsumeWs(COMMENT)) return consume(compComment(), i); if(wsConsumeWs(PI)) return consume(compPI(), i); return null; } /** * Consumes the specified expression or resets the query position. * @param expr expression * @param p query position * @return expression or {@code null} */ private Expr consume(final Expr expr, final int p) { if(expr == null) pos = p; return expr; } /** * Parses the "CompDocConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr compDoc() throws QueryException { return curr('{') ? new CDoc(sc, info(), enclosedExpr()) : null; } /** * Parses the "CompElemConstructor" rule. * Parses the "ContextExpr" rule. * @return query expression * @throws QueryException query exception */ private Expr compElement() throws QueryException { skipWs(); final Expr name; final InputInfo info = info(); final QNm qn = eQName(null, SKIPCHECK); if(qn != null) { name = qn; qnames.add(qn, info); } else { if(!wsConsume(CURLY1)) return null; name = check(expr(), NOELEMNAME); wsCheck(CURLY2); } skipWs(); return curr('{') ? new CElem(sc, info(), name, null, enclosedExpr()) : null; } /** * Parses the "CompAttrConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr compAttribute() throws QueryException { skipWs(); final Expr name; final InputInfo info = info(); final QNm qn = eQName(null, SKIPCHECK); if(qn != null) { name = qn; qnames.add(qn, false, info); } else { if(!wsConsume(CURLY1)) return null; name = check(expr(), NOATTNAME); wsCheck(CURLY2); } skipWs(); return curr('{') ? new CAttr(sc, info(), true, name, enclosedExpr()) : null; } /** * Parses the "CompNamespaceConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr compNamespace() throws QueryException { skipWs(); final Expr name; final byte[] str = ncName(null); if(str.length == 0) { if(!curr('{')) return null; name = enclosedExpr(); } else { name = Str.get(str); } skipWs(); return curr('{') ? new CNSpace(sc, info(), name, enclosedExpr()) : null; } /** * Parses the "CompTextConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr compText() throws QueryException { return curr('{') ? new CTxt(sc, info(), enclosedExpr()) : null; } /** * Parses the "CompCommentConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr compComment() throws QueryException { return curr('{') ? new CComm(sc, info(), enclosedExpr()) : null; } /** * Parses the "CompPIConstructor" rule. * @return query expression * @throws QueryException query exception */ private Expr compPI() throws QueryException { skipWs(); final Expr name; final byte[] str = ncName(null); if(str.length == 0) { if(!wsConsume(CURLY1)) return null; name = check(expr(), PIWRONG); wsCheck(CURLY2); } else { name = Str.get(str); } skipWs(); return curr('{') ? new CPI(sc, info(), name, enclosedExpr()) : null; } /** * Parses the "SimpleType" rule. * @return sequence type * @throws QueryException query exception */ private SeqType simpleType() throws QueryException { skipWs(); final QNm name = eQName(TYPEINVALID, sc.elemNS); Type t = ListType.find(name); if(t == null) { t = AtomType.find(name, false); if(t == null) { if(wsConsume(PAREN1)) throw error(SIMPLETYPE_X, name); if(!AtomType.AST.name.eq(name)) throw error(TYPE30_X, name.prefixId(XML)); t = AtomType.AST; } if(t == AtomType.AST || t == AtomType.AAT || t == AtomType.NOT) throw error(CASTUNKNOWN_X, name.prefixId(XML)); } skipWs(); return SeqType.get(t, consume('?') ? Occ.ZERO_ONE : Occ.ONE); } /** * Parses the "SequenceType" rule. * Parses the "OccurrenceIndicator" rule. * Parses the "KindTest" rule. * @return sequence type * @throws QueryException query exception */ private SeqType sequenceType() throws QueryException { // empty sequence if(wsConsumeWs(EMPTY_SEQUENCE, PAREN1, null)) { wsCheck(PAREN1); wsCheck(PAREN2); return SeqType.EMP; } // parse item type and occurrence indicator final SeqType tw = itemType(); skipWs(); final Occ occ = consume('?') ? Occ.ZERO_ONE : consume('+') ? Occ.ONE_MORE : consume('*') ? Occ.ZERO_MORE : Occ.ONE; skipWs(); return tw.withOcc(occ); } /** * Parses the "ItemType" rule. * Parses the "ParenthesizedItemType" rule. * @return item type * @throws QueryException query exception */ private SeqType itemType() throws QueryException { skipWs(); // parenthesized item type if(consume(PAREN1)) { final SeqType type = itemType(); wsCheck(PAREN2); return type; } // parse optional annotation and type name final AnnList anns = annotations(false).check(false); final QNm name = eQName(TYPEINVALID, null); skipWs(); // check if name is followed by parentheses final boolean func = curr('('); // item type Type t = null; if(func) { consume(PAREN1); // item type if(name.eq(AtomType.ITEM.name)) t = AtomType.ITEM; // node types if(t == null) t = NodeType.find(name); // function types if(t == null) { t = FuncType.find(name); if(t != null) return functionTest(anns, t).seqType(); } // no type found if(t == null) throw error(WHICHTYPE_X, name.prefixId(XML)); } else { // attach default element namespace if(!name.hasURI()) name.uri(sc.elemNS); // atomic types t = AtomType.find(name, false); // no type found if(t == null) throw error(TYPEUNKNOWN_X, name.prefixId(XML)); } // annotations are not allowed for remaining types if(!anns.isEmpty()) throw error(NOANN); // atomic value, or closing parenthesis if(!func || wsConsume(PAREN2)) return t.seqType(); // raise error if type different to node is not finalized by a parenthesis if(!(t instanceof NodeType)) wsCheck(PAREN2); // return type with an optional kind test for node types return SeqType.get(t, Occ.ONE, kindTest((NodeType) t)); } /** * Parses the "FunctionTest" rule. * @param anns annotations * @param t function type * @return resulting type * @throws QueryException query exception */ private Type functionTest(final AnnList anns, final Type t) throws QueryException { // wildcard if(wsConsume(ASTERISK)) { wsCheck(PAREN2); return t; } // map if(t instanceof MapType) { final Type key = itemType().type; if(!key.instanceOf(AtomType.AAT)) throw error(MAPTAAT_X, key); wsCheck(COMMA); final MapType tp = MapType.get((AtomType) key, sequenceType()); wsCheck(PAREN2); return tp; } // array if(t instanceof ArrayType) { final ArrayType tp = ArrayType.get(sequenceType()); wsCheck(PAREN2); return tp; } // function type SeqType[] args = { }; if(!wsConsume(PAREN2)) { // function has got arguments do args = Array.add(args, sequenceType()); while(wsConsume(COMMA)); wsCheck(PAREN2); } wsCheck(AS); return FuncType.get(anns, sequenceType(), args); } /** * Parses the "ElementTest" rule without the type name and the opening bracket. * @param t type * @return arguments * @throws QueryException query exception */ private Test kindTest(final NodeType t) throws QueryException { Test tp = null; switch(t) { case DOC: tp = documentTest(); break; case ELM: tp = elementTest(); break; case ATT: tp = attributeTest(); break; case PI: tp = piTest(); break; case SCE: case SCA: tp = schemaTest(); break; default: break; } wsCheck(PAREN2); return tp == null ? KindTest.get(t) : tp; } /** * Parses the "DocumentTest" rule without the leading keyword and its brackets. * @return arguments * @throws QueryException query exception */ private Test documentTest() throws QueryException { final boolean elem = consume(ELEMENT); if(!elem && !consume(SCHEMA_ELEMENT)) return null; wsCheck(PAREN1); skipWs(); final NodeTest t = elem ? elementTest() : schemaTest(); wsCheck(PAREN2); return new DocTest(t != null ? t : KindTest.ELM); } /** * Parses the "ElementTest" rule without the leading keyword and its brackets. * @return arguments * @throws QueryException query exception */ private NodeTest elementTest() throws QueryException { final QNm name = eQName(null, sc.elemNS); if(name == null && !consume(ASTERISK)) return null; Type type = null; if(wsConsumeWs(COMMA)) { // parse type name final QNm tn = eQName(QNAME_X, sc.elemNS); type = ListType.find(tn); if(type == null) type = AtomType.find(tn, true); if(type == null) throw error(TYPEUNDEF_X, tn); // parse optional question mark wsConsume(QUESTION); } return new NodeTest(NodeType.ELM, name, type); } /** * Parses the "ElementTest" rule without the leading keyword and its brackets. * @return arguments * @throws QueryException query exception */ private NodeTest schemaTest() throws QueryException { final QNm name = eQName(QNAME_X, sc.elemNS); throw error(SCHEMAINV_X, name); } /** * Parses the "AttributeTest" rule without the leading keyword and its brackets. * @return arguments * @throws QueryException query exception */ private Test attributeTest() throws QueryException { final QNm name = eQName(null, null); if(name == null && !consume(ASTERISK)) return null; Type type = null; if(wsConsumeWs(COMMA)) { // parse type name final QNm tn = eQName(QNAME_X, sc.elemNS); type = ListType.find(tn); if(type == null) type = AtomType.find(tn, true); if(type == null) throw error(TYPEUNDEF_X, tn); } return new NodeTest(NodeType.ATT, name, type); } /** * Parses the "PITest" rule without the leading keyword and its brackets. * @return arguments * @throws QueryException query exception */ private Test piTest() throws QueryException { tok.reset(); final byte[] nm; if(quote(curr())) { nm = trim(stringLiteral()); if(!XMLToken.isNCName(nm)) throw error(INVNCNAME_X, nm); } else if(ncName()) { nm = tok.toArray(); } else { return null; } return new NodeTest(NodeType.PI, new QNm(nm)); } /** * Parses the "TryCatch" rules. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr tryCatch() throws QueryException { if(!wsConsumeWs(TRY)) return null; final Expr expr = enclosedExpr(); wsCheck(CATCH); Catch[] catches = { }; do { NameTest[] codes = { }; do { skipWs(); final NameTest test = (NameTest) nodeTest(false, false); if(test == null) throw error(NOCATCH); codes = Array.add(codes, test); } while(wsConsumeWs(PIPE)); final int s = localVars.openScope(); final int cl = Catch.NAMES.length; final Var[] vs = new Var[cl]; final InputInfo ii = info(); for(int i = 0; i < cl; i++) { vs[i] = localVars.add(new Var(Catch.NAMES[i], Catch.TYPES[i], false, qc, sc, ii)); } final Catch c = new Catch(ii, codes, vs); c.expr = enclosedExpr(); localVars.closeScope(s); catches = Array.add(catches, c); } while(wsConsumeWs(CATCH)); return new Try(info(), expr, catches); } /** * Parses the "FTSelection" rules. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftSelection(final boolean prg) throws QueryException { FTExpr expr = ftOr(prg); FTExpr old; FTExpr first = null; boolean ordered = false; do { old = expr; if(wsConsumeWs(ORDERED)) { ordered = true; old = null; } else if(wsConsumeWs(WINDOW)) { expr = new FTWindow(info(), expr, additive(), ftUnit()); } else if(wsConsumeWs(DISTANCE)) { final Expr[] rng = ftRange(false); if(rng == null) throw error(FTRANGE); expr = new FTDistance(info(), expr, rng[0], rng[1], ftUnit()); } else if(wsConsumeWs(AT)) { final FTContents cont = wsConsumeWs(START) ? FTContents.START : wsConsumeWs(END) ? FTContents.END : null; if(cont == null) throw error(INCOMPLETE); expr = new FTContent(info(), expr, cont); } else if(wsConsumeWs(ENTIRE)) { wsCheck(CONTENT); expr = new FTContent(info(), expr, FTContents.ENTIRE); } else { final boolean same = wsConsumeWs(SAME); final boolean diff = !same && wsConsumeWs(DIFFERENT); if(same || diff) { final FTUnit unit; if(wsConsumeWs(SENTENCE)) unit = FTUnit.SENTENCES; else if(wsConsumeWs(PARAGRAPH)) unit = FTUnit.PARAGRAPHS; else throw error(INCOMPLETE); expr = new FTScope(info(), expr, same, unit); } } if(first == null && old != null && old != expr) first = expr; } while(old != expr); if(ordered) { if(first == null) return new FTOrder(info(), expr); first.exprs[0] = new FTOrder(info(), first.exprs[0]); } return expr; } /** * Parses the "FTOr" rule. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftOr(final boolean prg) throws QueryException { final FTExpr e = ftAnd(prg); if(!wsConsumeWs(FTOR)) return e; FTExpr[] list = { e }; do list = Array.add(list, ftAnd(prg)); while(wsConsumeWs(FTOR)); return new FTOr(info(), list); } /** * Parses the "FTAnd" rule. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftAnd(final boolean prg) throws QueryException { final FTExpr e = ftMildNot(prg); if(!wsConsumeWs(FTAND)) return e; FTExpr[] list = { e }; do list = Array.add(list, ftMildNot(prg)); while(wsConsumeWs(FTAND)); return new FTAnd(info(), list); } /** * Parses the "FTMildNot" rule. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftMildNot(final boolean prg) throws QueryException { final FTExpr e = ftUnaryNot(prg); if(!wsConsumeWs(NOT)) return e; FTExpr[] list = { }; do { wsCheck(IN); list = Array.add(list, ftUnaryNot(prg)); } while(wsConsumeWs(NOT)); // convert "A not in B not in ..." to "A not in(B or ...)" return new FTMildNot(info(), e, list.length == 1 ? list[0] : new FTOr( info(), list)); } /** * Parses the "FTUnaryNot" rule. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftUnaryNot(final boolean prg) throws QueryException { final boolean not = wsConsumeWs(FTNOT); final FTExpr e = ftPrimaryWithOptions(prg); return not ? new FTNot(info(), e) : e; } /** * Parses the "FTPrimaryWithOptions" rule. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftPrimaryWithOptions(final boolean prg) throws QueryException { FTExpr expr = ftPrimary(prg); final FTOpt fto = new FTOpt(); boolean found = false; while(ftMatchOption(fto)) found = true; // check if specified language is not available if(found) { if(fto.ln == null) fto.ln = Language.def(); if(!Tokenizer.supportFor(fto.ln)) throw error(FTNOTOK_X, fto.ln); if(fto.is(ST) && fto.sd == null && !Stemmer.supportFor(fto.ln)) throw error(FTNOSTEM_X, fto.ln); } // consume weight option if(wsConsumeWs(WEIGHT)) expr = new FTWeight(info(), expr, enclosedExpr()); // skip options if none were specified... return found ? new FTOptions(info(), expr, fto) : expr; } /** * Parses the "FTPrimary" rule. * @param prg pragma flag * @return query expression * @throws QueryException query exception */ private FTExpr ftPrimary(final boolean prg) throws QueryException { final Pragma[] pragmas = pragma(); if(pragmas != null) { wsCheck(CURLY1); final FTExpr e = ftSelection(true); wsCheck(CURLY2); return new FTExtensionSelection(info(), pragmas, e); } if(wsConsumeWs(PAREN1)) { final FTExpr e = ftSelection(false); wsCheck(PAREN2); return e; } skipWs(); final Expr e; if(quote(curr())) { e = Str.get(stringLiteral()); } else if(curr('{')) { e = enclosedExpr(); } else { throw error(prg ? NOPRAGMA : NOFTSELECT_X, found()); } // FTAnyAllOption FTMode mode = FTMode.ANY; if(wsConsumeWs(ALL)) { mode = wsConsumeWs(WORDS) ? FTMode.ALL_WORDS : FTMode.ALL; } else if(wsConsumeWs(ANY)) { mode = wsConsumeWs(WORD) ? FTMode.ANY_WORD : FTMode.ANY; } else if(wsConsumeWs(PHRASE)) { mode = FTMode.PHRASE; } // FTTimes Expr[] occ = null; if(wsConsumeWs(OCCURS)) { occ = ftRange(false); if(occ == null) throw error(FTRANGE); wsCheck(TIMES); } return new FTWords(info(), e, mode, occ); } /** * Parses the "FTRange" rule. * @param i accept only integers ("FTLiteralRange") * @return query expression or {@code null} * @throws QueryException query exception */ private Expr[] ftRange(final boolean i) throws QueryException { final Expr[] occ = { Int.get(0), Int.get(Long.MAX_VALUE) }; if(wsConsumeWs(EXACTLY)) { occ[0] = ftAdditive(i); occ[1] = occ[0]; } else if(wsConsumeWs(AT)) { if(wsConsumeWs(LEAST)) { occ[0] = ftAdditive(i); } else if(wsConsumeWs(MOST)) { occ[1] = ftAdditive(i); } else { return null; } } else if(wsConsumeWs(FROM)) { occ[0] = ftAdditive(i); wsCheck(TO); occ[1] = ftAdditive(i); } else { return null; } return occ; } /** * Returns an argument of the "FTRange" rule. * @param i accept only integers * @return query expression * @throws QueryException query exception */ private Expr ftAdditive(final boolean i) throws QueryException { if(!i) return additive(); skipWs(); tok.reset(); while(digit(curr())) tok.add(consume()); if(tok.isEmpty()) throw error(INTEXP); return Int.get(toLong(tok.toArray())); } /** * Parses the "FTUnit" rule. * @return query expression * @throws QueryException query exception */ private FTUnit ftUnit() throws QueryException { if(wsConsumeWs(WORDS)) return FTUnit.WORDS; if(wsConsumeWs(SENTENCES)) return FTUnit.SENTENCES; if(wsConsumeWs(PARAGRAPHS)) return FTUnit.PARAGRAPHS; throw error(INCOMPLETE); } /** * Parses the "FTMatchOption" rule. * @param opt options instance * @return false if no options were found * @throws QueryException query exception */ private boolean ftMatchOption(final FTOpt opt) throws QueryException { if(!wsConsumeWs(USING)) return false; if(wsConsumeWs(LOWERCASE)) { if(opt.cs != null) throw error(FTDUP_X, CASE); opt.cs = FTCase.LOWER; } else if(wsConsumeWs(UPPERCASE)) { if(opt.cs != null) throw error(FTDUP_X, CASE); opt.cs = FTCase.UPPER; } else if(wsConsumeWs(CASE)) { if(opt.cs != null) throw error(FTDUP_X, CASE); if(wsConsumeWs(SENSITIVE)) { opt.cs = FTCase.SENSITIVE; } else { opt.cs = FTCase.INSENSITIVE; wsCheck(INSENSITIVE); } } else if(wsConsumeWs(DIACRITICS)) { if(opt.isSet(DC)) throw error(FTDUP_X, DIACRITICS); opt.set(DC, wsConsumeWs(SENSITIVE)); if(!opt.is(DC)) wsCheck(INSENSITIVE); } else if(wsConsumeWs(LANGUAGE)) { if(opt.ln != null) throw error(FTDUP_X, LANGUAGE); final byte[] lan = stringLiteral(); opt.ln = Language.get(string(lan)); if(opt.ln == null) throw error(FTNOTOK_X, lan); } else if(wsConsumeWs(OPTION)) { optionDecl(); } else { final boolean using = !wsConsumeWs(NO); if(wsConsumeWs(STEMMING)) { if(opt.isSet(ST)) throw error(FTDUP_X, STEMMING); opt.set(ST, using); } else if(wsConsumeWs(THESAURUS)) { if(opt.th != null) throw error(FTDUP_X, THESAURUS); opt.th = new ThesQuery(); if(using) { final boolean par = wsConsume(PAREN1); if(!wsConsumeWs(DEFAULT)) ftThesaurusID(opt.th); while(par && wsConsume(COMMA)) ftThesaurusID(opt.th); if(par) wsCheck(PAREN2); } } else if(wsConsumeWs(STOP)) { // add union/except wsCheck(WORDS); if(opt.sw != null) throw error(FTDUP_X, STOP + ' ' + WORDS); opt.sw = new StopWords(); if(wsConsumeWs(DEFAULT)) { if(!using) throw error(FTSTOP); } else { boolean union = false, except = false; while(using) { if(wsConsume(PAREN1)) { do { final byte[] sl = stringLiteral(); if(except) opt.sw.delete(sl); else if(!union || !opt.sw.contains(sl)) opt.sw.add(sl); } while(wsConsume(COMMA)); wsCheck(PAREN2); } else if(wsConsumeWs(AT)) { final String fn = string(stringLiteral()); // optional: resolve URI reference final IO fl = qc.resources.stopWords(fn, sc); try { opt.sw.read(fl, except); } catch(final IOException ex) { Util.debug(ex); throw error(NOSTOPFILE_X, fl); } } else if(!union && !except) { throw error(FTSTOP); } union = wsConsumeWs(UNION); except = !union && wsConsumeWs(EXCEPT); if(!union && !except) break; } } } else if(wsConsumeWs(WILDCARDS)) { if(opt.isSet(WC)) throw error(FTDUP_X, WILDCARDS); if(opt.is(FZ)) throw error(BXFT_MATCH); opt.set(WC, using); } else if(wsConsumeWs(FUZZY)) { // extension to the official extension: "using fuzzy" if(opt.isSet(FZ)) throw error(FTDUP_X, FUZZY); if(opt.is(WC)) throw error(BXFT_MATCH); opt.set(FZ, using); } else { throw error(FTMATCH_X, consume()); } } return true; } /** * Parses the "FTThesaurusID" rule. * @param thes link to thesaurus * @throws QueryException query exception */ private void ftThesaurusID(final ThesQuery thes) throws QueryException { wsCheck(AT); final String fn = string(stringLiteral()); // optional: resolve URI reference final IO fl = qc.resources.thesaurus(fn, sc); final byte[] rel = wsConsumeWs(RELATIONSHIP) ? stringLiteral() : EMPTY; final Expr[] range = ftRange(true); long min = 0; long max = Long.MAX_VALUE; if(range != null) { wsCheck(LEVELS); // values will always be integer instances min = ((ANum) range[0]).itr(); max = ((ANum) range[1]).itr(); } thes.add(new Thesaurus(fl, rel, min, max, qc.context)); } /** * Parses the "InsertExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr insert() throws QueryException { final int i = pos; if(!wsConsumeWs(INSERT) || !wsConsumeWs(NODE) && !wsConsumeWs(NODES)) { pos = i; return null; } final Expr s = check(single(), INCOMPLETE); boolean first = false; boolean last = false; boolean before = false; boolean after = false; if(wsConsumeWs(AS)) { first = wsConsumeWs(FIRST); if(!first) { wsCheck(LAST); last = true; } wsCheck(INTO); } else if(!wsConsumeWs(INTO)) { after = wsConsumeWs(AFTER); before = !after && wsConsumeWs(BEFORE); if(!after && !before) throw error(INCOMPLETE); } final Expr trg = check(single(), INCOMPLETE); qc.updating(); return new Insert(sc, info(), s, first, last, before, after, trg); } /** * Parses the "DeleteExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr delete() throws QueryException { final int i = pos; if(!wsConsumeWs(DELETE) || !wsConsumeWs(NODES) && !wsConsumeWs(NODE)) { pos = i; return null; } qc.updating(); return new Delete(sc, info(), check(single(), INCOMPLETE)); } /** * Parses the "RenameExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr rename() throws QueryException { final int i = pos; if(!wsConsumeWs(RENAME) || !wsConsumeWs(NODE)) { pos = i; return null; } final Expr trg = check(single(), INCOMPLETE); wsCheck(AS); final Expr n = check(single(), INCOMPLETE); qc.updating(); return new Rename(sc, info(), trg, n); } /** * Parses the "ReplaceExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr replace() throws QueryException { final int i = pos; if(!wsConsumeWs(REPLACE)) return null; final boolean v = wsConsumeWs(VALUEE); if(v) { wsCheck(OF); wsCheck(NODE); } else if(!wsConsumeWs(NODE)) { pos = i; return null; } final Expr t = check(single(), INCOMPLETE); wsCheck(WITH); final Expr r = check(single(), INCOMPLETE); qc.updating(); return new Replace(sc, info(), t, r, v); } /** * Parses the "CopyModifyExpr" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr copyModify() throws QueryException { if(!wsConsumeWs(COPY, DOLLAR, INCOMPLETE)) return null; final int s = localVars.openScope(); Let[] fl = { }; do { final Var var = newVar(SeqType.NOD); wsCheck(ASSIGN); final Expr e = check(single(), INCOMPLETE); fl = Array.add(fl, new Let(localVars.add(var), e, false)); } while(wsConsumeWs(COMMA)); wsCheck(MODIFY); final InputInfo ii = info(); final Expr m = check(single(), INCOMPLETE); wsCheck(RETURN); final Expr r = check(single(), INCOMPLETE); localVars.closeScope(s); qc.updating(); return new Transform(ii, fl, m, r); } /** * Parses the "UpdatingFunctionCall" rule. * @return query expression or {@code null} * @throws QueryException query exception */ private Expr updatingFunctionCall() throws QueryException { final int p = pos; wsConsume(INVOKE); final boolean upd = wsConsumeWs(UPDATING), ndt = wsConsumeWs(NON_DETERMINISTIC); if(upd || ndt) { final Expr func = primary(); if(wsConsume(PAREN1)) { final InputInfo ii = info(); final ExprList argList = new ExprList(); if(!wsConsume(PAREN2)) { do { final Expr e = single(); if(e == null) throw funcMiss(func); argList.add(e); } while(wsConsume(COMMA)); if(!wsConsume(PAREN2)) throw funcMiss(func); } // skip if primary expression cannot be a function if(func instanceof Value && !(func instanceof FItem)) throw error(NOPAREN_X_X, func); if(upd) qc.updating(); return new DynFuncCall(ii, sc, upd, ndt, func, argList.finish()); } } pos = p; return null; } /** * Parses the "NCName" rule. * @param err optional error message * @return string * @throws QueryException query exception */ private byte[] ncName(final QueryError err) throws QueryException { tok.reset(); if(ncName()) return tok.toArray(); if(err != null) { final char ch = consume(); throw error(err, ch == 0 ? "" : ch); } return EMPTY; } /** * Parses the "EQName" rule. * @param err optional error message. Will be thrown if no EQName is found, or ignored if set to * {@code null} * @param def default namespace, or operation mode ({@link #URICHECK}, {@link #SKIPCHECK}) * @return QName or {@code null} * @throws QueryException query exception */ private QNm eQName(final QueryError err, final byte[] def) throws QueryException { final int i = pos; if(consume(EQNAME)) { final byte[] uri = bracedURILiteral(); final byte[] name = ncName(null); if(name.length != 0) { if(def == URICHECK && uri.length == 0) { pos = i; throw error(NOURI_X, name); } return new QNm(name, uri); } pos = i; } // parse QName final byte[] nm = qName(err); if(nm.length == 0) return null; if(def == SKIPCHECK) return new QNm(nm); // create new EQName and set namespace final QNm name = new QNm(nm, sc); if(!name.hasURI()) { if(def == URICHECK) { pos = i; throw error(NSMISS_X, name); } if(name.hasPrefix()) { pos = i; throw error(NOURI_X, name.string()); } name.uri(def); } return name; } /** * Parses the "QName" rule. * @param err optional error message. Will be thrown if no QName is found, and ignored if set to * {@code null} * @return QName string * @throws QueryException query exception */ private byte[] qName(final QueryError err) throws QueryException { tok.reset(); if(!ncName()) { if(err != null) { final char ch = consume(); throw error(err, ch == 0 ? "" : ch); } } else if(consume(':')) { if(XMLToken.isNCStartChar(curr())) { tok.add(':'); do { tok.add(consume()); } while(XMLToken.isNCChar(curr())); } else { --pos; } } return tok.toArray(); } /** * Helper method for parsing NCNames. * @return true for success */ private boolean ncName() { if(!XMLToken.isNCStartChar(curr())) return false; do tok.add(consume()); while(XMLToken.isNCChar(curr())); return true; } /** * Parses and converts entities. * @param tb token builder * @return true if an entity was found * @throws QueryException query exception */ private boolean entity(final TokenBuilder tb) throws QueryException { final int i = pos; final boolean ent = consume('&'); if(ent) { if(consume('#')) { final int b = consume('x') ? 0x10 : 10; boolean ok = true; int n = 0; do { final char c = curr(); final boolean m = digit(c); final boolean h = b == 0x10 && (c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'); if(!m && !h) entityError(i, INVENTITY_X); final long nn = n; n = n * b + (consume() & 0xF); if(n < nn) ok = false; if(!m) n += 9; } while(!consume(';')); if(!ok) entityError(i, INVCHARREF_X); if(!XMLToken.valid(n)) entityError(i, INVCHARREF_X); tb.add(n); } else { if(consume("lt")) { tb.add('<'); } else if(consume("gt")) { tb.add('>'); } else if(consume("amp")) { tb.add('&'); } else if(consume("quot")) { tb.add('"'); } else if(consume("apos")) { tb.add('\''); } else { entityError(i, INVENTITY_X); } if(!consume(';')) entityError(i, INVENTITY_X); } } else { final char c = consume(); int ch = c; if(Character.isHighSurrogate(c) && curr() != 0 && Character.isLowSurrogate(curr())) { ch = Character.toCodePoint(c, consume()); } if(ch == '\r') { ch = '\n'; if(curr(ch)) consume(); } tb.add(ch); } return ent; } /** * Raises an entity error. * @param start start position * @param code error code * @throws QueryException query exception */ private void entityError(final int start, final QueryError code) throws QueryException { final String sub = input.substring(start, Math.min(start + 20, length)); final int semi = sub.indexOf(';'); final String ent = semi == -1 ? sub + DOTS : sub.substring(0, semi + 1); throw error(code, ent); } /** * Raises an error if the specified expression is {@code null}. * @param <E> expression type * @param expr expression * @param err error message * @return expression * @throws QueryException query exception */ private <E extends Expr> E check(final E expr, final QueryError err) throws QueryException { if(expr == null) throw error(err); return expr; } /** * Raises an error if the specified character cannot be consumed. * @param ch character to be found * @throws QueryException query exception */ private void check(final int ch) throws QueryException { if(!consume(ch)) throw error(WRONGCHAR_X_X, (char) ch, found()); } /** * Skips whitespaces, raises an error if the specified string cannot be consumed. * @param s string to be found * @throws QueryException query exception */ private void wsCheck(final String s) throws QueryException { if(!wsConsume(s)) throw error(WRONGCHAR_X_X, s, found()); } /** * Consumes the next character and normalizes new line characters. * @return next character * @throws QueryException query exception */ private char consumeContent() throws QueryException { char ch = consume(); if(ch == 0) throw error(WRONGCHAR_X_X, ch, found()); if(ch == '\r') { ch = '\n'; consume('\n'); } return ch; } /** * Consumes the specified token and surrounding whitespaces. * @param t token to consume * @return true if token was found * @throws QueryException query exception */ private boolean wsConsumeWs(final String t) throws QueryException { final int i = pos; if(!wsConsume(t)) return false; if(skipWs() || !XMLToken.isNCStartChar(t.charAt(0)) || !XMLToken.isNCChar(curr())) return true; pos = i; return false; } /** * Consumes the specified two strings or jumps back to the old query position. If the strings are * found, the cursor is placed after the first token. * @param s1 string to be consumed * @param s2 second string * @param expr alternative error message (can be {@code null}) * @return result of check * @throws QueryException query exception */ private boolean wsConsumeWs(final String s1, final String s2, final QueryError expr) throws QueryException { final int i1 = pos; if(!wsConsumeWs(s1)) return false; final int i2 = pos; alter = expr; alterPos = i2; final boolean ok = wsConsume(s2); pos = ok ? i2 : i1; return ok; } /** * Skips whitespaces, consumes the specified string and ignores trailing characters. * @param str string to consume * @return true if string was found * @throws QueryException query exception */ private boolean wsConsume(final String str) throws QueryException { skipWs(); return consume(str); } /** * Consumes all whitespace characters from the remaining query. * @return true if whitespaces were found * @throws QueryException query exception */ private boolean skipWs() throws QueryException { final int i = pos; while(more()) { final int c = curr(); if(c == '(' && next() == ':') { comment(); } else { if(c <= 0 || c > ' ') return i != pos; ++pos; } } return i != pos; } /** * Consumes a comment. * @throws QueryException query exception */ private void comment() throws QueryException { ++pos; final boolean xqdoc = next() == '~'; if(xqdoc) { currDoc.setLength(0); ++pos; } comment(false, xqdoc); } /** * Consumes a comment. * @param nested nested flag * @param xqdoc xqdoc flag * @throws QueryException query exception */ private void comment(final boolean nested, final boolean xqdoc) throws QueryException { while(++pos < length) { char curr = curr(); if(curr == '(' && next() == ':') { ++pos; comment(true, xqdoc); curr = curr(); } if(curr == ':' && next() == ')') { pos += 2; if(!nested && doc.isEmpty()) { doc = currDoc.toString().trim(); currDoc.setLength(0); } return; } if(xqdoc) currDoc.append(curr); } throw error(COMCLOSE); } /** * Consumes all following whitespace characters. * @return true if whitespaces were found */ private boolean consumeWS() { final int i = pos; while(more()) { final int c = curr(); if(c <= 0 || c > ' ') return i != pos; ++pos; } return true; } /** * Creates an alternative error. * @return error */ private QueryException error() { pos = alterPos; if(alter != WHICHFUNC_X) return error(alter); final QueryException qe = qc.funcs.similarError(alterFunc, info()); return qe == null ? error(alter, alterFunc.string()) : qe; } /** * Adds an expression to the specified array. * @param ar input array * @param ex new expression * @throws QueryException query exception */ private void add(final ExprList ar, final Expr ex) throws QueryException { if(ex == null) throw error(INCOMPLETE); ar.add(ex); } /** * Creates a function parsing error. * @param func function * @return error */ private QueryException funcMiss(final Object func) { Object name = func; if(func instanceof XQFunctionExpr) { final QNm qnm = ((XQFunctionExpr) func).funcName(); if(qnm != null) name = qnm.prefixId(); } return error(FUNCMISS_X, name); } /** * Creates the specified error. * @param err error to be thrown * @param arg error arguments * @return error */ private QueryException error(final QueryError err, final Object... arg) { return error(err, info(), arg); } /** * Creates the specified error. * @param err error to be thrown * @param info input info * @param arg error arguments * @return error */ public QueryException error(final QueryError err, final InputInfo info, final Object... arg) { return err.get(info, arg); } }