|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using System.Xml.Xsl.Xslt;
namespace System.Xml.Xsl.Runtime
{
/// <summary>
/// The context of a query consists of all user-provided information which influences the operation of the
/// query. The context manages the following information:
///
/// 1. Input data sources, including the default data source if one exists
/// 2. Extension objects
/// 3. External parameters
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class XmlQueryContext
{
private readonly XmlQueryRuntime _runtime;
private readonly XPathNavigator? _defaultDataSource;
private readonly XmlResolver _dataSources;
private readonly Hashtable _dataSourceCache;
private readonly XsltArgumentList? _argList;
private XmlExtensionFunctionTable? _extFuncsLate;
private readonly WhitespaceRuleLookup? _wsRules;
private readonly QueryReaderSettings _readerSettings; // If we create reader out of stream we will use these settings
/// <summary>
/// This constructor is internal so that external users cannot construct it (and therefore we do not have to test it separately).
/// </summary>
internal XmlQueryContext(XmlQueryRuntime runtime, object defaultDataSource, XmlResolver dataSources, XsltArgumentList? argList, WhitespaceRuleLookup? wsRules)
{
_runtime = runtime;
_dataSources = dataSources;
_dataSourceCache = new Hashtable();
_argList = argList;
_wsRules = wsRules;
if (defaultDataSource is XmlReader)
{
_readerSettings = new QueryReaderSettings((XmlReader)defaultDataSource);
}
else
{
// Consider allowing users to set DefaultReaderSettings in XsltArgumentList
// readerSettings = argList.DefaultReaderSettings;
_readerSettings = new QueryReaderSettings(new NameTable());
}
if (defaultDataSource is string s)
{
// Load the default document from a Uri
_defaultDataSource = GetDataSource(s, null);
if (_defaultDataSource == null)
throw new XslTransformException(SR.XmlIl_UnknownDocument, s);
}
else if (defaultDataSource != null)
{
_defaultDataSource = ConstructDocument(defaultDataSource, null, null);
}
}
//-----------------------------------------------
// Input data sources
//-----------------------------------------------
/// <summary>
/// Returns the name table that should be used in the query to atomize search names and to load
/// new documents.
/// </summary>
public XmlNameTable QueryNameTable
{
get { return _readerSettings.NameTable; }
}
/// <summary>
/// Returns the name table used by the default data source, or null if there is no default data source.
/// </summary>
public XmlNameTable? DefaultNameTable
{
get { return _defaultDataSource?.NameTable; }
}
/// <summary>
/// Return the document which is queried by default--i.e. no data source is explicitly selected in the query.
/// </summary>
public XPathNavigator DefaultDataSource
{
get
{
// Throw exception if there is no default data source to return
if (_defaultDataSource == null)
throw new XslTransformException(SR.XmlIl_NoDefaultDocument, string.Empty);
return _defaultDataSource;
}
}
/// <summary>
/// Fetch the data source specified by "uriRelative" and "uriBase" from the XmlResolver that the user provided.
/// If the resolver returns a stream or reader, create an instance of XPathDocument. If the resolver returns an
/// XPathNavigator, return the navigator. Throw an exception if no data source was found.
/// </summary>
public XPathNavigator? GetDataSource(string uriRelative, string? uriBase)
{
object? input;
Uri? uriResolvedBase, uriResolved;
XPathNavigator? nav = null;
try
{
// If the data source has already been retrieved, then return the data source from the cache.
uriResolvedBase = (uriBase != null) ? _dataSources.ResolveUri(null, uriBase) : null;
uriResolved = _dataSources.ResolveUri(uriResolvedBase, uriRelative);
if (uriResolved != null)
nav = _dataSourceCache[uriResolved] as XPathNavigator;
if (nav == null)
{
// Get the entity from the resolver and ensure it is cached as a document
input = _dataSources.GetEntity(uriResolved!, null, null);
if (input != null)
{
// Construct a document from the entity and add the document to the cache
nav = ConstructDocument(input, uriRelative, uriResolved);
_dataSourceCache.Add(uriResolved!, nav);
}
}
}
catch (XslTransformException)
{
// Don't need to wrap XslTransformException
throw;
}
catch (Exception e)
{
if (!XmlException.IsCatchableException(e))
{
throw;
}
throw new XslTransformException(e, SR.XmlIl_DocumentLoadError, uriRelative);
}
return nav;
}
/// <summary>
/// Ensure that "dataSource" is cached as an XPathDocument and return a navigator over the document.
/// </summary>
private XPathNavigator? ConstructDocument(object dataSource, string? uriRelative, Uri? uriResolved)
{
Debug.Assert(dataSource != null, "GetType() below assumes dataSource is not null");
Stream? stream = dataSource as Stream;
if (stream != null)
{
// Create document from stream
XmlReader reader = _readerSettings.CreateReader(stream, uriResolved?.ToString());
try
{
// Create WhitespaceRuleReader if whitespace should be stripped
return new XPathDocument(WhitespaceRuleReader.CreateReader(reader, _wsRules), XmlSpace.Preserve).CreateNavigator();
}
finally
{
// Always close reader that was opened here
reader.Close();
}
}
else if (dataSource is XmlReader reader)
{
// Create document from reader
// Create WhitespaceRuleReader if whitespace should be stripped
return new XPathDocument(WhitespaceRuleReader.CreateReader(reader, _wsRules), XmlSpace.Preserve).CreateNavigator();
}
else if (dataSource is IXPathNavigable xPathNavigable)
{
if (_wsRules != null)
throw new XslTransformException(SR.XmlIl_CantStripNav, string.Empty);
return xPathNavigable.CreateNavigator();
}
Debug.Assert(uriRelative != null, "Relative URI should not be null");
throw new XslTransformException(SR.XmlIl_CantResolveEntity, uriRelative, dataSource.GetType().ToString());
}
//-----------------------------------------------
// External parameters
//-----------------------------------------------
/// <summary>
/// Get a named parameter from the external argument list. Return null if no argument list was provided, or if
/// there is no parameter by that name.
/// </summary>
public object? GetParameter(string localName, string namespaceUri)
{
return _argList?.GetParam(localName, namespaceUri);
}
//-----------------------------------------------
// Extension objects
//-----------------------------------------------
/// <summary>
/// Return the extension object that is mapped to the specified namespace, or null if no object is mapped.
/// </summary>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = XsltArgumentList.ExtensionObjectSuppresion)]
public object? GetLateBoundObject(string namespaceUri)
{
return _argList?.GetExtensionObject(namespaceUri);
}
/// <summary>
/// Return true if the late bound object identified by "namespaceUri" contains a method that matches "name".
/// </summary>
[RequiresUnreferencedCode(Scripts.ExtensionFunctionCannotBeStaticallyAnalyzed)]
public bool LateBoundFunctionExists(string name, string namespaceUri)
{
object? instance;
if (_argList == null)
return false;
instance = _argList.GetExtensionObject(namespaceUri);
if (instance == null)
return false;
return new XmlExtensionFunction(name, namespaceUri, -1, instance.GetType(), XmlQueryRuntime.LateBoundFlags).CanBind();
}
/// <summary>
/// Get a late-bound extension object from the external argument list. Bind to a method on the object and invoke it,
/// passing "args" as arguments.
/// </summary>
[RequiresUnreferencedCode(Scripts.ExtensionFunctionCannotBeStaticallyAnalyzed)]
public IList<XPathItem> InvokeXsltLateBoundFunction(string name, string namespaceUri, IList<XPathItem>[] args)
{
object? instance;
object[] objActualArgs;
XmlQueryType xmlTypeFormalArg;
Type clrTypeFormalArg;
object? objRet;
// Get external object instance from argument list (throw if either the list or the instance doesn't exist)
instance = _argList?.GetExtensionObject(namespaceUri);
if (instance == null)
throw new XslTransformException(SR.XmlIl_UnknownExtObj, namespaceUri);
// Bind to a method on the instance object
_extFuncsLate ??= new XmlExtensionFunctionTable();
// Bind to the instance, looking for a matching method (throws if no matching method)
XmlExtensionFunction extFunc = _extFuncsLate.Bind(name, namespaceUri, args.Length, instance.GetType(), XmlQueryRuntime.LateBoundFlags);
// Create array which will contain the actual arguments
objActualArgs = new object[args.Length];
for (int i = 0; i < args.Length; i++)
{
// 1. Assume that the input value can only have one of the following 5 Xslt types:
// xs:double, xs:string, xs:boolean, node* (can be rtf)
// 2. Convert each Rtf value to a NodeSet containing one node. Now the value may only have one of the 4 Xslt types.
// 3. Convert from one of the 4 Xslt internal types to the Xslt internal type which is closest to the formal
// argument's Xml type (inferred from the Clr type of the formal argument).
xmlTypeFormalArg = extFunc.GetXmlArgumentType(i);
switch (xmlTypeFormalArg.TypeCode)
{
case XmlTypeCode.Boolean: objActualArgs[i] = XsltConvert.ToBoolean(args[i]); break;
case XmlTypeCode.Double: objActualArgs[i] = XsltConvert.ToDouble(args[i]); break;
case XmlTypeCode.String: objActualArgs[i] = XsltConvert.ToString(args[i]); break;
case XmlTypeCode.Node:
if (xmlTypeFormalArg.IsSingleton)
objActualArgs[i] = XsltConvert.ToNode(args[i]);
else
objActualArgs[i] = XsltConvert.ToNodeSet(args[i]);
break;
case XmlTypeCode.Item:
objActualArgs[i] = args[i];
break;
default:
Debug.Fail($"This XmlTypeCode should never be inferred from a Clr type: {xmlTypeFormalArg.TypeCode}");
break;
}
// 4. Change the Clr representation to the Clr type of the formal argument
clrTypeFormalArg = extFunc.GetClrArgumentType(i);
if (xmlTypeFormalArg.TypeCode == XmlTypeCode.Item || !clrTypeFormalArg.IsAssignableFrom(objActualArgs[i].GetType()))
objActualArgs[i] = XmlQueryRuntime.ChangeTypeXsltArgument(xmlTypeFormalArg, objActualArgs[i], clrTypeFormalArg);
}
// 1. Invoke the late bound method
objRet = extFunc.Invoke(instance, objActualArgs);
// 2. Convert to IList<XPathItem>
if (objRet == null && extFunc.ClrReturnType == typeof(void))
return XmlQueryNodeSequence.Empty;
return (IList<XPathItem>)_runtime.ChangeTypeXsltResult(XmlQueryTypeFactory.ItemS, objRet);
}
//-----------------------------------------------
// Event
//-----------------------------------------------
/// <summary>
/// Fire the XsltMessageEncounteredEvent, passing the specified text as the message.
/// </summary>
public void OnXsltMessageEncountered(string message)
{
_argList?.xsltMessageEncountered?.Invoke(this, new XmlILQueryEventArgs(message));
}
}
/// <summary>
/// Simple implementation of XsltMessageEncounteredEventArgs.
/// </summary>
internal sealed class XmlILQueryEventArgs : XsltMessageEncounteredEventArgs
{
private readonly string _message;
public XmlILQueryEventArgs(string message)
{
_message = message;
}
public override string Message
{
get { return _message; }
}
}
}
|