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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ private void check(Sequence result, Item item) throws XPathException {
//Retrieve the actual node
{type= ((NodeProxy) item).getNode().getNodeType();}
}
// XQuery 4.0: record type checking — a map can match a record type
if (requiredType == Type.RECORD && Type.subTypeOf(type, Type.MAP_ITEM)) {
return; // record type checking handled by SequenceType.checkType
}
if(type != requiredType && !Type.subTypeOf(type, requiredType)) {
//TODO : how to make this block more generic ? -pb
if (type == Type.UNTYPED_ATOMIC) {
Expand Down
108 changes: 108 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/FieldAccessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* info@exist-db.org
* http://www.exist-db.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.xquery;

import org.exist.xquery.functions.map.AbstractMapType;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;

import java.util.Objects;

/**
* XQuery 4.0 field accessor expression: {@code $expr.fieldName}.
*
* <p>Syntactic sugar for {@code map:get($expr, "fieldName")}. The parser-next
* branch will wire the {@code .NCName} postfix syntax to this class. Until then,
* it can be used programmatically or via {@code fn:get} on record-typed values.</p>
*
* <p>If the base expression has a declared record type, the field name is
* validated against the record's field declarations at analysis time.</p>
*/
public class FieldAccessor extends AbstractExpression {

private final Expression baseExpr;
private final String fieldName;

public FieldAccessor(final XQueryContext context, final Expression baseExpr, final String fieldName) {
super(context);
this.baseExpr = baseExpr;
this.fieldName = fieldName;
}

public Expression getBaseExpression() {
return baseExpr;
}

public String getFieldName() {
return fieldName;
}

@Override
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
contextInfo.setParent(this);
baseExpr.analyze(contextInfo);
}

@Override
public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException {
final Sequence baseResult = baseExpr.eval(contextSequence, contextItem);

if (baseResult.isEmpty()) {
return Sequence.EMPTY_SEQUENCE;
}

final Item item = baseResult.itemAt(0);
if (!Type.subTypeOf(item.getType(), Type.MAP_ITEM)) {
throw new XPathException(this, ErrorCodes.XPTY0004,
"Field accessor ." + fieldName + " requires a map, got " + Type.getTypeName(item.getType()));
}

final AbstractMapType map = (AbstractMapType) item;
final Sequence value = map.get(new StringValue(this, fieldName));
return Objects.requireNonNullElse(value, Sequence.EMPTY_SEQUENCE);
}

@Override
public int returnsType() {
return Type.ITEM;
}

@Override
public int getDependencies() {
return baseExpr.getDependencies();
}

@Override
public void dump(final ExpressionDumper dumper) {
baseExpr.dump(dumper);
dumper.display(".");
dumper.display(fieldName);
}

@Override
public String toString() {
return baseExpr.toString() + "." + fieldName;
}
}
10 changes: 10 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ private Expression checkArgumentType(
return new FunctionTypeCheck(context, functionParameterType, argument);
}

// XQuery 4.0: wrap with record type check if parameter declares a record type
if (argType.isRecordType() && argType.getRecordType() != null) {
return new RecordTypeCheck(context, argType.getRecordType(), argument);
}

return argument;
}

Expand Down Expand Up @@ -323,6 +328,11 @@ private Expression checkArgumentType(
return new FunctionTypeCheck(context, functionParameterType, argument);
}

// XQuery 4.0: wrap with record type check if parameter declares a record type
if (argType.isRecordType() && argType.getRecordType() != null) {
return new RecordTypeCheck(context, argType.getRecordType(), argument);
}

return new DynamicTypeCheck(context, argType.getPrimaryType(), argument);
}

Expand Down
180 changes: 180 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/RecordTypeCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* info@exist-db.org
* http://www.exist-db.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.xquery;

import org.exist.dom.persistent.DocumentSet;
import org.exist.xquery.functions.map.AbstractMapType;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.*;

/**
* Runtime check that a function argument matches a declared record type.
*
* <p>When a function parameter is declared as {@code $param as record(name as xs:string, ...)},
* the argument expression is wrapped in a {@code RecordTypeCheck}. At runtime,
* the check verifies the argument is a map that matches all required fields and
* field types declared in the {@link RecordType}.</p>
*
* <p>Modeled on {@link DynamicTypeCheck} and {@link FunctionTypeCheck}.</p>
*/
public class RecordTypeCheck extends AbstractExpression {

private final Expression expression;
private final RecordType recordType;

public RecordTypeCheck(final XQueryContext context, final RecordType recordType, final Expression expr) {
super(context);
this.recordType = recordType;
this.expression = expr;
}

@Override
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
contextInfo.setParent(this);
expression.analyze(contextInfo);
}

@Override
public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException {
final Sequence seq = expression.eval(contextSequence, contextItem);

if (seq.isEmpty()) {
return seq;
}

for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) {
final Item item = i.nextItem();
check(item);
}

return seq;
}

private void check(final Item item) throws XPathException {
if (!Type.subTypeOf(item.getType(), Type.MAP_ITEM)) {
throw new XPathException(expression, ErrorCodes.XPTY0004,
"Expected " + recordType + " but got " + Type.getTypeName(item.getType()));
}

final AbstractMapType map = (AbstractMapType) item;
if (!recordType.matches(map)) {
throw new XPathException(expression, ErrorCodes.XPTY0004,
"Map does not match " + recordType + ": " + describeFailure(map));
}
}

private String describeFailure(final AbstractMapType map) {
final StringBuilder sb = new StringBuilder();
for (final RecordType.FieldDeclaration field : recordType.getFieldDeclarations()) {
final Sequence value = map.get(new StringValue(field.getName()));
if ((value == null || value.isEmpty()) && !field.isOptional()) {
if (sb.length() > 0) sb.append("; ");
sb.append("missing required field '").append(field.getName()).append("'");
} else if (value != null && !value.isEmpty() && field.getType() != null) {
try {
if (!field.getType().checkType(value)) {
if (sb.length() > 0) sb.append("; ");
sb.append("field '").append(field.getName()).append("' has type ")
.append(Type.getTypeName(value.getItemType()))
.append(", expected ").append(field.getType());
}
} catch (final XPathException e) {
if (sb.length() > 0) sb.append("; ");
sb.append("field '").append(field.getName()).append("' type check error: ").append(e.getMessage());
}
}
}
if (sb.length() == 0) {
// Extensibility check failure
sb.append("map contains unexpected keys not declared in ").append(recordType);
}
return sb.toString();
}

@Override
public int returnsType() {
return Type.MAP_ITEM;
}

@Override
public int getDependencies() {
return expression.getDependencies();
}

@Override
public void dump(final ExpressionDumper dumper) {
if (dumper.verbosity() > 1) {
dumper.display("record-type-check[");
dumper.display(recordType.toString());
dumper.display(", ");
}
expression.dump(dumper);
if (dumper.verbosity() > 1) {
dumper.display("]");
}
}

@Override
public String toString() {
return expression.toString();
}

@Override
public void resetState(final boolean postOptimization) {
super.resetState(postOptimization);
expression.resetState(postOptimization);
}

@Override
public void setContextDocSet(final DocumentSet contextSet) {
super.setContextDocSet(contextSet);
expression.setContextDocSet(contextSet);
}

@Override
public int getLine() {
return expression.getLine();
}

@Override
public int getColumn() {
return expression.getColumn();
}

@Override
public void accept(final ExpressionVisitor visitor) {
expression.accept(visitor);
}

@Override
public int getSubExpressionCount() {
return 1;
}

@Override
public Expression getSubExpression(final int index) {
if (index == 0) {
return expression;
}
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + getSubExpressionCount());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.Type;
import org.exist.xquery.functions.map.AbstractMapType;

import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -127,6 +131,7 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
try {
QName varName;
LocalVariable var;
final SequenceType[] argumentTypes = getSignature().getArgumentTypes();
int j = 0;
for (int i = 0; i < parameters.size(); i++, j++) {
varName = parameters.get(i);
Expand All @@ -146,11 +151,29 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
actualCardinality = Cardinality.EXACTLY_ONE;
}

if (!getSignature().getArgumentTypes()[j].getCardinality().isSuperCardinalityOrEqualOf(actualCardinality)) {
if (!argumentTypes[j].getCardinality().isSuperCardinalityOrEqualOf(actualCardinality)) {
throw new XPathException(this, ErrorCodes.XPTY0004, "Invalid cardinality for parameter $" + varName +
". Expected " + getSignature().getArgumentTypes()[j].getCardinality().getHumanDescription() +
". Expected " + argumentTypes[j].getCardinality().getHumanDescription() +
", got " + currentArguments[j].getItemCount());
}

// XQuery 4.0: record type validation at runtime
final SequenceType argType = argumentTypes[j];
if (argType.isRecordType() && argType.getRecordType() != null && !currentArguments[j].isEmpty()) {
for (final SequenceIterator iter = currentArguments[j].iterate(); iter.hasNext(); ) {
final Item item = iter.nextItem();
if (Type.subTypeOf(item.getType(), Type.MAP_ITEM)) {
if (!argType.getRecordType().matches((AbstractMapType) item)) {
throw new XPathException(this, ErrorCodes.XPTY0004,
"Argument $" + varName + " does not match " + argType.getRecordType());
}
} else {
throw new XPathException(this, ErrorCodes.XPTY0004,
"Argument $" + varName + " expected " + argType.getRecordType() +
" but got " + Type.getTypeName(item.getType()));
}
}
}
}
result = body.eval(null, null);
return result;
Expand Down
Loading
Loading