net.sf.saxon.ma.map.RecordTest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Saxon-HE Show documentation
Show all versions of Saxon-HE Show documentation
The XSLT and XQuery Processor
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2022 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.ma.map;
import net.sf.saxon.expr.Expression;
import net.sf.saxon.expr.StaticProperty;
import net.sf.saxon.expr.parser.RoleDiagnostic;
import net.sf.saxon.om.Genre;
import net.sf.saxon.om.GroundedValue;
import net.sf.saxon.om.Item;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.transpile.CSharpModifiers;
import net.sf.saxon.tree.iter.AtomicIterator;
import net.sf.saxon.type.*;
import net.sf.saxon.value.AtomicValue;
import net.sf.saxon.value.Cardinality;
import net.sf.saxon.value.SequenceType;
import net.sf.saxon.value.StringValue;
import java.util.*;
import java.util.function.Function;
/**
* An instance of this class represents a specific record item type, for example
* record(x as xs:double, y as element(employee)).
*
* Record types are a proposed extension for XPath 4.0. They were previously introduced
* as a Saxon extension in Saxon 9.8, under the name "tuple types". The syntax for constructing
* a record type requires Saxon-PE or higher, but the supporting code is included in
* Saxon-HE for convenience.
*
* Extended in 10.0 to distinguish extensible vs non-extensible record types. Extensible record
* types permit fields other than those listed to appear; non-extensible record types do not.
* An extensible record type is denoted by record(... ,*).
*/
public class RecordTest extends AnyFunctionType implements TupleType {
private final Map fieldTypes = new HashMap<>();
private final Set optionalFields = new HashSet<>();
private boolean extensible;
/**
* Construct a dummy RecordTest, details to be supplied later
*/
public RecordTest() {}
/**
* Construct a RecordTest
* @param names the names of the fields
* @param types the types of the fields
* @param optionalFieldNames a list of the names of the fields that are declared optional
* @param extensible indicates whether the RecordTest is extensible (allows fields other
* than those declared)
*/
public RecordTest(List names, List types, List optionalFieldNames, boolean extensible) {
setDetails(names, types, optionalFieldNames, extensible);
}
/**
* Supply the details of the RecordTest. This method is only to be used during initialisation,
* it is needed so that a RecordTest can refer to itself. Apart from this, the RecordTest
* is immutable.
* @param names the names of the fields
* @param types the types of the fields
* @param optionalFieldNames a list of the names of the fields that are declared optional
* @param extensible indicates whether the RecordTest is extensible (allows fields other
* than those declared)
*/
public void setDetails(List names, List types, List optionalFieldNames, boolean extensible) {
for (int i = 0; i < names.size(); i++) {
fieldTypes.put(names.get(i), types.get(i));
}
optionalFields.addAll(optionalFieldNames);
this.extensible = extensible;
}
/**
* Determine the Genre (top-level classification) of this type
*
* @return the Genre to which this type belongs, specifically {@link Genre#MAP}
*/
@Override
@CSharpModifiers(code = {"public", "override"})
public Genre getGenre() {
return Genre.MAP;
}
/**
* Ask whether this function item type is a map type. In this case function coercion (to the map type)
* will never succeed.
*
* @return true if this FunctionItemType is a map type
*/
@Override
public boolean isMapType() {
return true;
}
/**
* Ask whether this function item type is an array type. In this case function coercion (to the array type)
* will never succeed.
*
* @return true if this FunctionItemType is an array type
*/
@Override
public boolean isArrayType() {
return false;
}
/**
* Get the names of all the fields
*
* @return the names of the fields (in arbitrary order)
*/
@Override
public Iterable getFieldNames() {
return fieldTypes.keySet();
}
/**
* Get the type of a given field
* @param field the name of the field
* @return the type of the field if it is defined, or null otherwise
*/
@Override
public SequenceType getFieldType(String field) {
return fieldTypes.get(field);
}
/**
* Ask whether a given field is optional
* @param field the name of the field
* @return true if the field is defined as an optional field
*/
@Override
public boolean isOptionalField(String field) {
return optionalFields.contains(field);
}
/**
* Ask whether the record type is extensible, that is, whether fields other than those named are permitted
*
* @return true if fields other than the named fields are permitted to appear
*/
@Override
public boolean isExtensible() {
return extensible;
}
/**
* Test whether a given item conforms to this type
*
* @param item The item to be tested
* @param th type hierarchy data
* @return true if the item is an instance of this type; false otherwise
*/
@Override
public boolean matches(Item item, TypeHierarchy th) {
if (!(item instanceof MapItem)) {
return false;
}
MapItem map = (MapItem)item;
for (Map.Entry field : fieldTypes.entrySet()) {
GroundedValue val = map.get(new StringValue(field.getKey()));
if (val == null) {
if (!isOptionalField(field.getKey())) {
return false;
}
} else if (!field.getValue().matches(val, th)) {
return false;
}
}
if (!extensible) {
AtomicIterator keyIter = map.keys();
AtomicValue key;
while ((key = keyIter.next()) != null) {
if (!(key instanceof StringValue) || !fieldTypes.containsKey(key.getStringValue())) {
return false;
}
}
}
return true;
}
/**
* Get the arity (number of arguments) of this function type
*
* @return the number of argument types in the function signature
*/
public int getArity() {
return 1;
}
/**
* Get the argument types of this map, viewed as a function
*
* @return the list of argument types of this map, viewed as a function
*/
@Override
public SequenceType[] getArgumentTypes() {
// regardless of the key type, a function call on this map can supply any atomic value
return new SequenceType[]{SequenceType.SINGLE_ATOMIC};
}
/**
* Get the result type of this record type, viewed as a function
*
* @return the result type of this record type, viewed as a function
*/
@Override
public SequenceType getResultType() {
if (extensible) {
return SequenceType.ANY_SEQUENCE;
} else {
ItemType resultType = null;
boolean allowsMany = false;
for (Map.Entry field : fieldTypes.entrySet()) {
if (resultType == null) {
resultType = field.getValue().getPrimaryType();
} else {
resultType = Type.getCommonSuperType(resultType, field.getValue().getPrimaryType());
}
allowsMany = allowsMany || Cardinality.allowsMany(field.getValue().getCardinality());
}
return SequenceType.makeSequenceType(resultType,
allowsMany ? StaticProperty.ALLOWS_ZERO_OR_MORE : StaticProperty.ALLOWS_ZERO_OR_ONE);
}
}
/**
* Get the default priority when this ItemType is used as an XSLT pattern
*
* @return the default priority
*/
@Override
public double getDefaultPriority() {
// TODO: this algorithm means that adding fields to the record type reduces its priority, which is wrong
double prio = 1;
for (SequenceType st : fieldTypes.values()) {
prio *= st.getPrimaryType().getNormalizedDefaultPriority();
}
return extensible ? 0.5 + prio/2 : prio;
}
/**
* Produce a representation of this type name for use in error messages.
*
* @return a string representation of the type, in notation resembling but not necessarily
* identical to XPath syntax
*/
public String toString() {
return makeString(SequenceType::toString);
}
/**
* Return a string representation of this ItemType suitable for use in stylesheet
* export files. This differs from the result of toString() in that it will not contain
* any references to anonymous types. Note that it may also use the Saxon extended syntax
* for union types and record types.
*
* @return the string representation as an instance of the XPath ItemType construct
*/
@Override
@CSharpModifiers(code={"public", "override"})
public String toExportString() {
return makeString(SequenceType::toExportString);
}
/**
* Get an alphabetic code representing the type, or at any rate, the nearest built-in type
* from which this type is derived. The codes are designed so that for any two built-in types
* A and B, alphaCode(A) is a prefix of alphaCode(B) if and only if A is a supertype of B.
*
* @return the alphacode for the nearest containing built-in type
*/
@Override
public String getBasicAlphaCode() {
return "FM";
}
/**
* Return a string representation of the record type
* @param show a function to use for converting the types of the component fields to strings
* @return the string representation
*/
private String makeString(Function show) {
StringBuilder sb = new StringBuilder(100);
sb.append("record(");
boolean first = true;
for (Map.Entry field : fieldTypes.entrySet()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(field.getKey());
if (isOptionalField(field.getKey())) {
sb.append('?');
}
sb.append(": ");
if (field.getValue().getPrimaryType() == this) {
sb.append("..").append(Cardinality.getOccurrenceIndicator(field.getValue().getCardinality()));
} else {
sb.append(show.apply(field.getValue()));
}
}
if (isExtensible()) {
sb.append(", *");
}
sb.append(")");
return sb.toString();
}
/**
* Test whether this function type equals another function type
*/
public boolean equals(Object other) {
return this == other ||
other instanceof RecordTest
&& extensible == ((RecordTest) other).extensible
&& fieldTypes.equals(((RecordTest) other).fieldTypes)
&& optionalFields.equals(((RecordTest) other).optionalFields);
}
/**
* Returns a hash code value for the object.
*/
@Override
public int hashCode() {
// Need to avoid infinite recursion for self-reference fields
int h = 0x27ca481f;
for (Map.Entry entry : fieldTypes.entrySet()) {
h ^= entry.getKey().hashCode();
if (entry.getValue().getPrimaryType() == this) {
h ^= 0x05050505;
} else {
h ^= entry.getValue().hashCode();
}
}
return h;
}
/**
* Determine the relationship of one function item type to another
*
* @return for example {@link Affinity#SUBSUMES}, {@link Affinity#SAME_TYPE}
*/
@Override
public Affinity relationship(FunctionItemType other, TypeHierarchy th) {
if (other == AnyFunctionType.getInstance()) {
return Affinity.SUBSUMED_BY;
} else if (other instanceof RecordTest) {
return recordTypeRelationship((RecordTest)other, th);
} else if (other == MapType.ANY_MAP_TYPE) {
return Affinity.SUBSUMED_BY;
} else if (other.isArrayType()) {
return Affinity.DISJOINT;
} else if (other instanceof MapType) {
return recordToMapRelationship((MapType)other, th);
} else {
Affinity rel;
rel = new SpecificFunctionType(getArgumentTypes(), getResultType()).relationship(other, th);
return rel;
}
}
private Affinity recordToMapRelationship(MapType other, TypeHierarchy th) {
AtomicType recordKeyType = isExtensible() ? BuiltInAtomicType.ANY_ATOMIC : BuiltInAtomicType.STRING;
Affinity keyRel = th.relationship(recordKeyType, other.getKeyType());
if (keyRel == Affinity.DISJOINT) {
return Affinity.DISJOINT;
}
// Handle map(xxx, item()*)
if (other.getValueType().getPrimaryType().equals(AnyItemType.getInstance()) && other.getValueType().getCardinality() == StaticProperty.ALLOWS_ZERO_OR_MORE) {
if (keyRel == Affinity.SUBSUMED_BY || keyRel == Affinity.SAME_TYPE) {
return Affinity.SUBSUMED_BY;
} else {
return Affinity.OVERLAPS;
}
} else if (isExtensible()) {
return Affinity.OVERLAPS;
} else {
// The type of every field in the record must be a subtype of the map value type
for (SequenceType entry : fieldTypes.values()) {
Affinity rel = th.sequenceTypeRelationship(entry, other.getValueType());
if (!(rel == Affinity.SUBSUMED_BY || rel == Affinity.SAME_TYPE)) {
return Affinity.OVERLAPS;
}
}
return Affinity.SUBSUMED_BY;
}
}
private Affinity recordTypeRelationship(RecordTest other, TypeHierarchy th) {
Set keys = new HashSet<>(fieldTypes.keySet());
keys.addAll(other.fieldTypes.keySet());
boolean foundSubsuming = false;
boolean foundSubsumed = false;
boolean foundOverlap = false;
if (isExtensible()) {
if (!other.isExtensible()) {
foundSubsuming = true;
}
} else if (other.isExtensible()) {
foundSubsumed = true;
}
for (String key : keys) {
SequenceType t1 = fieldTypes.get(key);
SequenceType t2 = other.fieldTypes.get(key);
if (t1 == null) {
if (isExtensible()) {
foundSubsuming = true;
} else if (Cardinality.allowsZero(t2.getCardinality())) {
foundOverlap = true;
} else {
return Affinity.DISJOINT;
}
} else if (t2 == null) {
if (other.isExtensible()) {
foundSubsumed = true;
} else if (Cardinality.allowsZero(t1.getCardinality())) {
foundOverlap = true;
} else {
return Affinity.DISJOINT;
}
} else {
Affinity a = th.sequenceTypeRelationship(t1, t2);
switch (a) {
case SAME_TYPE:
break;
case SUBSUMED_BY:
foundSubsumed = true;
break;
case SUBSUMES:
foundSubsuming = true;
break;
case OVERLAPS:
foundOverlap = true;
break;
case DISJOINT:
return Affinity.DISJOINT;
}
}
}
if (foundOverlap || (foundSubsumed && foundSubsuming)) {
return Affinity.OVERLAPS;
} else if (foundSubsuming) {
return Affinity.SUBSUMES;
} else if (foundSubsumed) {
return Affinity.SUBSUMED_BY;
} else {
return Affinity.SAME_TYPE;
}
}
/**
* Get extra diagnostic information about why a supplied item does not conform to this
* item type, if available. If extra information is returned, it should be in the form of a complete
* sentence, minus the closing full stop. No information should be returned for obvious cases.
*
* @param item the item being matched
* @param th the type hierarchy cache
*/
@Override
@CSharpModifiers(code = {"public", "override"})
public Optional explainMismatch(Item item, TypeHierarchy th) {
if (item instanceof MapItem) {
for (Map.Entry entry : fieldTypes.entrySet()) {
String key = entry.getKey();
SequenceType required = entry.getValue();
GroundedValue value = ((MapItem) item).get(new StringValue(key));
if (value == null) {
if (!Cardinality.allowsZero(required.getCardinality())) {
return Optional.of("Field " + key + " is absent; it must have a value");
}
} else {
if (!required.matches(value, th)) {
String s = "Field " + key + " has value "
+ Err.depictSequence(value)
+ " which does not match the required type "
+ required.toString();
Optional more = required.explainMismatch(value, th);
if (more.isPresent()) {
s += ". " + more.get();
}
return Optional.of(s);
}
}
}
if (!extensible) {
AtomicIterator keyIter = ((MapItem)item).keys();
AtomicValue key;
while ((key = keyIter.next()) != null) {
if (!(key instanceof StringValue)) {
return Optional.of("Undeclared field " + key + " is present, but it is not a string, and the record type is not extensible");
} else if (!fieldTypes.containsKey(key.getStringValue())) {
return Optional.of("Undeclared field " + key + " is present, but the record type is not extensible");
}
}
}
}
return Optional.empty();
}
@Override
public Expression makeFunctionSequenceCoercer(Expression exp, RoleDiagnostic role)
throws XPathException {
return new SpecificFunctionType(getArgumentTypes(), getResultType()).makeFunctionSequenceCoercer(exp, role);
}
}
// Copyright (c) 2011-2022 Saxonica Limited