net.sf.saxon.trans.SimpleMode 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.trans;
import net.sf.saxon.expr.ComponentBinding;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.expr.XPathContextMajor;
import net.sf.saxon.expr.accum.Accumulator;
import net.sf.saxon.expr.accum.AccumulatorRegistry;
import net.sf.saxon.expr.instruct.SlotManager;
import net.sf.saxon.expr.instruct.TemplateRule;
import net.sf.saxon.om.*;
import net.sf.saxon.pattern.*;
import net.sf.saxon.str.StringView;
import net.sf.saxon.style.StylesheetModule;
import net.sf.saxon.style.StylesheetPackage;
import net.sf.saxon.trace.ExpressionPresenter;
import net.sf.saxon.trans.rules.*;
import net.sf.saxon.tree.util.Navigator;
import net.sf.saxon.type.ErrorType;
import net.sf.saxon.type.ItemType;
import net.sf.saxon.type.Type;
import net.sf.saxon.type.UType;
import net.sf.saxon.value.AtomicValue;
import net.sf.saxon.value.Whitespace;
import net.sf.saxon.z.IntHashMap;
import net.sf.saxon.z.IntIterator;
import java.util.*;
import java.util.function.Predicate;
/**
* A Mode is a collection of rules; the selection of a rule to apply to a given element
* is determined by a Pattern. A SimpleMode is a mode contained within a single package,
* as opposed to a CompoundMode which can combine rules from several packages
*/
public class SimpleMode extends Mode {
// TODO:PERF the data structure does not cater well for a stylesheet making heavy use of
// match="schema-element(X)". We should probably expand the substitution group.
protected final RuleChain genericRuleChain = new RuleChain();
protected RuleChain atomicValueRuleChain = new RuleChain();
protected RuleChain functionItemRuleChain = new RuleChain();
protected RuleChain documentRuleChain = new RuleChain();
protected RuleChain textRuleChain = new RuleChain();
protected RuleChain commentRuleChain = new RuleChain();
protected RuleChain processingInstructionRuleChain = new RuleChain();
protected RuleChain namespaceRuleChain = new RuleChain();
protected RuleChain unnamedElementRuleChain = new RuleChain();
protected RuleChain unnamedAttributeRuleChain = new RuleChain();
protected IntHashMap namedElementRuleChains = new IntHashMap<>(32);
protected IntHashMap namedAttributeRuleChains = new IntHashMap<>(8);
protected Map qNamedElementRuleChains;
protected Map qNamedAttributeRuleChains;
private BuiltInRuleSet builtInRuleSet = TextOnlyCopyRuleSet.getInstance();
private Rule mostRecentRule;
private int mostRecentModuleHash;
private int stackFrameSlotsNeeded = 0;
private int highestRank;
private final Map explicitPropertyPrecedences = new HashMap<>();
private final Map explicitPropertyValues = new HashMap<>();
/**
* Default constructor - creates a Mode containing no rules
*
* @param modeName the name of the mode
*/
public SimpleMode(StructuredQName modeName) {
super(modeName);
}
/**
* Set the built-in template rules to be used with this Mode in the case where there is no
* explicit template rule
*
* @param defaultRules the built-in rule set
*/
public void setBuiltInRuleSet(BuiltInRuleSet defaultRules) {
this.builtInRuleSet = defaultRules;
hasRules = true; // if mode is explicitly declared, treat it as containing rules
}
/**
* Get the built-in template rules to be used with this Mode in the case where there is no
* explicit template rule
*
* @return the built-in rule set, defaulting to the TextOnlyCopyRuleSet if no other rule set has
* been supplied
*/
@Override
public BuiltInRuleSet getBuiltInRuleSet() {
return this.builtInRuleSet;
}
/**
* Get the active component of this mode. For a simple mode this is the mode itself;
* for a compound mode it is the "overriding" part
*/
@Override
public SimpleMode getActivePart() {
return this;
}
/**
* Check that the mode does not contain conflicting property values, and select property
* values if there were multiple xsl:mode declarations, possible at different import precedence
*
* @throws XPathException if there are conflicts
*/
public void resolveProperties(RuleManager manager) throws XPathException {
boolean failOnMultipleMatch = false;
boolean warningOnMultipleMatch = true;
for (Map.Entry entry : getActivePart().explicitPropertyValues.entrySet()) {
String prop = entry.getKey();
String value = entry.getValue();
if (value.equals("##conflict##")) {
throw new XPathException(
"For " + getLabel() +
", there are conflicting values for xsl:mode/@" +
prop +
" at the same import precedence", "XTSE0545");
}
switch (prop) {
case "streamable":
boolean streamable = "yes".equals(value);
setStreamable(streamable);
if (streamable) {
Mode omniMode = manager.obtainMode(Mode.OMNI_MODE, true);
omniMode.setStreamable(true);
}
break;
case "typed":
mustBeTyped = "yes".equals(value) || "strict".equals(value) || "lax".equals(value);
mustBeUntyped = "no".equals(value);
break;
case "on-no-match":
BuiltInRuleSet base = null;
switch (value) {
case "text-only-copy":
base = TextOnlyCopyRuleSet.getInstance();
break;
case "shallow-copy":
base = ShallowCopyRuleSet.getInstance();
break;
case "deep-copy":
base = DeepCopyRuleSet.getInstance();
break;
case "shallow-skip":
base = ShallowSkipRuleSet.getInstance();
break;
case "deep-skip":
base = DeepSkipRuleSet.getInstance();
break;
case "fail":
base = FailRuleSet.getInstance();
break;
default:
// already validated
break;
}
if ("yes".equals(explicitPropertyValues.get("warning-on-no-match"))) {
base = new RuleSetWithWarnings(base);
}
setBuiltInRuleSet(base);
break;
case "on-multiple-match":
if (value.equals("fail")) {
failOnMultipleMatch = true;
}
break;
case "warning-on-multiple-match":
warningOnMultipleMatch = value.equals("yes");
break;
case "use-accumulators":
AccumulatorRegistry registry = manager.getStylesheetPackage().getAccumulatorRegistry();
Set accumulators = new HashSet<>();
if (!value.isEmpty()) {
String[] tokens = value.split("[ \t\r\n]+");
for (String eqname : tokens) {
Accumulator acc = registry.getAccumulator(StructuredQName.fromEQName(eqname));
accumulators.add(acc);
}
}
setAccumulators(accumulators);
break;
}
}
if (failOnMultipleMatch) {
setRecoveryPolicy(RecoveryPolicy.DO_NOT_RECOVER);
} else if (warningOnMultipleMatch) {
setRecoveryPolicy(RecoveryPolicy.RECOVER_WITH_WARNINGS);
} else {
setRecoveryPolicy(RecoveryPolicy.RECOVER_SILENTLY);
}
}
/**
* Get an identifier for the mode for use in error messages
* @return either the string "the unnamed mode" or the string "mode NNNN" where "NNNN"
* is the display form of the mode's name.
*/
public String getLabel() {
return isUnnamedMode() ?
"the unnamed mode" :
"mode " + modeName.getDisplayName();
}
/**
* Construct a new Mode, copying the contents of an existing Mode
*
* @param from the existing mode.
* @param to the name of the new mode to be created
*/
public static void copyRules(final SimpleMode from, final SimpleMode to) {
try {
from.processRules(r -> {
Rule r2 = r.copy(false);
to.addRule(r2.getPattern(), r2);
});
} catch (XPathException e) {
throw new AssertionError(e);
}
to.mostRecentRule = from.mostRecentRule;
to.mostRecentModuleHash = from.mostRecentModuleHash;
}
/**
* Generate a search state for processing a given node
*
* @return a new object capable of holding the state of a search for a rule
*/
protected RuleSearchState makeRuleSearchState(RuleChain chain, XPathContext context) {
return new RuleSearchState();
}
/**
* Ask whether there are any template rules in this mode
* (a mode could exist merely because it is referenced in apply-templates)
*
* @return true if no template rules exist in this mode
*/
@Override
public boolean isEmpty() {
return !hasRules;
}
/**
* Set an explicit property at a particular precedence. Used for detecting conflicts
*
* @param name the name of the property
* @param value the value of the property
* @param precedence the import precedence of this property value
*/
public void setExplicitProperty(String name, String value, int precedence) {
int p = explicitPropertyPrecedences.getOrDefault(name, Integer.MIN_VALUE);
if (p != Integer.MIN_VALUE) {
if (p < precedence) {
explicitPropertyPrecedences.put(name, precedence);
explicitPropertyValues.put(name, value);
} else if (p == precedence) {
String v = explicitPropertyValues.get(name);
if (v != null & !v.equals(value)) {
// We don't throw an exception, because the conflict is an error only if this
// is the highest-precedence declaration of this mode
explicitPropertyValues.put(name, "##conflict##");
}
} else {
// no action
}
} else {
explicitPropertyPrecedences.put(name, precedence);
explicitPropertyValues.put(name, value);
}
final String typed = explicitPropertyValues.get("typed");
mustBeTyped = "yes".equals(typed) || "strict".equals(typed) || "lax".equals(typed);
mustBeUntyped = "no".equals(typed);
}
/**
* Get the value of a property of this mode, e.g. the "typed" property
*
* @param name the property name, e.g. "typed"
* @return the property value
*/
public String getPropertyValue(String name) {
return explicitPropertyValues.get(name);
}
/**
* Get the "explicit namespaces" matched by this mode. Returns a set containing all the namespaces
* matched by specific template rules in this mode
*
* @param pool the NamePool for the configuration
* @return the set of all namespace URIs of names explicitly matched by rules in this mode
*/
@Override
public Set getExplicitNamespaces(NamePool pool) {
Set namespaces = new HashSet<>();
IntIterator ii = namedElementRuleChains.keyIterator();
while (ii.hasNext()) {
int fp = ii.next();
namespaces.add(pool.getURI(fp));
}
return namespaces;
}
/**
* Add a rule to the Mode.
*
* @param pattern a Pattern
* @param action the Object to return from getRule() when the supplied node matches this Pattern
* @param module the stylesheet module containing the rule
* @param precedence the import precedence of the rule
* @param priority the priority of the rule
* @param position the relative position of the rule in declaration order. If two rules have the same
* position in declaration order, this indicates that they were formed by splitting
* a single rule whose pattern is a union pattern
* @param part the relative position of a rule within a family of rules created by splitting a
* single rule governed by a union pattern. This is used where the splitting of the
* rule was mandated by the XSLT specification, that is, where there is no explicit
* priority specified. In cases where Saxon splits a rule for optimization reasons,
* the subrules will all have the same subsequence number.
*/
public void addRule(Pattern pattern, RuleTarget action,
StylesheetModule module, int precedence, double priority, int position, int part) {
hasRules = true;
// Ignore a pattern that will never match, e.g. "@comment"
if (pattern.getItemType() instanceof ErrorType) {
return;
}
// for fast lookup, we maintain one list for each element name for patterns that can only
// match elements of a given name, one list for each node type for patterns that can only
// match one kind of non-element node, and one generic list.
// Each list is sorted in precedence/priority order so we find the highest-priority rule first
// This logic is designed to ensure that when a UnionPattern contains multiple branches
// with the same priority, next-match doesn't select the same template twice (next-match-024)
int moduleHash = module.hashCode();
// int sequence;
// if (mostRecentRule == null) {
// sequence = 0;
// } else if (action == mostRecentRule.getAction() && moduleHash == mostRecentModuleHash) {
// sequence = mostRecentRule.getSequence();
// } else {
// sequence = mostRecentRule.getSequence() + 1;
// }
//int precedence = module.getPrecedence();
int minImportPrecedence = module.getMinImportPrecedence();
Rule newRule = makeRule(pattern, action, precedence, minImportPrecedence, priority, position, part);
if (pattern instanceof NodeTestPattern) {
ItemType test = pattern.getItemType();
if (test instanceof AnyNodeTest) {
newRule.setAlwaysMatches(true);
} else if (test instanceof NodeKindTest) {
newRule.setAlwaysMatches(true);
} else if (test instanceof NameTest) {
int kind = test.getPrimitiveType();
if (kind == Type.ELEMENT || kind == Type.ATTRIBUTE) {
newRule.setAlwaysMatches(true);
}
}
}
mostRecentRule = newRule;
mostRecentModuleHash = moduleHash;
addRule(pattern, newRule);
}
/**
* Generate a new rule - so it can be overridden to make more specialist rules
*
* @param pattern the pattern that this rule matches
* @param action the object invoked by this rule (usually a Template)
* @param precedence the precedence of the rule
* @param minImportPrecedence the minimum import precedence for xsl:apply-imports
* @param priority the priority of the rule
* @param sequence a sequence number for ordering of rules
* @param part distinguishes rules formed by splitting a rule on a union pattern
* @return the newly created rule
*/
public Rule makeRule(/*@NotNull*/ Pattern pattern, /*@NotNull*/ RuleTarget action,
int precedence, int minImportPrecedence, double priority, int sequence, int part) {
return new Rule(pattern, action, precedence, minImportPrecedence, priority, sequence, part);
}
public void addRule(Pattern pattern, Rule newRule) {
UType uType = pattern.getUType();
if (uType.equals(UType.ELEMENT)) {
int fp = pattern.getFingerprint();
addRuleToNamedOrUnnamedChain(
newRule, fp, unnamedElementRuleChain, namedElementRuleChains);
} else if (uType.equals(UType.ATTRIBUTE)) {
int fp = pattern.getFingerprint();
addRuleToNamedOrUnnamedChain(
newRule, fp, unnamedAttributeRuleChain, namedAttributeRuleChains);
} else if (uType.equals(UType.DOCUMENT)) {
addRuleToList(newRule, documentRuleChain);
} else if (uType.equals(UType.TEXT)) {
addRuleToList(newRule, textRuleChain);
} else if (uType.equals(UType.COMMENT)) {
addRuleToList(newRule, commentRuleChain);
} else if (uType.equals(UType.PI)) {
addRuleToList(newRule, processingInstructionRuleChain);
} else if (uType.equals(UType.NAMESPACE)) {
addRuleToList(newRule, namespaceRuleChain);
} else if (UType.ANY_ATOMIC.subsumes(uType)) {
addRuleToList(newRule, atomicValueRuleChain);
} else if (UType.FUNCTION.subsumes(uType)) {
addRuleToList(newRule, functionItemRuleChain);
} else {
addRuleToList(newRule, genericRuleChain);
}
}
protected void addRuleToNamedOrUnnamedChain(
Rule newRule, int fp, RuleChain unnamedRuleChain, IntHashMap namedRuleChains) {
if (fp == -1) {
addRuleToList(newRule, unnamedRuleChain);
} else {
RuleChain chain = namedRuleChains.get(fp);
if (chain == null) {
chain = new RuleChain(newRule);
namedRuleChains.put(fp, chain);
} else {
addRuleToList(newRule, chain);
}
}
}
/**
* Insert a new rule into this list before others of the same precedence/priority
*
* @param newRule the new rule to be added into the list
* @param list the Rule at the head of the list, or null if the list is empty
*/
private void addRuleToList(Rule newRule, RuleChain list) {
if (list.head() == null) {
list.setHead(newRule);
} else {
int precedence = newRule.getPrecedence();
double priority = newRule.getPriority();
Rule rule = list.head();
Rule prev = null;
while (rule != null) {
if ((rule.getPrecedence() < precedence) ||
(rule.getPrecedence() == precedence && rule.getPriority() <= priority)) {
newRule.setNext(rule);
if (prev == null) {
list.setHead(newRule);
} else {
prev.setNext(newRule);
}
break;
} else {
prev = rule;
rule = rule.getNext();
}
}
if (rule == null) {
assert prev != null;
prev.setNext(newRule);
newRule.setNext(null);
}
}
}
/**
* Specify how many slots for local variables are required by a particular pattern
*
* @param slots the number of slots needed
*/
public void allocatePatternSlots(int slots) {
stackFrameSlotsNeeded = Math.max(stackFrameSlotsNeeded, slots);
}
/**
* Get the rule corresponding to a given item, by finding the best pattern match.
*
* @param item the item to be matched
* @param context the XPath dynamic evaluation context
* @return the best matching rule, if any (otherwise null).
* @throws XPathException if an error occurs matching a pattern
*/
@Override
public Rule getRule(Item item, XPathContext context) throws XPathException {
// If there are match patterns in the stylesheet that use local variables, we need to allocate
// a new stack frame for evaluating the match patterns. We base this on the match pattern with
// the highest number of range variables, so we can reuse the same stack frame for all rules
// that we test against. If no patterns use range variables, we don't bother allocating a new
// stack frame.
// Note, this method isn't functionally necessary; we could call the 3-argument version
// with a filter that always returns true. But this is the common path for apply-templates,
// and we want to squeeze every drop of performance from it.
if (stackFrameSlotsNeeded > 0) {
context = makeNewContext(context);
}
// search the specific list for this node type / node name
RuleChain unnamedNodeChain;
Rule bestRule = null;
if (item instanceof NodeInfo) {
NodeInfo node = (NodeInfo) item;
switch (node.getNodeKind()) {
case Type.DOCUMENT:
unnamedNodeChain = documentRuleChain;
break;
case Type.ELEMENT: {
unnamedNodeChain = unnamedElementRuleChain;
RuleChain namedNodeChain;
if (node.hasFingerprint()) {
namedNodeChain = namedElementRuleChains.get(node.getFingerprint());
} else {
namedNodeChain = getNamedRuleChain(context, Type.ELEMENT, node.getURI(), node.getLocalPart());
}
if (namedNodeChain != null) {
bestRule = searchRuleChain(node, context, null, namedNodeChain);
}
break;
}
case Type.ATTRIBUTE: {
unnamedNodeChain = unnamedAttributeRuleChain;
RuleChain namedNodeChain;
if (node.hasFingerprint()) {
namedNodeChain = namedAttributeRuleChains.get(node.getFingerprint());
} else {
namedNodeChain = getNamedRuleChain(context, Type.ATTRIBUTE, node.getURI(), node.getLocalPart());
}
if (namedNodeChain != null) {
bestRule = searchRuleChain(node, context, null, namedNodeChain);
}
break;
}
case Type.TEXT:
unnamedNodeChain = textRuleChain;
break;
case Type.COMMENT:
unnamedNodeChain = commentRuleChain;
break;
case Type.PROCESSING_INSTRUCTION:
unnamedNodeChain = processingInstructionRuleChain;
break;
case Type.NAMESPACE:
unnamedNodeChain = namespaceRuleChain;
break;
default:
throw new AssertionError("Unknown node kind");
}
// search the list for unnamed nodes of a particular kind
if (unnamedNodeChain != null) {
bestRule = searchRuleChain(node, context, bestRule, unnamedNodeChain);
}
// Search the list for rules for nodes of unknown node kind
bestRule = searchRuleChain(node, context, bestRule, genericRuleChain);
} else if (item instanceof AtomicValue) {
if (atomicValueRuleChain != null) {
bestRule = searchRuleChain(item, context, bestRule, atomicValueRuleChain);
}
bestRule = searchRuleChain(item, context, bestRule, genericRuleChain);
} else if (item instanceof Function) {
if (functionItemRuleChain != null) {
bestRule = searchRuleChain(item, context, bestRule, functionItemRuleChain);
}
bestRule = searchRuleChain(item, context, bestRule, genericRuleChain);
}
return bestRule;
}
/**
* Get a rule chain for a particular node name without allocating a fingerprint from the name pool
*
* @param kind the kind of node (element or attribute)
* @param uri the namespace URI of the node
* @param local the local name of the node
* @return the Rule at the head of the rule chain for nodes of this name, or null if there are no rules
* to consider
*/
protected RuleChain getNamedRuleChain(XPathContext c, int kind, String uri, String local) {
// If this is the first attempt to match a non-fingerprinted node, build indexes
// to the rule chains based on StructuredQName rather than fingerprint
synchronized(this) {
if (qNamedElementRuleChains == null) {
qNamedElementRuleChains = new HashMap<>(namedElementRuleChains.size());
qNamedAttributeRuleChains = new HashMap<>(namedAttributeRuleChains.size());
NamePool pool = c.getNamePool();
indexByQName(pool, namedElementRuleChains, qNamedElementRuleChains);
indexByQName(pool, namedAttributeRuleChains, qNamedAttributeRuleChains);
}
}
return (kind == Type.ELEMENT ? qNamedElementRuleChains : qNamedAttributeRuleChains)
.get(new StructuredQName("", uri, local));
}
private static void indexByQName(NamePool pool, IntHashMap indexByFP, Map indexByQN) {
IntIterator ii = indexByFP.keyIterator();
while (ii.hasNext()) {
int fp = ii.next();
RuleChain eChain = indexByFP.get(fp);
StructuredQName name = pool.getStructuredQName(fp);
indexByQN.put(name, eChain);
}
}
/**
* Search a chain of rules
*
* @param item the item being matched
* @param context XPath dynamic context
* @param bestRule the best rule so far in terms of precedence and priority (may be null)
* @param chain the chain to be searched
* @return the best match rule found in the chain, or the previous best rule, or null
* @throws XPathException if an error occurs matching a pattern
*/
protected Rule searchRuleChain(Item item, XPathContext context, /*@Nullable*/ Rule bestRule, RuleChain chain) throws XPathException {
while (!(context instanceof XPathContextMajor)) {
context = context.getCaller();
}
// Get the rule search state object - this could be reusable within a rule chain.
RuleSearchState ruleSearchState = makeRuleSearchState(chain, context);
Rule head = chain == null ? null : chain.head();
while (head != null) {
if (bestRule != null) {
int rank = head.compareRank(bestRule);
if (rank < 0) {
// if we already have a match, and the precedence or priority of this
// rule is lower, quit the search
break;
} else if (rank == 0) {
// this rule has the same precedence and priority as the matching rule already found
if (ruleMatches(head, item, (XPathContextMajor) context, ruleSearchState)) {
if (head.getSequence() != bestRule.getSequence()) {
reportAmbiguity(item, bestRule, head, context);
}
// choose whichever one comes last (assuming the error wasn't fatal)
int seqComp = Integer.compare(bestRule.getSequence(), head.getSequence());
if (seqComp > 0) {
return bestRule;
} else if (seqComp < 0) {
return head;
} else {
// we're dealing with two rules formed by partitioning a union pattern
bestRule = bestRule.getPartNumber() > head.getPartNumber() ? bestRule : head;
}
break;
} else {
// keep searching other rules of the same precedence and priority
}
} else {
// this rule has higher rank than the matching rule already found
if (ruleMatches(head, item, (XPathContextMajor) context, ruleSearchState)) {
bestRule = head;
}
}
} else if (ruleMatches(head, item, (XPathContextMajor) context, ruleSearchState)) {
bestRule = head;
if (getRecoveryPolicy() == RecoveryPolicy.RECOVER_SILENTLY) {
//ruleSearchState.count();
break; // choose the first match; rules within a chain are in order of rank
}
}
//ruleSearchState.count();// Keep tab of the number of checks
head = head.getNext();
}
return bestRule;
}
/**
* Does this rule match the given item? Can be overridden
*
* @param r the rule to check
* @param item the context item
* @param context the static context for evaluation
* @param pre An appropriate matcher for preconditions in this mode
* @return true if this rule does match
* @throws XPathException if a dynamic error occurs while matching the pattern
*/
protected boolean ruleMatches(Rule r, Item item, XPathContextMajor context, RuleSearchState pre) throws XPathException {
return r.isAlwaysMatches() || r.matches(item, context);
}
/**
* Get the rule corresponding to a given item, by finding the best Pattern match.
*
* @param item the item to be matched
* @param context the XPath dynamic evaluation context
* @param filter a filter to select which rules should be considered
* @return the best matching rule, if any (otherwise null).
* @throws XPathException if an error occurs
*/
/*@Nullable*/
@Override
public Rule getRule(Item item, XPathContext context, Predicate filter) throws XPathException {
// If there are match patterns in the stylesheet that use local variables, we need to allocate
// a new stack frame for evaluating the match patterns. We base this on the match pattern with
// the highest number of range variables, so we can reuse the same stack frame for all rules
// that we test against. If no patterns use range variables, we don't bother allocating a new
// stack frame.
if (stackFrameSlotsNeeded > 0) {
context = makeNewContext(context);
}
// Get the rule search state object
RuleSearchState ruleSearchState;
// search the specific list for this node type / node name
Rule bestRule = null;
RuleChain unnamedNodeChain;
// Search the list for unnamed nodes of a particular kind
if (item instanceof NodeInfo) {
NodeInfo node = (NodeInfo) item;
switch (node.getNodeKind()) {
case Type.DOCUMENT:
unnamedNodeChain = documentRuleChain;
break;
case Type.ELEMENT: {
unnamedNodeChain = unnamedElementRuleChain;
RuleChain namedNodeChain;
if (node.hasFingerprint()) {
namedNodeChain = namedElementRuleChains.get(node.getFingerprint());
} else {
namedNodeChain = getNamedRuleChain(context, Type.ELEMENT, node.getURI(), node.getLocalPart());
}
if (namedNodeChain != null) {
ruleSearchState = makeRuleSearchState(namedNodeChain, context);
bestRule = searchRuleChain(item, context, null, namedNodeChain, ruleSearchState, filter);
}
break;
}
case Type.ATTRIBUTE: {
unnamedNodeChain = unnamedAttributeRuleChain;
RuleChain namedNodeChain;
if (node.hasFingerprint()) {
namedNodeChain = namedAttributeRuleChains.get(node.getFingerprint());
} else {
namedNodeChain = getNamedRuleChain(context, Type.ATTRIBUTE, node.getURI(), node.getLocalPart());
}
if (namedNodeChain != null) {
ruleSearchState = makeRuleSearchState(namedNodeChain, context);
bestRule = searchRuleChain(item, context, null, namedNodeChain, ruleSearchState, filter);
}
break;
}
case Type.TEXT:
unnamedNodeChain = textRuleChain;
break;
case Type.COMMENT:
unnamedNodeChain = commentRuleChain;
break;
case Type.PROCESSING_INSTRUCTION:
unnamedNodeChain = processingInstructionRuleChain;
break;
case Type.NAMESPACE:
unnamedNodeChain = namespaceRuleChain;
break;
default:
throw new AssertionError("Unknown node kind");
}
ruleSearchState = makeRuleSearchState(unnamedNodeChain, context);
bestRule = searchRuleChain(item, context, bestRule, unnamedNodeChain, ruleSearchState, filter);
// Search the list for rules for nodes of unknown node kind
ruleSearchState = makeRuleSearchState(genericRuleChain, context);
return searchRuleChain(item, context, bestRule, genericRuleChain, ruleSearchState, filter);
} else if (item instanceof AtomicValue) {
if (atomicValueRuleChain != null) {
ruleSearchState = makeRuleSearchState(atomicValueRuleChain, context);
bestRule = searchRuleChain(item, context, bestRule, atomicValueRuleChain, ruleSearchState, filter);
}
ruleSearchState = makeRuleSearchState(genericRuleChain, context);
bestRule = searchRuleChain(item, context, bestRule, genericRuleChain, ruleSearchState, filter);
return bestRule;
} else if (item instanceof Function) {
if (functionItemRuleChain != null) {
ruleSearchState = makeRuleSearchState(functionItemRuleChain, context);
bestRule = searchRuleChain(item, context, bestRule, functionItemRuleChain, ruleSearchState, filter);
}
ruleSearchState = makeRuleSearchState(genericRuleChain, context);
bestRule = searchRuleChain(item, context, bestRule, genericRuleChain, ruleSearchState, filter);
return bestRule;
} else {
return null;
}
}
/**
* Search a chain of rules
*
* @param item the item being matched
* @param context XPath dynamic context
* @param bestRule the best rule so far in terms of precedence and priority (may be null)
* @param chain the chain to be searched
* @param ruleSearchState An appropriate ruleState in this mode
* @param filter filter used to select which rules are candidates to be searched
* @return the best match rule found in the chain, or the previous best rule, or null
* @throws XPathException if an error occurs while matching a pattern
*/
protected Rule searchRuleChain(Item item, XPathContext context,
Rule/*@Nullable*/ bestRule, RuleChain chain,
RuleSearchState ruleSearchState, Predicate filter) throws XPathException {
Rule head = chain == null ? null : chain.head();
while (!(context instanceof XPathContextMajor)) {
context = context.getCaller();
}
while (head != null) {
if (filter == null || filter.test(head)) {
if (bestRule != null) {
int rank = head.compareRank(bestRule);
if (rank < 0) {
// if we already have a match, and the precedence or priority of this
// rule is lower, quit the search
break;
} else if (rank == 0) {
// this rule has the same precedence and priority as the matching rule already found
if (ruleMatches(head, item, (XPathContextMajor) context, ruleSearchState)) {
reportAmbiguity(item, bestRule, head, context);
// choose whichever one comes last (assuming the error wasn't fatal)
bestRule = bestRule.getSequence() > head.getSequence() ? bestRule : head;
break;
} else {
// keep searching other rules of the same precedence and priority
}
} else {
// this rule has higher rank than the matching rule already found
if (ruleMatches(head, item, (XPathContextMajor) context, ruleSearchState)) {
bestRule = head;
}
}
} else if (ruleMatches(head, item, (XPathContextMajor) context, ruleSearchState)) {
bestRule = head;
if (getRecoveryPolicy() == RecoveryPolicy.RECOVER_SILENTLY) {
break; // choose the first match; rules within a chain are in order of rank
}
}
}
head = head.getNext();
}
return bestRule;
}
/**
* Report an ambiguity, that is, the situation where two rules of the same
* precedence and priority match the same node
*
* @param item The item that matches two or more rules
* @param r1 The first rule that the node matches
* @param r2 The second rule that the node matches
* @param c The context for the transformation
* @throws XPathException if the system is configured to treat ambiguous template matching as a
* non-recoverable error
*/
protected void reportAmbiguity(Item item, Rule r1, Rule r2, XPathContext c)
throws XPathException {
// Save the effort of constructing the message if it's not going to be reported anyway
if (getRecoveryPolicy() == RecoveryPolicy.RECOVER_SILENTLY) {
return;
}
// don't report an error if the conflict is between two branches of the same Union pattern
if (r1.getAction() == r2.getAction() && r1.getSequence() == r2.getSequence()) {
return;
}
String path;
String errorCode = "XTDE0540";
if (item instanceof NodeInfo) {
path = Navigator.getPath((NodeInfo) item);
} else {
path = item.toShortString();
}
Pattern pat1 = r1.getPattern();
Pattern pat2 = r2.getPattern();
String message;
if (r1.getAction() == r2.getAction()) {
message = "Ambiguous rule match for " + path + ". " +
"Matches \"" + showPattern(pat1) + "\" on line " + pat1.getLocation().getLineNumber() +
" of " + pat1.getLocation().getSystemId() +
", a rule which appears in the stylesheet more than once, because the containing module was included more than once";
} else {
message = "Ambiguous rule match for " + path + '\n' +
"Matches both \"" + showPattern(pat1) + "\" on line " + pat1.getLocation().getLineNumber() +
" of " + pat1.getLocation().getSystemId() +
"\nand \"" + showPattern(pat2) + "\" on line " + pat2.getLocation().getLineNumber() +
" of " + pat2.getLocation().getSystemId();
}
switch (getRecoveryPolicy()) {
case DO_NOT_RECOVER:
throw new XPathException(message, errorCode, getLocation());
case RECOVER_WITH_WARNINGS:
c.getController().warning(message, errorCode, getLocation());
break;
case RECOVER_SILENTLY:
default:
// no action
}
}
private static String showPattern(Pattern p) {
// Complex patterns can be laid out with lots of whitespace, which looks messy in the error message
return Whitespace.collapseWhitespace(StringView.of(p.toShortString()).tidy()).toString();
}
/**
* Prepare for possible streamability - null here, but can be subclassed
*
* @throws XPathException if a failure occurs
*/
public void prepareStreamability() throws XPathException {
}
/**
* Allocate slot numbers to all the external component references in this component
*
* @param pack the containing package
*/
@Override
public void allocateAllBindingSlots(final StylesheetPackage pack) {
if (getDeclaringComponent().getDeclaringPackage() == pack && !bindingSlotsAllocated) {
forceAllocateAllBindingSlots(pack, this, getDeclaringComponent().getComponentBindings());
bindingSlotsAllocated = true;
}
}
public static void forceAllocateAllBindingSlots(
final StylesheetPackage pack, final SimpleMode mode, final List bindings) {
final Set rulesProcessed = new HashSet<>();
final IdentityHashMap patternsProcessed = new IdentityHashMap<>();
try {
mode.processRules(r -> {
// A rule can appear twice, for example at different import precedences or
// because the match pattern is a union pattern; only allocate slots once
Pattern pattern = r.getPattern();
if (!patternsProcessed.containsKey(pattern)) {
allocateBindingSlotsRecursive(pack, mode, pattern, bindings);
patternsProcessed.put(pattern, true);
}
TemplateRule tr = (TemplateRule) r.getAction();
if (tr.getBody() != null && !rulesProcessed.contains(tr)) {
allocateBindingSlotsRecursive(pack, mode, tr.getBody(), bindings);
rulesProcessed.add(tr);
}
});
} catch (XPathException e) {
throw new AssertionError(e);
}
}
/**
* Compute the streamability of all template rules. No action in Saxon-HE.
*/
public void computeStreamability() throws XPathException {
// Implemented in Saxon-EE
}
/**
* For a streamable mode, invert all the templates to generate streamable code.
* No action in Saxon-HE.
*
* @throws XPathException if there is a non-streamable template in the mode
*/
public void invertStreamableTemplates() throws XPathException {
// Implemented in Saxon-EE
}
/**
* Explain all template rules in this mode by showing their
* expression tree represented in XML. Note that this produces more information
* than the simpler exportTemplateRules() method: this method is intended for
* the human reader wanting diagnostic explanations, whereas exportTemplateRules()
* is designed to produce a package that can be re-imported.
*
* @param out used to display the expression tree
*/
@Override
public void explainTemplateRules(final ExpressionPresenter out) throws XPathException {
RuleAction action = r -> r.export(out, isDeclaredStreamable());
try {
processRules(action, new RuleGroupExplainAction(out));
} catch (XPathException err) {
// can't happen, and doesn't matter if it does
}
}
private static class RuleGroupExplainAction implements RuleGroupAction {
private String type;
private final ExpressionPresenter presenter;
public RuleGroupExplainAction(ExpressionPresenter presenter) {
this.presenter = presenter;
}
@Override
public void start() {
presenter.startElement("ruleSet");
presenter.emitAttribute("type", type);
}
@Override
public void setLabel(String type) {
this.type = type;
}
@Override
public void start(int i) {
presenter.startElement("ruleChain");
presenter.emitAttribute("key", presenter.getNamePool().getClarkName(i));
}
@Override
public void end() {
presenter.endElement();
}
}
/**
* Export all template rules in this mode in a form that can be re-imported.
* Note that template rules with union patterns may have been split into multiple
* rules. We need to avoid outputting them more than once.
*
* @param out used to display the expression tree
*/
@Override
public void exportTemplateRules(final ExpressionPresenter out) throws XPathException {
// TODO: if two rules share the same template, avoid duplicate output. This can happen with union patterns, and also
// when a template is present in more than one mode.
processRules(r -> r.export(out, isDeclaredStreamable()));
}
/**
* Walk over all the rules, applying a specified action to each one.
*
* @param action an action that is to be applied to all the rules in this Mode
* @throws XPathException if an error occurs processing any of the rules
*/
@Override
public void processRules(RuleAction action) throws XPathException {
processRules(action, null);
}
/**
* Walk over all the rules, applying a specified action to each one.
*
* @param action an action that is to be applied to all the rules in this Mode
* @param group - actions to be performed at group start and group end
* @throws XPathException if an error occurs processing any of the rules
*/
public void processRules(RuleAction action, RuleGroupAction group) throws XPathException {
processRuleChain(documentRuleChain, action, setGroup(group, "document-node()"));
processRuleChain(unnamedElementRuleChain, action, setGroup(group, "element()"));
processRuleChains(namedElementRuleChains, action, setGroup(group, "namedElements"));
processRuleChain(unnamedAttributeRuleChain, action, setGroup(group, "attribute()"));
processRuleChains(namedAttributeRuleChains, action, setGroup(group, "namedAttributes"));
processRuleChain(textRuleChain, action, setGroup(group, "text()"));
processRuleChain(commentRuleChain, action, setGroup(group, "comment()"));
processRuleChain(processingInstructionRuleChain, action, setGroup(group, "processing-instruction()"));
processRuleChain(namespaceRuleChain, action, setGroup(group, "namespace()"));
processRuleChain(genericRuleChain, action, setGroup(group, "node()"));
processRuleChain(atomicValueRuleChain, action, setGroup(group, "atomicValue"));
processRuleChain(functionItemRuleChain, action, setGroup(group, "function()"));
}
/**
* Set the string associated with a rule group
*
* @param group the group action object
* @param type the type of the rule group
* @return modified rulegroup action
*/
protected RuleGroupAction setGroup(RuleGroupAction group, String type) {
if (group != null) {
group.setLabel(type);
}
return group;
}
public void processRuleChains(IntHashMap chains, RuleAction action, RuleGroupAction group) throws XPathException {
if (chains.size() > 0) {
if (group != null) {
group.start();
}
IntIterator ii = chains.keyIterator();
while (ii.hasNext()) {
int i = ii.next();
if (group != null) {
group.start(i);
}
RuleChain r = chains.get(i);
processRuleChain(r, action, null);
if (group != null) {
group.end();
}
}
if (group != null) {
group.end();
}
}
}
/**
* Walk over all the rules, applying a specified action to each one.
*
* @param action an action that is to be applied to all the rules in this Mode
* @throws XPathException if an error occurs processing any of the rules
*/
public void processRuleChain(RuleChain chain, RuleAction action) throws XPathException {
Rule r = chain == null ? null : chain.head();
while (r != null) {
action.processRule(r);
r = r.getNext();
}
}
public void processRuleChain(RuleChain chain, RuleAction action, RuleGroupAction group) throws XPathException {
Rule r = chain == null ? null : chain.head();
if (r != null) {
if (group != null) {
group.start();
}
while (r != null) {
action.processRule(r);
r = r.getNext();
}
if (group != null) {
group.end();
}
}
}
/**
* Perform optimization on the complete set of rules comprising this Mode. This is a null operation
* in Saxon-HE.
*/
public void optimizeRules() {
}
@Override
public int getMaxPrecedence() {
try {
List capturedPrecedence = new ArrayList<>(1);
capturedPrecedence.add(0);
processRules(r -> {
if (r.getPrecedence() > capturedPrecedence.get(0)) {
capturedPrecedence.set(0, r.getPrecedence());
}
});
return capturedPrecedence.get(0);
} catch (XPathException e) {
throw new AssertionError(e);
}
}
/**
* Compute a rank for each rule, as a combination of the precedence and priority, to allow
* rapid comparison. This method also checks that there are no conflicts for
* property values in different xsl:mode declarations
*
* @param start the lowest rank to use
* @throws XPathException if an error occurs processing the rules
*/
@Override
public void computeRankings(int start) throws XPathException {
// Now sort the rules into ranking order
final RuleSorter sorter = new RuleSorter(start);
// add all the rules in this Mode to the sorter
processRules(sorter::addRule);
// now allocate ranks to all the rules in this Mode
sorter.allocateRanks();
highestRank = start + sorter.getNumberOfRules();
}
@Override
public int getMaxRank() {
return highestRank;
}
/**
* Supporting class used at compile time to sort all the rules into precedence/priority
* order and allocate a rank to each one, so that at run-time, comparing two rules to see
* which has higher precedence/priority is a simple integer subtraction.
*/
private static class RuleSorter {
public ArrayList rules = new ArrayList<>(100);
private final int start;
public RuleSorter(int start) {
this.start = start;
}
public void addRule(Rule rule) {
rules.add(rule);
}
// public int compare(int a, int b) {
// return rules.get(a).compareComputedRank(rules.get(b));
// }
//
// public void swap(int a, int b) {
// Rule temp = rules.get(a);
// rules.set(a, rules.get(b));
// rules.set(b, temp);
// }
public void allocateRanks() {
//noinspection Convert2MethodRef
rules.sort((x,y) -> x.compareComputedRank(y));
int rank = start;
for (int i = 0; i < rules.size(); i++) {
if (i > 0 && rules.get(i - 1).compareComputedRank(rules.get(i)) != 0) {
rank++;
}
rules.get(i).setRank(rank);
}
}
public int getNumberOfRules() {
return rules.size();
}
}
/**
* Interface used around a group of rules - principally at the
* group start and the group end
*/
public interface RuleGroupAction {
/**
* Set some string parameter for the group
*
* @param s a string value to be used for the group
*/
void setLabel(String s);
/**
* Start of a generic group
*/
void start();
/**
* Start of a group characterised by some integer parameter
* @param i characteristic of this group
*/
void start(int i);
/**
* Invoked at the end of a group
*/
void end();
}
/**
* Allocate slots for local variables in all patterns used by the rules in this mode.
* Currently used only for accumulator rules
*/
public void allocateAllPatternSlots() {
final List count = new ArrayList<>(1); // used to allow inner class to have side-effects
count.add(0);
final SlotManager slotManager = new SlotManager(); // TODO: allocate this via the Configuration
try {
processRules(r -> {
int slots = r.getPattern().allocateSlots(slotManager, 0);
int max = Math.max(count.get(0), slots);
count.set(0, max);
});
} catch (XPathException e) {
throw new AssertionError(e);
}
stackFrameSlotsNeeded = count.get(0);
}
@Override
public int getStackFrameSlotsNeeded() {
return stackFrameSlotsNeeded;
}
public void setStackFrameSlotsNeeded(int slots) {
this.stackFrameSlotsNeeded = slots;
}
}