net.sf.saxon.expr.instruct.ValueOf 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-2023 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.expr.instruct;
import net.sf.saxon.event.Outputter;
import net.sf.saxon.event.ProxyOutputter;
import net.sf.saxon.event.ReceiverOption;
import net.sf.saxon.expr.*;
import net.sf.saxon.expr.elab.*;
import net.sf.saxon.expr.parser.ContextItemStaticInfo;
import net.sf.saxon.expr.parser.ExpressionTool;
import net.sf.saxon.expr.parser.ExpressionVisitor;
import net.sf.saxon.expr.parser.RebindingMap;
import net.sf.saxon.functions.SystemFunction;
import net.sf.saxon.om.GroundedValue;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.om.StandardNames;
import net.sf.saxon.pattern.NodeKindTest;
import net.sf.saxon.s9api.Location;
import net.sf.saxon.str.EmptyUnicodeString;
import net.sf.saxon.str.UniStringConsumer;
import net.sf.saxon.str.UnicodeString;
import net.sf.saxon.trace.ExpressionPresenter;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.transpile.CSharpModifiers;
import net.sf.saxon.tree.util.Orphan;
import net.sf.saxon.type.*;
import net.sf.saxon.value.Cardinality;
import net.sf.saxon.value.Whitespace;
import java.util.function.BiConsumer;
/**
* An xsl:value-of element in the stylesheet.
* The xsl:value-of element takes attributes:
* - a mandatory attribute select="expression".
* This must be a valid String expression
* - an optional disable-output-escaping attribute, value "yes" or "no"
* - an optional separator attribute. This is handled at compile-time: if the separator attribute
* is present, the select expression passed in here will be a call to the string-join() function.
*
*/
public final class ValueOf extends SimpleNodeConstructor {
private int options;
private boolean numberingInstruction = false; // set to true if generated by xsl:number
private final boolean noNodeIfEmpty;
/**
* Create a new ValueOf expression
*
* @param select the select expression
* @param disable true if disable-output-escaping is in force
* @param noNodeIfEmpty true if the instruction is to return () if the select expression is (),
* false if it is to return an empty text node
*/
public ValueOf(Expression select, boolean disable, boolean noNodeIfEmpty) {
setSelect(select);
options = disable ? ReceiverOption.DISABLE_ESCAPING : ReceiverOption.NONE;
this.noNodeIfEmpty = noNodeIfEmpty;
adoptChildExpression(select);
// If value is fixed, test whether there are any special characters that might need to be
// escaped when the time comes for serialization
if (select instanceof StringLiteral) {
UnicodeString val = ((StringLiteral) select).getString();
boolean special = val.indexWhere(c -> c < 33 || c > 126 || c == '<' || c == '>' || c == '&', 0) >= 0;
if (!special) {
options |= ReceiverOption.NO_SPECIAL_CHARS;
}
}
}
/**
* Indicate that this is really an xsl:number instruction
*/
public void setIsNumberingInstruction() {
numberingInstruction = true;
}
/**
* Determine whether this is really an xsl:number instruction
*
* @return true if this derives from xsl:number
*/
public boolean isNumberingInstruction() {
return numberingInstruction;
}
public boolean isNoNodeIfEmpty() {
return noNodeIfEmpty;
}
@Override
public String toShortString() {
if (getSelect() instanceof StringLiteral) {
return "text{" + Err.depict(((StringLiteral)getSelect()).getGroundedValue()) + "}";
} else {
return super.toShortString();
}
}
/**
* Get the properties of this object to be included in trace messages, by supplying
* the property values to a supplied consumer function
*
* @param consumer the function to which the properties should be supplied, as (property name,
* value) pairs.
*/
@Override
public void gatherProperties(BiConsumer consumer) {
if (getSelect() instanceof StringLiteral) {
consumer.accept("text", (((StringLiteral) getSelect()).getGroundedValue().getUnicodeStringValue()));
}
}
/**
* Get the name of this instruction for diagnostic and tracing purposes
*
* @return the namecode of the instruction name
*/
@Override
public int getInstructionNameCode() {
if (numberingInstruction) {
return StandardNames.XSL_NUMBER;
} else if (getSelect() instanceof StringLiteral) {
return StandardNames.XSL_TEXT;
} else {
return StandardNames.XSL_VALUE_OF;
}
}
/**
* Test for any special options such as disable-output-escaping
*
* @return any special options
*/
public int getOptions() {
return options;
}
/**
* Test whether disable-output-escaping was requested
*
* @return true if disable-output-escaping was requested
*/
public boolean isDisableOutputEscaping() {
return ReceiverOption.contains(options, ReceiverOption.DISABLE_ESCAPING);
}
/*@NotNull*/
@Override
public ItemType getItemType() {
return NodeKindTest.TEXT;
}
@Override
protected int computeCardinality() {
if (noNodeIfEmpty) {
return StaticProperty.ALLOWS_ZERO_OR_ONE;
} else {
return StaticProperty.EXACTLY_ONE;
}
}
@Override
public void localTypeCheck(ExpressionVisitor visitor, ContextItemStaticInfo contextItemType) {
//
}
/**
* Determine the intrinsic dependencies of an expression, that is, those which are not derived
* from the dependencies of its subexpressions. For example, position() has an intrinsic dependency
* on the context position, while (position()+1) does not. The default implementation
* of the method returns 0, indicating "no dependencies".
*
* @return a set of bit-significant flags identifying the "intrinsic"
* dependencies. The flags are documented in class net.sf.saxon.value.StaticProperty
*/
@Override
public int getIntrinsicDependencies() {
int d = super.getIntrinsicDependencies();
if (isDisableOutputEscaping()) {
// Bug 2312 : prevent extraction of global variables
d |= StaticProperty.DEPENDS_ON_ASSIGNABLE_GLOBALS;
}
return d;
}
/**
* Copy an expression. This makes a deep copy.
*
* @return the copy of the original expression
* @param rebindings variables that need to be re-bound
*/
/*@NotNull*/
@Override
public Expression copy(RebindingMap rebindings) {
ValueOf exp = new ValueOf(getSelect().copy(rebindings),
ReceiverOption.contains(options, ReceiverOption.DISABLE_ESCAPING), noNodeIfEmpty);
ExpressionTool.copyLocationInfo(this, exp);
if (numberingInstruction) {
exp.setIsNumberingInstruction();
}
return exp;
}
/**
* Check statically that the results of the expression are capable of constructing the content
* of a given schema type.
*
* @param parentType The schema type
* @param whole true if this expression is to account for the whole value of the type
* @throws net.sf.saxon.trans.XPathException
* if the expression doesn't match the required content type
*/
@Override
public void checkPermittedContents(SchemaType parentType, boolean whole) throws XPathException {
// if the expression is a constant value, check that it is valid for the type
if (getSelect() instanceof Literal) {
GroundedValue selectValue = ((Literal) getSelect()).getGroundedValue();
SimpleType stype = null;
if (parentType instanceof SimpleType && whole) {
stype = (SimpleType) parentType;
} else if (parentType instanceof ComplexType && ((ComplexType) parentType).isSimpleContent()) {
stype = ((ComplexType) parentType).getSimpleContentType();
}
if (whole && stype != null && !stype.isNamespaceSensitive()) {
// Can't validate namespace-sensitive content statically
ValidationFailure err = stype.validateContent(
selectValue.getUnicodeStringValue(), null, getConfiguration().getConversionRules());
if (err != null) {
err.setLocator(getLocation());
err.setErrorCode(isXSLT() ? "XTTE1540" : "XQDY0027");
throw err.makeException();
}
return;
}
if (parentType instanceof ComplexType &&
!((ComplexType) parentType).isSimpleContent() &&
!((ComplexType) parentType).isMixedContent() &&
!Whitespace.isAllWhite(selectValue.getUnicodeStringValue())) {
XPathException err = new XPathException("The containing element must be of type " + parentType.getDescription() +
", which does not allow text content " +
Err.wrap(selectValue.getUnicodeStringValue()));
err.setLocation(getLocation());
err.setIsTypeError(true);
throw err;
}
} else {
// check that the type allows text nodes. If not, this is a warning condition, since the text
// node might turn out to be whitespace
// DROPPED because we can't do env.issueWarning
// if (parentType instanceof ComplexType &&
// !((ComplexType) parentType).isSimpleContent() &&
// !((ComplexType) parentType).isMixedContent()) {
// env.issueWarning("The containing element must be of type " + parentType.getDescription() +
// ", which does not allow text content other than whitespace", this);
// }
}
}
/**
* Convert this value-of instruction to an expression that delivers the string-value of the resulting
* text node as an untyped atomic value.
*
* @return the converted expression
*/
public Expression convertToCastAsString() {
if (noNodeIfEmpty || !Cardinality.allowsZero(getSelect().getCardinality())) {
return new CastExpression(getSelect(), BuiltInAtomicType.UNTYPED_ATOMIC, true);
} else {
// must return zero-length string rather than () if empty
Expression sf = SystemFunction.makeCall("string", getRetainedStaticContext(), getSelect());
return new CastExpression(sf, BuiltInAtomicType.UNTYPED_ATOMIC, false);
}
}
/**
* Process the value of the node, to create the new node.
*
* @param value the string value of the new node
* @param output the destination for the result
* @param context the dynamic evaluation context
* @throws XPathException if a dynamic error occurs
*
*/
@Override
public void processValue(UnicodeString value, Outputter output, XPathContext context) throws XPathException {
output.characters(value, getLocation(), options);
}
/**
* Evaluate this expression, returning the resulting text node to the caller
*
* @param context the dynamic evaluation context
* @return the parentless text node that results from evaluating this instruction, or null to
* represent an empty sequence
* @throws XPathException if a dynamic error occurs
*/
@Override
public NodeInfo evaluateItem(XPathContext context) throws XPathException {
try {
Item content = getSelect().makeElaborator().elaborateForItem().eval(context);
return makeTextNode(content, noNodeIfEmpty, isDisableOutputEscaping(), context);
} catch (XPathException err) {
err.maybeSetLocation(getLocation());
throw err;
}
}
private static NodeInfo makeTextNode(Item content, boolean noNodeIfEmpty, boolean doe, XPathContext context) throws XPathException {
UnicodeString val;
if (content == null) {
if (noNodeIfEmpty) {
return null;
} else {
val = EmptyUnicodeString.getInstance();
}
} else {
val = content.getUnicodeStringValue();
}
Orphan o = new Orphan(context.getConfiguration());
o.setNodeKind(Type.TEXT);
o.setStringValue(val);
if (doe) {
o.setDisableOutputEscaping(true);
}
return o;
}
/**
* Diagnostic print of expression structure. The abstract expression tree
* is written to the supplied output destination.
*/
@Override
public void export(ExpressionPresenter out) throws XPathException {
out.startElement("valueOf", this);
String flags = "";
if (isDisableOutputEscaping()) {
flags += "d";
}
if (ReceiverOption.contains(options, ReceiverOption.NO_SPECIAL_CHARS)) {
flags += "S";
}
if (noNodeIfEmpty) {
flags += "e";
}
if (isLocal()) {
flags += "l";
}
if (!flags.isEmpty()) {
out.emitAttribute("flags", flags);
}
getSelect().export(out);
out.endElement();
}
/**
* Make an elaborator for this expression
*
* @return a suitable elaborator
*/
@Override
public Elaborator getElaborator() {
return new ValueOfElaborator();
}
private static class ValueOfElaborator extends SimpleNodePushElaborator {
@Override
public PushEvaluator elaborateForPush() {
ValueOf expr = (ValueOf) getExpression();
Location loc = expr.getLocation();
boolean noNodeIfEmpty = expr.noNodeIfEmpty;
int options = expr.getOptions();
if (noNodeIfEmpty) {
ItemEvaluator contentEval = expr.getSelect().makeElaborator().elaborateForItem();
return (output, context) -> {
Item value = contentEval.eval(context);
if (value != null) {
output.characters(value.getUnicodeStringValue(), loc, options);
}
return null;
};
} else if (expr.getSelect().getItemType() == BuiltInAtomicType.STRING &&
!expr.isDisableOutputEscaping()
&& !Cardinality.allowsZero(expr.getCardinality())) {
// Try to stream the value direct to the serializer where possible
// Note: see bug 4944, which is why we don't use this path if the select
// expression might return an empty sequence.
// TODO: use a learning strategy, don't do the streaming unless it has benefits
// (monitor the number of calls on characters(), or the size of the strings)
PushEvaluator contentEval = expr.getSelect().makeElaborator().elaborateForPush();
return (output, context) -> {
Outputter streamer = new TextNodeOutputStreamer(output, loc, options);
return contentEval.processLeavingTail(streamer, context);
};
} else {
UnicodeStringEvaluator contentEval = expr.getSelect().makeElaborator().elaborateForUnicodeString(true);
return (output, context) -> {
UnicodeString value = contentEval.eval(context);
output.characters(value, loc, options);
return null;
};
}
}
@Override
public ItemEvaluator elaborateForItem() {
ValueOf expr = (ValueOf)getExpression();
ItemEvaluator contentEval = expr.getSelect().makeElaborator().elaborateForItem();
boolean noNodeIfEmpty = expr.noNodeIfEmpty;
boolean doe = expr.isDisableOutputEscaping();
return context -> ValueOf.makeTextNode(contentEval.eval(context), noNodeIfEmpty, doe, context);
}
}
/**
* An Outputter that streams the text node incrementally to the destination, rather than evaluating
* it first as an in-memory string.
*/
private static class TextNodeOutputStreamer extends ProxyOutputter {
private final Location instructionLoc;
private final int options;
public TextNodeOutputStreamer(Outputter output, Location instructionLoc, int options) {
super(output);
this.instructionLoc = instructionLoc;
this.options = options;
}
@Override
public void append(Item item) throws XPathException {
getNextOutputter().characters(item.getUnicodeStringValue(), instructionLoc, options);
}
@Override
public void append(Item item, Location loc, int properties) throws XPathException {
Location location = loc.getLineNumber() == -1 ? instructionLoc : loc;
getNextOutputter().characters(item.getUnicodeStringValue(), location, properties | options);
}
@Override
@CSharpModifiers(code = {"public", "override"})
public UniStringConsumer getStringReceiver(boolean asTextNode, Location loc) {
Location location = loc.getLineNumber() == -1 ? instructionLoc : loc;
return getNextOutputter().getStringReceiver(true, location);
}
}
}