Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g
Original file line number Diff line number Diff line change
Expand Up @@ -1108,8 +1108,6 @@ throws XPathException
STAR
|
(
// TODO: parameter types are collected, but not used!
// Change SequenceType accordingly.
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
(
{ SequenceType paramType = new SequenceType(); }
Expand All @@ -1118,6 +1116,10 @@ throws XPathException
)*
{ SequenceType returnType = new SequenceType(); }
"as" sequenceType [returnType]
{
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
type.setFunctionReturnType(returnType);
}
)
)
)
Expand All @@ -1128,14 +1130,15 @@ throws XPathException
STAR
|
(
// TODO: parameter types are collected, but not used!
// Change SequenceType accordingly.
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
(
{ SequenceType paramType = new SequenceType(); }
sequenceType [paramType]
{ paramTypes.add(paramType); }
)*
{
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
}
)
)
)
Expand All @@ -1146,14 +1149,15 @@ throws XPathException
STAR
|
(
// TODO: parameter types are collected, but not used!
// Change SequenceType accordingly.
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
(
{ SequenceType paramType = new SequenceType(); }
sequenceType [paramType]
{ paramTypes.add(paramType); }
)*
{
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
}
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ private void check(Sequence result, Item item) throws XPathException {
} else if (type == Type.ANY_URI && requiredType == Type.STRING) {
item = item.convertTo(Type.STRING);
type = Type.STRING;
//Binary type promotion (XQuery 4.0): xs:base64Binary ↔ xs:hexBinary
} else if ((type == Type.BASE64_BINARY && requiredType == Type.HEX_BINARY)
|| (type == Type.HEX_BINARY && requiredType == Type.BASE64_BINARY)) {
try {
item = item.convertTo(requiredType);
} catch (final XPathException e) {
throw new XPathException(expression, ErrorCodes.XPTY0004,
"cannot convert " + Type.getTypeName(type) + " to " + Type.getTypeName(requiredType));
}
} else {
if (!(Type.subTypeOf(type, requiredType))) {
throw new XPathException(expression, typeMismatchError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class ErrorCodes {
public static final ErrorCode XQST0052 = new W3CErrorCode("XQST0052", "It is a static error if the type-name in a single-type or sequence-type for a cast or castable expression does not refer to a defined atomic type.");
public static final ErrorCode XQST0053 = new W3CErrorCode("XQST0053", "(Not currently used.)");
public static final ErrorCode XQST0054 = new W3CErrorCode("XQST0054", "It is a static error if a variable depends on itself.");
public static final ErrorCode XQDY0054 = new W3CErrorCode("XQDY0054", "It is a dynamic error if a variable depends on itself.");
public static final ErrorCode XQST0055 = new W3CErrorCode("XQST0055", "It is a static error if a Prolog contains more than one copy-namespaces declaration.");
public static final ErrorCode XQST0056 = new W3CErrorCode("XQST0056", "(Not currently used.)");
public static final ErrorCode XQST0057 = new W3CErrorCode("XQST0057", "It is a static error if a schema import binds a namespace prefix but does not specify a target namespace other than a zero-length string.");
Expand Down
30 changes: 28 additions & 2 deletions exist-core/src/main/java/org/exist/xquery/FunctionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.exist.Namespaces;
import org.exist.dom.QName;
Expand All @@ -48,13 +49,37 @@ public class FunctionFactory {
public static final String PROPERTY_DISABLE_DEPRECATED_FUNCTIONS = "xquery.disable-deprecated-functions";
public static final boolean DISABLE_DEPRECATED_FUNCTIONS_BY_DEFAULT = false;

/**
* Reserved function names per XQuery 3.1/4.0 spec.
* These names must not be used as unprefixed function calls (XPST0003).
*/
private static final Set<String> RESERVED_FUNCTION_NAMES = Set.of(
"array", "attribute", "comment", "document-node", "element",
"function", "if", "item", "map", "namespace-node", "node",
"processing-instruction", "schema-attribute", "schema-element",
"switch", "text", "typeswitch"
);

public static Expression createFunction(XQueryContext context, XQueryAST ast, PathExpr parent, List<Expression> params) throws XPathException {
QName qname = null;
try {
qname = QName.parse(context, ast.getText(), context.getDefaultFunctionNamespace());
} catch(final QName.IllegalQNameException xpe) {
throw new XPathException(ast, ErrorCodes.XPST0081, "Invalid qname " + ast.getText() + ". " + xpe.getMessage());
}

// Check for reserved function names — unprefixed reserved names cannot be
// used as function calls (XPST0003). Prefixed names like fn:item() are not
// subject to the reserved name restriction (they just won't be found → XPST0017).
final String rawName = ast.getText();
if (rawName != null && !rawName.contains(":") && !rawName.contains("{")) {
final String local = qname.getLocalPart();
if (RESERVED_FUNCTION_NAMES.contains(local)) {
throw new XPathException(ast.getLine(), ast.getColumn(), ErrorCodes.XPST0003,
"'" + local + "' is a reserved function name and cannot be used as a function call");
}
}

return createFunction(context, qname, ast, parent, params);
}

Expand Down Expand Up @@ -508,12 +533,13 @@ public static FunctionCall wrap(XQueryContext context, Function call) throws XPa
newSignature.setArgumentTypes(newParamArray);

final UserDefinedFunction func = new UserDefinedFunction(context, newSignature);
func.setPassContextToBody(true);
for (final QName varName: variables) {
func.addVariable(varName);
}

call.setArguments(innerArgs);

func.setFunctionBody(call);

final FunctionCall wrappedCall = new FunctionCall(context, func);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,17 @@ private AtomicValue convertForValueComparison(final AtomicValue value, final int
}

/*
* d. Otherwise, a type error is raised [err:XPTY0004].
* d. (XQuery 4.0) If each operand is an instance of one of the types
* xs:hexBinary or xs:base64Binary, then both operands are cast to
* type xs:base64Binary.
*/
if ((thisType == Type.HEX_BINARY || thisType == Type.BASE64_BINARY)
&& (otherType == Type.HEX_BINARY || otherType == Type.BASE64_BINARY)) {
return value.convertTo(Type.BASE64_BINARY);
}

/*
* e. Otherwise, a type error is raised [err:XPTY0004].
*/
throw new XPathException(this, ErrorCodes.XPTY0004, "Incompatible primitive types");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.ArrayList;
import java.util.List;

import java.util.Set;

import org.exist.dom.QName;
import org.exist.xquery.parser.XQueryAST;
import org.exist.xquery.util.ExpressionDumper;
Expand Down Expand Up @@ -52,7 +54,29 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
resolvedFunction.analyze(contextInfo);
}

/**
* Reserved function names per XQuery 3.1/4.0 spec.
* These names must not be used as unprefixed named function references (XPST0003).
*/
private static final Set<String> RESERVED_FUNCTION_NAMES = Set.of(
"array", "attribute", "comment", "document-node", "element",
"function", "if", "item", "map", "namespace-node", "node",
"processing-instruction", "schema-attribute", "schema-element",
"switch", "text", "typeswitch"
);

public static FunctionCall lookupFunction(Expression self, XQueryContext context, QName funcName, int arity) throws XPathException {
// Check for reserved function names — these cannot be used as named function references
final String localPart = funcName.getLocalPart();
final String nsURI = funcName.getNamespaceURI();
if (RESERVED_FUNCTION_NAMES.contains(localPart) &&
(nsURI == null || nsURI.isEmpty() ||
Function.BUILTIN_FUNCTION_NS.equals(nsURI) ||
context.getDefaultFunctionNamespace().equals(nsURI))) {
throw new XPathException(self, ErrorCodes.XPST0003,
"'" + localPart + "' is a reserved function name and cannot be used as a named function reference");
}

if (Function.BUILTIN_FUNCTION_NS.equals(funcName.getNamespaceURI())
&& "concat".equals(funcName.getLocalPart())
&& arity < 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class UserDefinedFunction extends Function implements Cloneable {
private FunctionCall call;
private boolean hasBeenReset = false;
private List<ClosureVariable> closureVariables = null;
private boolean passContextToBody = false;

public UserDefinedFunction(XQueryContext context, FunctionSignature signature) {
super(context, signature);
Expand All @@ -60,6 +61,17 @@ public void setFunctionBody(Expression body) {
this.body = body.simplify();
}

/**
* Mark this UDF as a wrapper for an internal function (created by
* {@link FunctionFactory#wrap}). Wrapper functions pass the evaluation
* context through to their body so that context-dependent built-in
* functions (like fn:id, fn:idref, fn:string, etc.) can access the
* focus when called via function references.
*/
public void setPassContextToBody(boolean passContext) {
this.passContextToBody = passContext;
}

public void addVariable(final String varName) throws XPathException {
try {
final QName qname = QName.parse(context, varName, null);
Expand Down Expand Up @@ -155,7 +167,15 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
", got " + currentArguments[j].getItemCount());
}
}
result = body.eval(null, null);
// For wrapper functions (created by FunctionFactory.wrap for internal
// function references), pass the context through so context-dependent
// built-in functions can access the focus. For regular user-declared
// functions, the focus is absent per the XQuery spec.
if (passContextToBody) {
result = body.eval(contextSequence, contextItem);
} else {
result = body.eval(null, null);
}
return result;
} finally {
// restore the local variable stack
Expand Down
10 changes: 5 additions & 5 deletions exist-core/src/main/java/org/exist/xquery/VariableImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,26 +120,26 @@ public void setSequenceType(SequenceType type) throws XPathException {
else {actualCardinality = Cardinality.EXACTLY_ONE;}
//Type.EMPTY is *not* a subtype of other types ; checking cardinality first
if (!getSequenceType().getCardinality().isSuperCardinalityOrEqualOf(actualCardinality))
{throw new XPathException(getValue(), "XPTY0004: Invalid cardinality for variable $" + getQName() +
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid cardinality for variable $" + getQName() +
". Expected " +
getSequenceType().getCardinality().getHumanDescription() +
", got " + actualCardinality.getHumanDescription());}
//TODO : ignore nodes right now ; they are returned as xs:untypedAtomicType
if (!Type.subTypeOf(getSequenceType().getPrimaryType(), Type.NODE)) {
if (!getValue().isEmpty() && !Type.subTypeOf(getValue().getItemType(), getSequenceType().getPrimaryType()))
{throw new XPathException(getValue(), "XPTY0004: Invalid type for variable $" + getQName() +
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid type for variable $" + getQName() +
". Expected " +
Type.getTypeName(getSequenceType().getPrimaryType()) +
", got " +Type.getTypeName(getValue().getItemType()));}
//Here is an attempt to process the nodes correctly
} else {
//Same as above : we probably may factorize
//Same as above : we probably may factorize
if (!getValue().isEmpty() && !Type.subTypeOf(getValue().getItemType(), getSequenceType().getPrimaryType()))
{throw new XPathException(getValue(), "XPTY0004: Invalid type for variable $" + getQName() +
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid type for variable $" + getQName() +
". Expected " +
Type.getTypeName(getSequenceType().getPrimaryType()) +
", got " +Type.getTypeName(getValue().getItemType()));}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException
"Variable '$" + qname + "' is not declared.");
}
if (!var.isInitialized()) {
throw new XPathException(this, ErrorCodes.XQST0054,
throw new XPathException(this, ErrorCodes.XQDY0054,
"variable declaration of '$" + qname + "' cannot " +
"be executed because of a circularity.");
}
Expand Down
25 changes: 25 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/XQueryContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,31 @@ public DocumentSet getStaticDocs() {
return textResourceSupplier.apply(getBroker(), getBroker().getCurrentTransaction(), uri, charset);
}

/**
* Gets a text resource from the "Available text resources" of the
* dynamic context, matching by URI only. This is used when no encoding
* is specified, allowing the resource to be found regardless of what
* charset it was registered with.
*
* @param uri the URI of the resource to retrieve
* @return a reader to read the resource content from, or null if not found
* @throws XPathException in case of a dynamic error
*/
public @Nullable Reader getDynamicallyAvailableTextResourceByUri(final String uri)
throws XPathException {
if (dynamicTextResources == null) {
return null;
}

for (final Map.Entry<Tuple2<String, Charset>, QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException>> entry : dynamicTextResources.entrySet()) {
if (entry.getKey()._1.equals(uri)) {
final Charset registeredCharset = entry.getKey()._2;
return entry.getValue().apply(getBroker(), getBroker().getCurrentTransaction(), uri, registeredCharset);
}
}
return null;
}

/**
* Gets a collection from the "Available collections" of the
* dynamic context.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class FnModule extends AbstractInternalModule {
new FunctionDef(FunDocumentURI.FS_DOCUMENT_URI_1, FunDocumentURI.class),
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[0], FunElementWithId.class),
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[1], FunElementWithId.class),
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[2], FunElementWithId.class),
new FunctionDef(FunEmpty.signature, FunEmpty.class),
new FunctionDef(FunEncodeForURI.signature, FunEncodeForURI.class),
new FunctionDef(FunEndsWith.signatures[0], FunEndsWith.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,23 @@
}
}

private void analyzeString(final MemTreeBuilder builder, final String input, String pattern, final String flags) throws XPathException {
private void analyzeString(final MemTreeBuilder builder, final String input, final String pattern, final String flags) throws XPathException {
final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration();

// XPath 4.0 lookaround syntax is not yet implemented in eXist's XQuery 3.1 runtime.
// When XQuery 4.0 lands (v2/xq4-core-functions), replace this guard with the
// translateXPath4Lookaround() dispatch path.
if (org.exist.xquery.regex.RegexUtil.hasXPath4Lookaround(pattern)) {

Check notice on line 134 in exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java#L134

Unnecessary use of fully qualified name 'org.exist.xquery.regex.RegexUtil.hasXPath4Lookaround' due to existing static import 'org.exist.xquery.regex.RegexUtil.*'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address Codacy issue: Unnecessary use of fully qualified name 'org.exist.xquery.regex.RegexUtil.validateXPathRegex' due to existing static import 'org.exist.xquery.regex.RegexUtil.*'

throw new XPathException(this, ErrorCodes.XPST0017,
"XPath 4.0 lookaround syntax in regex patterns (e.g. (*positive_lookahead:...)) "
+ "is not yet implemented in this XQuery 3.1 build. Rewrite the regex without lookaround.");
}

// Pre-validate: reject constructs not valid in XPath 3.1 regex
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address Codacy issue: Unnecessary use of fully qualified name 'org.exist.xquery.regex.RegexUtil.validateXPathRegex' due to existing static import 'org.exist.xquery.regex.RegexUtil.*'

if (!org.exist.xquery.regex.RegexUtil.hasLiteral(flags)) {

Check notice on line 141 in exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java#L141

Unnecessary use of fully qualified name 'org.exist.xquery.regex.RegexUtil.hasLiteral' due to existing static import 'org.exist.xquery.regex.RegexUtil.*'
org.exist.xquery.regex.RegexUtil.validateXPathRegex(this, pattern, false);

Check notice on line 142 in exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java#L142

Unnecessary use of fully qualified name 'org.exist.xquery.regex.RegexUtil.validateXPathRegex' due to existing static import 'org.exist.xquery.regex.RegexUtil.*'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address Codacy issue: Unnecessary use of fully qualified name 'org.exist.xquery.regex.RegexUtil.validateXPathRegex' due to existing static import 'org.exist.xquery.regex.RegexUtil.*'

}

final List<String> warnings = new ArrayList<>(1);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class FunContainsToken extends BasicFunction {

private final static FunctionParameterSequenceType FS_INPUT = optManyParam("input", Type.STRING, "The input string");
private final static FunctionParameterSequenceType FS_TOKEN = param("token", Type.STRING, "The token to be searched for");
private final static FunctionParameterSequenceType FS_COLLATION = param("pattern", Type.STRING, "Collation to use");
private final static FunctionParameterSequenceType FS_COLLATION = optParam("collation", Type.STRING, "Collation to use; an empty sequence selects the default collation");

public final static FunctionSignature[] FS_CONTAINS_TOKEN = functionSignatures(
FS_CONTAINS_TOKEN_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public class FunElementWithId extends BasicFunction {
"If none is matching or $idrefs is the empty sequence, returns the empty sequence.";
private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.STRING, "the elements with IDs matching IDREFs from $idref-sequence");
private static final FunctionParameterSequenceType PARAM_ID_REFS_STRING = optManyParam("idrefs", Type.STRING, "The IDREF sequence");
private static final FunctionParameterSequenceType PARAM_NODE = param("node", Type.NODE, "A node in the document to search");
public static final FunctionSignature[] FS_ELEMENT_WITH_ID_SIGNATURES = functionSignatures(
FN_NAME,
FN_DESCRIPTION,
FN_RETURN,
arities(
arity(),
arity(PARAM_ID_REFS_STRING)
arity(PARAM_ID_REFS_STRING),
arity(PARAM_ID_REFS_STRING, PARAM_NODE)
)
);

Expand Down
Loading
Loading