diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index 30ec8b2ae3f..fef482dc750 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -180,6 +180,7 @@ imaginaryTokenDefinitions PRAGMA GTEQ SEQUENCE + INSERT_TARGET ; // === XPointer === @@ -238,7 +239,7 @@ moduleDecl throws XPathException // === Prolog === prolog throws XPathException -{ boolean inSetters = true; } +{ boolean inSetters = true; boolean redeclaration = false; } : ( ( @@ -263,8 +264,17 @@ prolog throws XPathException ( "declare" "context" "item" ) => contextItemDeclUp { inSetters = false; } | - ( "declare" MOD ) + // bare keyword updating is valid because of rule CompatibilityAnnotation in the XQUF standard + ( "declare" (MOD | "updating") ) => annotateDecl { inSetters = false; } + | + ( "declare" "revalidation" ) + => revalidationDecl { + inSetters = false; + if(redeclaration) + throw new XPathException((XQueryAST) returnAST, ErrorCodes.XUST0003, "It is a static error if a Prolog contains more than one revalidation declaration."); + redeclaration = true; + } ) SEMICOLON! )* @@ -456,6 +466,17 @@ annotation : MOD! name=eqName! (LPAREN! literal (COMMA! literal)* RPAREN!)? { #annotation= #(#[ANNOT_DECL, name], #annotation); } + + | "updating"! + { + name = "updating"; + #annotation= #(#[ANNOT_DECL, name], #annotation); + } + ; + +revalidationDecl throws XPathException +: + "declare"! "revalidation"^ ("strict" | "lax" | "skip") ; eqName returns [String name] @@ -576,7 +597,7 @@ itemType throws XPathException : ( "item" LPAREN ) => "item"^ LPAREN! RPAREN! | - ( "function" LPAREN ) => functionTest + ( ("function" LPAREN) | ( MOD ) ) => functionTest | ( "map" LPAREN ) => mapType | @@ -611,20 +632,23 @@ atomicType throws XPathException functionTest throws XPathException : - ( "function" LPAREN STAR RPAREN) => anyFunctionTest - | - typedFunctionTest + annotations + ( + ( "function" LPAREN STAR RPAREN ) => anyFunctionTest + | + typedFunctionTest + ) ; anyFunctionTest throws XPathException : - "function"! LPAREN! s:STAR RPAREN! + annotations "function"! LPAREN! s:STAR RPAREN! { #anyFunctionTest = #(#[FUNCTION_TEST, "anyFunction"], #s); } ; typedFunctionTest throws XPathException : - "function"! LPAREN! (sequenceType (COMMA! sequenceType)*)? RPAREN! "as" sequenceType + annotations "function"! LPAREN! (sequenceType (COMMA! sequenceType)*)? RPAREN! "as" sequenceType { #typedFunctionTest = #(#[FUNCTION_TEST, "anyFunction"], #typedFunctionTest); } ; @@ -699,6 +723,12 @@ exprSingle throws XPathException | ( "switch" LPAREN ) => switchExpr | ( "typeswitch" LPAREN ) => typeswitchExpr | ( "update" ( "replace" | "value" | "insert" | "delete" | "rename" )) => updateExpr + | ( "insert" ( "node" | "nodes" ) ) => xqufInsertExpr + | ( "delete" ( "node" | "nodes" ) ) => xqufDeleteExpr + | ( "replace" ( "value" | "node" ) ) => xqufReplaceExpr + | ( "rename" "node" ) => xqufRenameExpr + | ( "copy" DOLLAR ) => copyModifyExpr + | ( "invoke" "updating" ) => dynamicUpdFunCall | orExpr ; @@ -742,6 +772,50 @@ renameExpr throws XPathException "rename" exprSingle "as"! exprSingle ; +xqufInsertExpr throws XPathException +: + "insert"^ ( "node"! | "nodes"! ) exprSingle + insertExprTargetChoice exprSingle + ; + +insertExprTargetChoice throws XPathException +{ String target = null; } +: + ( + ( ( "as"! ( "first"! { target = "first"; } | "last"! { target = "last"; } ) )? "into"! { + if (target == null) + target = "into"; + } ) + | "after"! { target = "after"; } + | "before"! { target = "before"; } + ) + { #insertExprTargetChoice= #(#[INSERT_TARGET, target]); } + +; + +xqufDeleteExpr throws XPathException +: + "delete"^ ( "node"! | "nodes"! ) exprSingle + ; + +xqufReplaceExpr throws XPathException +: + "replace"^ ("value" "of"!)? "node"! exprSingle "with"! exprSingle + ; + +xqufRenameExpr throws XPathException +: + "rename"^ "node"! exprSingle "as"! exprSingle + ; + +copyModifyExpr throws XPathException +: + "copy"^ letVarBinding ( COMMA! letVarBinding )* + "modify"! exprSingle + "return"! exprSingle + ; + + // === try/catch === tryCatchExpr throws XPathException : @@ -1007,7 +1081,7 @@ castableExpr throws XPathException castExpr throws XPathException : - arrowExpr ( "cast"^ "as"! singleType )? + transformWithExpr ( "cast"^ "as"! singleType )? ; comparisonExpr throws XPathException @@ -1276,11 +1350,27 @@ postfixExpr throws XPathException )* ; +dynamicUpdFunCall throws XPathException +: + "invoke"! "updating"^ primaryExpr ( argumentList )* + ; + arrowExpr throws XPathException : unaryExpr ( ARROW_OP^ arrowFunctionSpecifier argumentList )* ; + +// This is not perfectly adherent to the standard grammar +// at https://www.w3.org/TR/xquery-31/#prod-xquery31-ArrowExpr +// but the standard XQuery 3.1 grammar conflicts with the XQuery Update Facility 3.0 grammar +// https://www.w3.org/TR/xquery-update-30/#prod-xquery30-TransformWithExpr +// However, the end behavior should be identical +transformWithExpr throws XPathException +: + arrowExpr ( "transform"^ "with"! LCURLY! ( expr )? RCURLY! )? + ; + arrowFunctionSpecifier throws XPathException { String name= null; } : @@ -2224,6 +2314,30 @@ reservedKeywords returns [String name] "map" { name = "map"; } | "array" { name = "array"; } + | + "updating" { name = "updating"; } + | + "revalidation" { name = "revalidation"; } + | + "strict" { name = "strict"; } + | + "lax" { name = "lax"; } + | + "skip" { name = "skip"; } + | + "transform" { name = "transform"; } + | + "invoke" { name = "invoke"; } + | + "nodes" { name = "nodes"; } + | + "first" { name = "first"; } + | + "last" { name = "last"; } + | + "after" { name = "after"; } + | + "before" { name = "before"; } ; /** diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index d1bea32bfdc..d45d1eda559 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -146,7 +146,7 @@ options { String ns = qname.getNamespaceURI(); if (ns.equals(Namespaces.XPATH_FUNCTIONS_NS)) { String ln = qname.getLocalPart(); - return ("private".equals(ln) || "public".equals(ln)); + return ("private".equals(ln) || "public".equals(ln) || "updating".equals(ln) || "simple".equals(ln)); } else { return !(ns.equals(Namespaces.XML_NS) || ns.equals(Namespaces.SCHEMA_NS) @@ -156,8 +156,36 @@ options { } } + private static Annotation[] processAnnotations(List annots) { + Annotation[] anns = new Annotation[annots.size()]; + + //iterate the declare Annotations + for(int i = 0; i < anns.length; i++) { + List la = (List)annots.get(i); + + //extract the Value for the Annotation + LiteralValue[] aValue; + if(la.size() > 1) { + + PathExpr aPath = (PathExpr)la.get(1); + + aValue = new LiteralValue[aPath.getSubExpressionCount()]; + for(int j = 0; j < aValue.length; j++) { + aValue[j] = (LiteralValue)aPath.getExpression(j); + } + } else { + aValue = new LiteralValue[0]; + } + + Annotation a = new Annotation((QName)la.get(0), aValue, null); + anns[i] = a; + } + + return anns; + } + private static void processAnnotations(List annots, FunctionSignature signature) { - Annotation[] anns = new Annotation[annots.size()]; + Annotation[] anns = new Annotation[annots.size()]; //iterate the declare Annotations for(int i = 0; i < anns.length; i++) { @@ -497,6 +525,24 @@ throws PermissionDeniedException, EXistException, XPathException { List annots = new ArrayList(); } (annotations [annots] )? + { + for(int i = 0; i < annots.size(); i++) + { + List la = (List) annots.get(i); + if(la.size() > 0) + { + for(int a = 0; a < la.size(); a++) + { + if(la.get(a).toString().equals("simple") || la.get(a).toString().equals("updating")) + { + throw new XPathException(qname, ErrorCodes.XUST0032, + "It is a static error if an %updating or %simple annotation is used on a VarDecl."); + } + } + } + } + + } ( #( "as" @@ -612,6 +658,27 @@ throws PermissionDeniedException, EXistException, XPathException functionDecl [path] | importDecl [path] + | + #( + "revalidation" + ( + "strict" + { + staticContext.setRevalidationMode(XQueryContext.RevalidationMode.STRICT); + } + | + "lax" + { + staticContext.setRevalidationMode(XQueryContext.RevalidationMode.LAX); + } + | + "skip" + { + staticContext.setRevalidationMode(XQueryContext.RevalidationMode.SKIP); + } + ) + + ) )* ; @@ -1048,6 +1115,12 @@ throws XPathException } ) | + { List annots = new ArrayList(); } + (annotations [annots])? + { + Annotation[] anns = processAnnotations(annots); + type.setAnnotations(anns); + } #( FUNCTION_TEST { type.setPrimaryType(Type.FUNCTION_REFERENCE); } ( @@ -2065,6 +2138,20 @@ throws PermissionDeniedException, EXistException, XPathException step=numericExpr [path] | step=updateExpr [path] + | + step=xqufInsertExpr [path] + | + step=xqufDeleteExpr [path] + | + step=xqufReplaceExpr [path] + | + step=xqufRenameExpr [path] + | + step=transformWithExpr [path] + | + step=copyModifyExpr [path] + | + step=dynamicUpdFunCall [path] ; /** @@ -2863,6 +2950,37 @@ throws PermissionDeniedException, EXistException, XPathException ) ; +dynamicUpdFunCall [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step = null; + PathExpr primary = new PathExpr(context); +} +: + #( + "updating" + { + List params = new ArrayList(5); + boolean isPartial = false; + } + step=primaryExpr [primary] + ( + ( + { PathExpr pathExpr = new PathExpr(context); } + expr [pathExpr] { params.add(pathExpr); } + ) + )* + { + DynamicFunctionCall dynCall = new DynamicFunctionCall(context, step, params, isPartial); + dynCall.setCategory(Expression.Category.UPDATING); + step = dynCall; + path.add(step); + } + ) +; + + functionCall [PathExpr path] returns [Expression step] throws PermissionDeniedException, EXistException, XPathException @@ -3413,6 +3531,92 @@ throws PermissionDeniedException, EXistException, XPathException ) ; + +transformWithExpr [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step= null; + CopyModifyExpression cpme = new CopyModifyExpression(context); + QName virtualVariable = null; +}: + #( + tr:"transform" + { + PathExpr transformExpr = new PathExpr(context); + } + t:expr [transformExpr] + { + try { + virtualVariable = QName.parse(staticContext, "virtualCopyModifyName", null); + } + catch (final IllegalQNameException e) { + // this should never happen, since it is a virtual QName + } + + final VariableDeclaration decl = new VariableDeclaration(context, virtualVariable, transformExpr); + decl.setASTNode(t); + + cpme.addCopySource("virtualCopyModifyName", decl); + } + { + PathExpr withExpr = new PathExpr(context); + } + ( + expr [withExpr] + )? + { + // see https://www.w3.org/TR/xquery-update-30/#id-transform-with for explanation + // in short TransformWith is a shorthand notation for a common Copy Modify Expression + + PathExpr refExpr = new PathExpr(context); + refExpr.add(new VariableReference(context, virtualVariable)); + cpme.setModifyExpr(new OpSimpleMap(context, refExpr, withExpr)); + cpme.setReturnExpr(refExpr); + + cpme.setASTNode(tr); + path.add(cpme); + step = cpme; + } + ) + ; + +copyModifyExpr [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step= null; + CopyModifyExpression cpme = new CopyModifyExpression(context); + PathExpr modify = new PathExpr(context); + PathExpr retExpr = new PathExpr(context); +}: + #( + cp:"copy" + ( + #( + copyVarName:VARIABLE_BINDING + { + PathExpr inputSequence= new PathExpr(context); + } + step=expr [inputSequence] + { + cpme.addCopySource(copyVarName.getText(), inputSequence); + } + ) + )+ + step=expr [modify] + step=expr [retExpr] + { + cpme.setModifyExpr(modify); + cpme.setReturnExpr(retExpr); + + cpme.setASTNode(cp); + path.add(cpme); + step = cpme; + } + ) + ; + typeCastExpr [PathExpr path] returns [Expression step] throws PermissionDeniedException, EXistException, XPathException @@ -3562,6 +3766,122 @@ throws XPathException, PermissionDeniedException, EXistException ) ; +xqufInsertExpr [PathExpr path] +returns [Expression step] +throws XPathException, PermissionDeniedException, EXistException +{ +}: + #( + insertAST:"insert" + { + PathExpr source = new PathExpr(context); + PathExpr target = new PathExpr(context); + InsertExpr.Choice choice = null; + } + step=expr [source] + #( + it:INSERT_TARGET + { + switch (it.getText()) { + case "first": + choice = InsertExpr.Choice.FIRST; + break; + case "last": + choice = InsertExpr.Choice.LAST; + break; + case "into": + choice = InsertExpr.Choice.INTO; + break; + case "before": + choice = InsertExpr.Choice.BEFORE; + break; + case "after": + choice = InsertExpr.Choice.AFTER; + break; + } + } + ) + step=expr [target] + { + InsertExpr insertExpr = new InsertExpr(context, source, target, choice); + insertExpr.setASTNode(insertAST); + path.add(insertExpr); + step = insertExpr; + } + ) + ; + +xqufDeleteExpr [PathExpr path] +returns [Expression step] +throws XPathException, PermissionDeniedException, EXistException +{ +}: + #( + deleteAST:"delete" + { + PathExpr target = new PathExpr(context); + } + step=expr [target] + { + DeleteExpr deleteExpr = new DeleteExpr(context, target); + deleteExpr.setASTNode(deleteAST); + path.add(deleteExpr); + step = deleteExpr; + } + ) + ; + +xqufReplaceExpr [PathExpr path] +returns [Expression step] +throws XPathException, PermissionDeniedException, EXistException +{ +}: + #( + replaceAST:"replace" + { + PathExpr target = new PathExpr(context); + PathExpr with = new PathExpr(context); + ReplaceExpr.ReplacementType replacementType = ReplaceExpr.ReplacementType.NODE; + } + ( + "value" + { + replacementType = ReplaceExpr.ReplacementType.VALUE; + } + )? + step=expr [target] + step=expr [with] + { + ReplaceExpr replaceExpr = new ReplaceExpr(context, target, with, replacementType); + replaceExpr.setASTNode(replaceAST); + path.add(replaceExpr); + step = replaceExpr; + } + ) + ; + +xqufRenameExpr [PathExpr path] +returns [Expression step] +throws XPathException, PermissionDeniedException, EXistException +{ +}: + #( + renameAST:"rename" + { + PathExpr target = new PathExpr(context); + PathExpr newName = new PathExpr(context); + } + step=expr [target] + step=expr [newName] + { + RenameExpr renameExpr = new RenameExpr(context, target, newName); + renameExpr.setASTNode(renameAST); + path.add(renameExpr); + step = renameExpr; + } + ) + ; + mapConstr [PathExpr path] returns [Expression step] throws XPathException, PermissionDeniedException, EXistException diff --git a/exist-core/src/main/java/org/exist/xquery/CopyModifyExpression.java b/exist-core/src/main/java/org/exist/xquery/CopyModifyExpression.java new file mode 100644 index 00000000000..47973ba141d --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/CopyModifyExpression.java @@ -0,0 +1,130 @@ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; + +import java.util.ArrayList; +import java.util.List; + +public class CopyModifyExpression extends PathExpr { + + public static class CopySource { + protected String varName; + protected Expression inputSequence; + + public String getVariable() { + return this.varName; + } + public Expression getInputSequence() { + return this.inputSequence; + } + + public void setVariable(String varName) { + this.varName = varName; + } + + public void setInputSequence(Expression inputSequence) { + this.inputSequence = inputSequence; + } + + public CopySource(String name, Expression value) { + this.varName = name; + this.inputSequence = value; + } + + public CopySource() { + } + } + + + protected List sources; + protected Expression modifyExpr; + protected Expression returnExpr; + + // see https://www.w3.org/TR/xquery-update-30/#id-copy-modify for details + public Category getCategory() { + // placeholder implementation + return Category.SIMPLE; + } + + public CopyModifyExpression(XQueryContext context) { + super(context); + this.sources = new ArrayList(); + } + + public void addCopySource(String varName, Expression value) { + this.sources.add(new CopySource(varName, value)); + } + + public void setModifyExpr(Expression expr) { + this.modifyExpr = expr; + } + + public Expression getModifyExpr() { + return this.modifyExpr; + } + + public void setReturnExpr(Expression expr) { + this.returnExpr = expr; + } + + public Expression getReturnExpr() { + return this.returnExpr; + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { + return Sequence.EMPTY_SEQUENCE; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ONE_OR_MORE; + } + + @Override + public void dump(ExpressionDumper dumper) { + dumper.display("copy").nl(); + dumper.startIndent(); + for(int i = 0; i < sources.size(); i++) + { + dumper.display("$").display(sources.get(i).varName); + dumper.display(" := "); + sources.get(i).inputSequence.dump(dumper); + } + dumper.endIndent(); + dumper.display("modify").nl(); + modifyExpr.dump(dumper); + dumper.nl().display("return "); + dumper.startIndent(); + returnExpr.dump(dumper); + dumper.endIndent(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("copy "); + for(int i = 0; i < sources.size(); i++) + { + result.append("$").append(sources.get(i).varName); + result.append(sources.get(i).inputSequence.toString()); + if (sources.size() > 1 && i < sources.size() - 1) + result.append(", "); + else + result.append(" "); + } + result.append(" "); + result.append("modify "); + result.append(modifyExpr.toString()); + result.append(" "); + result.append("return "); + result.append(returnExpr.toString()); + return result.toString(); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/DeleteExpr.java b/exist-core/src/main/java/org/exist/xquery/DeleteExpr.java new file mode 100644 index 00000000000..5a10f5e103e --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/DeleteExpr.java @@ -0,0 +1,41 @@ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; + +public class DeleteExpr extends ModifyingExpression { + + public DeleteExpr(XQueryContext context, Expression target) + { + super(context, target); + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException + { + + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException + { + return Sequence.EMPTY_SEQUENCE; + } + + public void dump(ExpressionDumper dumper) { + dumper.display("delete").nl(); + dumper.startIndent(); + targetExpr.dump(dumper); + dumper.endIndent(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("delete "); + result.append(" "); + result.append(targetExpr.toString()); + return result.toString(); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/DynamicFunctionCall.java b/exist-core/src/main/java/org/exist/xquery/DynamicFunctionCall.java index 25b1aa73502..35d0c604612 100644 --- a/exist-core/src/main/java/org/exist/xquery/DynamicFunctionCall.java +++ b/exist-core/src/main/java/org/exist/xquery/DynamicFunctionCall.java @@ -34,9 +34,10 @@ public class DynamicFunctionCall extends AbstractExpression { private final Expression functionExpr; private final List arguments; private final boolean isPartial; - private AnalyzeContextInfo cachedContextInfo; + private Category category = Category.SIMPLE; + public DynamicFunctionCall(final XQueryContext context, final Expression fun, final List args, final boolean partial) { super(context); setLocation(fun.getLine(), fun.getColumn()); @@ -155,4 +156,14 @@ public String toString() { return builder.toString(); } + + + @Override + public Category getCategory() { + return this.category; + } + + public void setCategory(Category category) { + this.category = category; + } } diff --git a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java index 38faf8e7497..7d349fafc36 100644 --- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java +++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java @@ -228,6 +228,10 @@ public class ErrorCodes { "module has incorrect type"); public static final ErrorCode FOQM0006 = new W3CErrorCode("FOQM0006", "No suitable XQuery processor available."); + /* XQuery 3.0 Update Facility https://www.w3.org/TR/xquery-update-30/#id-new-error-codes */ + public static final ErrorCode XUST0032 = new W3CErrorCode("XUST0032", "It is a static error if an %updating or %simple annotation is used on a VarDecl."); + public static final ErrorCode XUST0003 = new W3CErrorCode("XUST0003", "It is a static error if a Prolog contains more than one revalidation declaration."); + /* eXist specific XQuery and XPath errors * * Codes have the format [EX][XQ|XP][DY|SE|ST][nnnn] diff --git a/exist-core/src/main/java/org/exist/xquery/Expression.java b/exist-core/src/main/java/org/exist/xquery/Expression.java index b32205a370d..b4dec95c638 100644 --- a/exist-core/src/main/java/org/exist/xquery/Expression.java +++ b/exist-core/src/main/java/org/exist/xquery/Expression.java @@ -36,6 +36,16 @@ */ public interface Expression { + /** + * Updating expressions are a new category of expression introduced by XQuery Update Facility 3.0 + * An updating expression is an expression that can return a non-empty pending update list + * Simple expressions are all expressions that are not updating expressions + */ + enum Category { + UPDATING, + SIMPLE + } + // Flags to be passed to analyze: /** * Indicates that the query engine will call the expression once for every @@ -255,4 +265,6 @@ public interface Expression { public boolean allowMixedNodesInReturn(); public Expression getParent(); + + public default Category getCategory() { return Category.SIMPLE; }; } \ No newline at end of file diff --git a/exist-core/src/main/java/org/exist/xquery/InsertExpr.java b/exist-core/src/main/java/org/exist/xquery/InsertExpr.java new file mode 100644 index 00000000000..9b41a7379a1 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/InsertExpr.java @@ -0,0 +1,70 @@ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; + +public class InsertExpr extends ModifyingExpression { + public enum Choice { + FIRST, + LAST, + INTO, + AFTER, + BEFORE + } + + protected final Expression sourceExpr; + protected final Choice choice; + + public InsertExpr(XQueryContext context, Expression source, Expression target, Choice choice) { + super(context, target); + this.sourceExpr = source; + this.choice = choice; + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { + return Sequence.EMPTY_SEQUENCE; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ONE_OR_MORE; + } + + @Override + public void dump(ExpressionDumper dumper) { + dumper.display("insert").nl(); + dumper.startIndent(); + sourceExpr.dump(dumper); + dumper.endIndent(); + dumper.display(choice).nl(); + dumper.startIndent(); + targetExpr.dump(dumper); + dumper.endIndent(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("insert "); + result.append(sourceExpr.toString()); + result.append(" "); + result.append(choice.toString()); + result.append(" "); + result.append(targetExpr.toString()); + return result.toString(); + } + + public Choice getChoice() { + return choice; + } + + public Expression getSourceExpr() { + return sourceExpr; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/ModifyingExpression.java b/exist-core/src/main/java/org/exist/xquery/ModifyingExpression.java new file mode 100644 index 00000000000..53c390a15b7 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ModifyingExpression.java @@ -0,0 +1,27 @@ +package org.exist.xquery; + +import org.exist.xquery.value.Type; + +public abstract class ModifyingExpression extends AbstractExpression { + + protected final Expression targetExpr; + + public ModifyingExpression(XQueryContext context, Expression target) { + super(context); + this.targetExpr = target; + } + + @Override + public int returnsType() { + // placeholder implementation + return Type.EMPTY; + } + + public Category getCategory() { + return Category.UPDATING; + } + + public Expression getTargetExpr() { + return targetExpr; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/RenameExpr.java b/exist-core/src/main/java/org/exist/xquery/RenameExpr.java new file mode 100644 index 00000000000..9255287a8a7 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/RenameExpr.java @@ -0,0 +1,53 @@ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; + +public class RenameExpr extends ModifyingExpression { + protected final Expression newNameExpr; + + public RenameExpr(XQueryContext context, Expression target, Expression newName) { + super(context, target); + this.newNameExpr = newName; + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { + return Sequence.EMPTY_SEQUENCE; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ONE_OR_MORE; + } + + @Override + public void dump(ExpressionDumper dumper) { + dumper.display("replace").nl(); + dumper.startIndent(); + targetExpr.dump(dumper); + dumper.endIndent(); + dumper.startIndent(); + newNameExpr.dump(dumper); + dumper.endIndent(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("replace "); + result.append(targetExpr.toString()); + result.append(" "); + result.append(newNameExpr.toString()); + return result.toString(); + } + + public Expression getNewNameExpr() { + return newNameExpr; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/ReplaceExpr.java b/exist-core/src/main/java/org/exist/xquery/ReplaceExpr.java new file mode 100644 index 00000000000..d8239ee1bb0 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ReplaceExpr.java @@ -0,0 +1,67 @@ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; + +public class ReplaceExpr extends ModifyingExpression{ + public enum ReplacementType { + NODE, + VALUE + } + + protected final Expression withExpr; + protected final ReplaceExpr.ReplacementType replacementType; + + public ReplaceExpr(XQueryContext context, Expression target, Expression with, ReplaceExpr.ReplacementType replacementType) { + super(context, target); + this.withExpr = with; + this.replacementType = replacementType; + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { + return Sequence.EMPTY_SEQUENCE; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ONE_OR_MORE; + } + + @Override + public void dump(ExpressionDumper dumper) { + dumper.display("replace").nl(); + dumper.startIndent(); + targetExpr.dump(dumper); + dumper.endIndent(); + dumper.display(replacementType).nl(); + dumper.startIndent(); + withExpr.dump(dumper); + dumper.endIndent(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("replace "); + result.append(targetExpr.toString()); + result.append(" "); + result.append(replacementType.toString()); + result.append(" "); + result.append(withExpr.toString()); + return result.toString(); + } + + public ReplaceExpr.ReplacementType getReplacementType() { + return replacementType; + } + + public Expression getWithExpr() { + return withExpr; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index a961b297d8d..aaa75d3dc95 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -109,6 +109,7 @@ * * @author Wolfgang Meier */ + public class XQueryContext implements BinaryValueManager, Context { private static final Logger LOG = LogManager.getLogger(XQueryContext.class); @@ -420,6 +421,8 @@ public class XQueryContext implements BinaryValueManager, Context { private final Map staticDecimalFormats = new HashMap<>(); private static final QName UNNAMED_DECIMAL_FORMAT = new QName("__UNNAMED__", Function.BUILTIN_FUNCTION_NS); + private RevalidationMode revalidationMode = RevalidationMode.LAX; + public XQueryContext() { profiler = new Profiler(null); staticDecimalFormats.put(UNNAMED_DECIMAL_FORMAT, DecimalFormat.UNNAMED); @@ -3260,6 +3263,22 @@ public void registerBinaryValueInstance(final BinaryValue binaryValue) { binaryValueInstances.push(binaryValue); } + public enum RevalidationMode { + STRICT, + LAX, + SKIP + } + + /** + * Revalidation mode controls the process by which type information + * is recovered for an updated document + */ + public void setRevalidationMode(final RevalidationMode rev) { + this.revalidationMode = rev; + } + + public RevalidationMode getRevalidationMode() { return this.revalidationMode; } + /** * Cleanup Task which is responsible for relasing the streams * of any {@link BinaryValue} which have been used during diff --git a/exist-core/src/main/java/org/exist/xquery/update/Modification.java b/exist-core/src/main/java/org/exist/xquery/update/Modification.java index 16b767f5dc5..5876a90c3ee 100644 --- a/exist-core/src/main/java/org/exist/xquery/update/Modification.java +++ b/exist-core/src/main/java/org/exist/xquery/update/Modification.java @@ -351,4 +351,7 @@ protected Txn getTransaction() { return node.getParentNode(); } } + + @Override + public Category getCategory() { return Category.UPDATING; } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java b/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java index 35e6165fa4f..675d7593bec 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java +++ b/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java @@ -22,6 +22,7 @@ package org.exist.xquery.value; import org.exist.dom.QName; +import org.exist.xquery.Annotation; import org.exist.xquery.Cardinality; import org.exist.xquery.XPathException; import org.w3c.dom.Document; @@ -40,6 +41,8 @@ public class SequenceType { private Cardinality cardinality = Cardinality.EXACTLY_ONE; private QName nodeName = null; + private Annotation[] annotations; + public SequenceType() { } @@ -229,6 +232,13 @@ public void checkCardinality(Sequence seq) throws XPathException { } } + public void setAnnotations(final Annotation[] annotations) { + this.annotations = annotations; + } + public Annotation[] getAnnotations() { + return annotations; + } + @Override public String toString() { if (cardinality == Cardinality.EMPTY_SEQUENCE) { diff --git a/exist-core/src/test/java/org/exist/xquery/XQueryUpdate3Test.java b/exist-core/src/test/java/org/exist/xquery/XQueryUpdate3Test.java new file mode 100644 index 00000000000..9eba0c49ba2 --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/XQueryUpdate3Test.java @@ -0,0 +1,622 @@ +package org.exist.xquery; + +import antlr.RecognitionException; +import antlr.TokenStreamException; +import antlr.collections.AST; +import org.exist.EXistException; +import org.exist.security.PermissionDeniedException; +import org.exist.storage.BrokerPool; +import org.exist.storage.DBBroker; +import org.exist.test.ExistEmbeddedServer; +import org.exist.xquery.parser.XQueryAST; +import org.exist.xquery.parser.XQueryLexer; +import org.exist.xquery.parser.XQueryParser; +import org.exist.xquery.parser.XQueryTreeParser; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.SequenceType; +import org.junit.ClassRule; +import org.junit.Test; + +import java.io.StringReader; + +import static org.junit.Assert.*; + +public class XQueryUpdate3Test +{ + + @ClassRule + public static final ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true); + + @Test + public void updatingCompatibilityAnnotation() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "xquery version \"3.0\"\n;" + + "module namespace t=\"http://exist-db.org/xquery/test/examples\";\n" + + "declare updating function" + + " t:upsert($e as element(), \n" + + " $an as xs:QName, \n" + + " $av as xs:anyAtomicType) \n" + + " {\n" + + " let $ea := $e/attribute()[fn:node-name(.) = $an]\n" + + " return\n" + + " $ea\n" + + " };"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.xpath(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.xpath(ast, expr); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + } + } + + @Test + public void simpleAnnotation() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "xquery version \"3.0\"\n;" + + "module namespace t=\"http://exist-db.org/xquery/test/examples\";\n" + + "declare %simple function" + + " t:upsert($e as element(), \n" + + " $an as xs:QName, \n" + + " $av as xs:anyAtomicType) \n" + + " {\n" + + " let $ea := $e/attribute()[fn:node-name(.) = $an]\n" + + " return\n" + + " $ea\n" + + " };"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.xpath(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.xpath(ast, expr); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + } + } + + @Test + public void simpleAnnotationIsInvalidForVariableDeclaration() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "declare %simple variable $ab := 1;"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.prolog(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.prolog(ast, expr); + } + catch(XPathException ex) { + assertEquals(ErrorCodes.XUST0032, ex.getErrorCode()); + } + } + + @Test + public void testingForUpdatingFunction() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "%simple function ( * )"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.sequenceType(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + SequenceType type = new SequenceType(); + treeParser.sequenceType(ast, type); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + } + } + + @Test + public void revalidationDeclaration() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "declare revalidation strict;"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.prolog(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.prolog(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + } + } + + @Test + public void transformWith() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "$e transform with { $e + 1 }\n"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + Expression ret = treeParser.expr(ast, expr); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(ret instanceof CopyModifyExpression); + } + } + + @Test + public void copyModifyExprTest() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "copy $je := $e\n" + + " modify delete node $je/salary\n" + + " return $je"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + Expression ret = treeParser.expr(ast, expr); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(ret instanceof CopyModifyExpression); + assertEquals("$je",((CopyModifyExpression) ret).getReturnExpr().toString()); + } + } + + @Test + public void copyModifyExprTestComplexModify() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "copy $newx := $oldx\n" + + " modify (rename node $newx as \"newx\", \n" + + " replace value of node $newx with $newx * 2)\n" + + " return ($oldx, $newx)"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + Expression ret = treeParser.expr(ast, expr); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(ret instanceof CopyModifyExpression); + assertEquals("( replace $newx \"newx\", replace $newx VALUE $newx * 2 )",((CopyModifyExpression) ret).getModifyExpr().toString()); + assertEquals("( $oldx, $newx )",((CopyModifyExpression) ret).getReturnExpr().toString()); + } + } + + @Test + public void dynamicUpdatingFunctionCall() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = "let $f := fn:put#2\n" + + "return invoke updating $f(,\"newnode.xml\")"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.xpath(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.xpath(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(((DebuggableExpression) ((LetExpr)expr.getFirst()).returnExpr).getFirst() instanceof DynamicFunctionCall); + DynamicFunctionCall dfc = (DynamicFunctionCall) ((DebuggableExpression) ((LetExpr)expr.getFirst()).returnExpr).getFirst(); + assertEquals(Expression.Category.UPDATING, dfc.getCategory()); + } + } + + @Test + public void insertExpr() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "insert node 2005\n" + + " after book/publisher"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof InsertExpr); + assertEquals(Expression.Category.UPDATING, expr.getFirst().getCategory()); + assertEquals(InsertExpr.Choice.AFTER, ((InsertExpr) expr.getFirst()).getChoice()); + } + } + + @Test + public void insertExprAsLast() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "insert node $new-police-report\n" + + " as last into fn:doc(\"insurance.xml\")/policies\n" + + " /policy[id = $pid]\n" + + " /driver[license = $license]\n" + + " /accident[date = $accdate]\n" + + " /police-reports"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof InsertExpr); + assertEquals(Expression.Category.UPDATING, expr.getFirst().getCategory()); + assertEquals("$new-police-report", ((InsertExpr) expr.getFirst()).getSourceExpr().toString()); + assertEquals(InsertExpr.Choice.LAST, ((InsertExpr) expr.getFirst()).getChoice()); + } + } + + @Test + public void deleteExpr() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "delete node fn:doc(\"bib.xml\")/books/book[1]/author[last()]"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof DeleteExpr); + assertEquals(Expression.Category.UPDATING, expr.getFirst().getCategory()); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[1]/child::{}author[last()]", ((DeleteExpr) expr.getFirst()).getTargetExpr().toString()); + } + } + + @Test + public void deleteExprComplex() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "delete nodes /email/message\n" + + " [date > xs:dayTimeDuration(\"P365D\")]"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof DeleteExpr); + assertEquals(Expression.Category.UPDATING, expr.getFirst().getCategory()); + } + } + + @Test + public void replaceNodeExpr() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "replace node fn:doc(\"bib.xml\")/books/book[1]/publisher\n" + + "with fn:doc(\"bib.xml\")/books/book[2]/publisher"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof ReplaceExpr); + assertEquals(ReplaceExpr.ReplacementType.NODE,((ReplaceExpr) expr.getFirst()).getReplacementType()); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[1]/child::{}publisher",((ReplaceExpr) expr.getFirst()).getTargetExpr().toString()); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[2]/child::{}publisher",((ReplaceExpr) expr.getFirst()).getWithExpr().toString()); + } + } + + @Test + public void replaceValueExpr() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "replace value of node fn:doc(\"bib.xml\")/books/book[1]/price\n" + + "with fn:doc(\"bib.xml\")/books/book[1]/price * 1.1"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof ReplaceExpr); + assertEquals(ReplaceExpr.ReplacementType.VALUE,((ReplaceExpr) expr.getFirst()).getReplacementType()); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[1]/child::{}price",((ReplaceExpr) expr.getFirst()).getTargetExpr().toString()); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[1]/child::{}price * 1.1",((ReplaceExpr) expr.getFirst()).getWithExpr().toString()); + } + } + + @Test + public void renameExpr() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "rename node fn:doc(\"bib.xml\")/books/book[1]/author[1]\n" + + "as \"principal-author\""; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof RenameExpr); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[1]/child::{}author[1]",((RenameExpr) expr.getFirst()).getTargetExpr().toString()); + assertEquals("\"principal-author\"",((RenameExpr) expr.getFirst()).getNewNameExpr().toString()); + } + } + + @Test + public void renameExprWithExpr() throws EXistException, RecognitionException, XPathException, TokenStreamException, PermissionDeniedException + { + String query = + "rename node fn:doc(\"bib.xml\")/books/book[1]/author[1]\n" + + "as $newname"; + + BrokerPool pool = BrokerPool.getInstance(); + try(final DBBroker broker = pool.getBroker()) { + // parse the query into the internal syntax tree + XQueryContext context = new XQueryContext(broker.getBrokerPool()); + XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + XQueryParser xparser = new XQueryParser(lexer); + xparser.expr(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + XQueryAST ast = (XQueryAST) xparser.getAST(); + + XQueryTreeParser treeParser = new XQueryTreeParser(context); + PathExpr expr = new PathExpr(context); + treeParser.expr(ast, expr); + + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + assertTrue(expr.getFirst() instanceof RenameExpr); + assertEquals("doc(\"bib.xml\")/child::{}books/child::{}book[1]/child::{}author[1]",((RenameExpr) expr.getFirst()).getTargetExpr().toString()); + assertEquals("$newname",((RenameExpr) expr.getFirst()).getNewNameExpr().toString()); + } + } +}