DynamicContext.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
package gov.nist.secauto.metaschema.core.metapath;
import com.github.benmanes.caffeine.cache.Caffeine;
import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
import gov.nist.secauto.metaschema.core.metapath.function.DefaultFunction.CallingContext;
import gov.nist.secauto.metaschema.core.metapath.function.IFunction.FunctionProperty;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
import gov.nist.secauto.metaschema.core.model.IUriResolver;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import java.io.IOException;
import java.net.URI;
import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.xml.namespace.QName;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
// TODO: add support for in-scope namespaces
/**
* The implementation of a Metapath
* <a href="https://www.w3.org/TR/xpath-31/#eval_context">dynamic context</a>.
*/
public class DynamicContext { // NOPMD - intentional data class
@NonNull
private final Map<QName, ISequence<?>> letVariableMap;
@NonNull
private final SharedState sharedState;
/**
* Construct a new dynamic context with a default static context.
*/
public DynamicContext() {
this(StaticContext.instance());
}
/**
* Construct a new Metapath dynamic context using the provided static context.
*
* @param staticContext
* the Metapath static context
*/
public DynamicContext(@NonNull StaticContext staticContext) {
this.letVariableMap = new ConcurrentHashMap<>();
this.sharedState = new SharedState(staticContext);
}
private DynamicContext(@NonNull DynamicContext context) {
this.letVariableMap = new ConcurrentHashMap<>(context.letVariableMap);
this.sharedState = context.sharedState;
}
private static class SharedState {
@NonNull
private final StaticContext staticContext;
@NonNull
private final ZoneId implicitTimeZone;
@NonNull
private final ZonedDateTime currentDateTime;
@NonNull
private final Map<URI, IDocumentNodeItem> availableDocuments;
@NonNull
private final Map<CallingContext, ISequence<?>> functionResultCache;
@Nullable
private CachingLoader documentLoader;
@NonNull
private final IMutableConfiguration<MetapathEvaluationFeature<?>> configuration;
public SharedState(@NonNull StaticContext staticContext) {
this.staticContext = staticContext;
Clock clock = Clock.systemDefaultZone();
this.implicitTimeZone = ObjectUtils.notNull(clock.getZone());
this.currentDateTime = ObjectUtils.notNull(ZonedDateTime.now(clock));
this.availableDocuments = new HashMap<>();
this.functionResultCache = ObjectUtils.notNull(Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.<CallingContext, ISequence<?>>build().asMap());
this.configuration = new DefaultConfiguration<>();
this.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
}
}
/**
* Generate a new dynamic context that is a copy of this dynamic context.
* <p>
* This method can be used to create a new sub-context where changes can be made
* without affecting this context. This is useful for setting information that
* is only used in a limited evaluation sub-scope, such as for handling variable
* assignment.
*
* @return a new dynamic context
*/
@NonNull
public DynamicContext subContext() {
return new DynamicContext(this);
}
/**
* Get the static context associated with this dynamic context.
*
* @return the associated static context
*/
@NonNull
public StaticContext getStaticContext() {
return sharedState.staticContext;
}
/**
* Get the default time zone used for evaluation.
*
* @return the time zone identifier object
*/
@NonNull
public ZoneId getImplicitTimeZone() {
return sharedState.implicitTimeZone;
}
/**
* Get the current date and time.
*
* @return the current date and time
*/
@NonNull
public ZonedDateTime getCurrentDateTime() {
return sharedState.currentDateTime;
}
/**
* Get the mapping of loaded documents from the document URI to the document
* node.
*
* @return the map of document URIs to document nodes
*/
@SuppressWarnings("null")
@NonNull
public Map<URI, IDocumentNodeItem> getAvailableDocuments() {
return Collections.unmodifiableMap(sharedState.availableDocuments);
}
/**
* Get the document loader assigned to this dynamic context.
*
* @return the loader
* @throws DynamicMetapathException
* with an error code
* {@link DynamicMetapathException#DYNAMIC_CONTEXT_ABSENT} if a
* document loader is not configured for this dynamic context
*/
@NonNull
public IDocumentLoader getDocumentLoader() {
IDocumentLoader retval = sharedState.documentLoader;
if (retval == null) {
throw new DynamicMetapathException(DynamicMetapathException.DYNAMIC_CONTEXT_ABSENT,
"No document loader configured for the dynamic context.");
}
return retval;
}
/**
* Assign a document loader to this dynamic context.
*
* @param documentLoader
* the document loader to assign
*/
public void setDocumentLoader(@NonNull IDocumentLoader documentLoader) {
this.sharedState.documentLoader = new CachingLoader(documentLoader);
}
/**
* Get the cached function call result for evaluating a function that has the
* property {@link FunctionProperty#DETERMINISTIC}.
*
* @param callingContext
* the function calling context information that distinguishes the call
* from any other call
* @return the cached result sequence for the function call
*/
@Nullable
public ISequence<?> getCachedResult(@NonNull CallingContext callingContext) {
return sharedState.functionResultCache.get(callingContext);
}
/**
* Cache a function call result for a that has the property
* {@link FunctionProperty#DETERMINISTIC}.
*
* @param callingContext
* the calling context information that distinguishes the call from any
* other call
* @param result
* the function call result
*/
public void cacheResult(@NonNull CallingContext callingContext, @NonNull ISequence<?> result) {
ISequence<?> old = sharedState.functionResultCache.put(callingContext, result);
assert old == null;
}
/**
* Used to disable the evaluation of predicate expressions during Metapath
* evaluation.
* <p>
* This can be useful for determining the potential targets identified by a
* Metapath expression as a partial evaluation, without evaluating that these
* targets match the predicate.
*
* @return this dynamic context
*/
@NonNull
public DynamicContext disablePredicateEvaluation() {
this.sharedState.configuration.disableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
return this;
}
/**
* Used to enable the evaluation of predicate expressions during Metapath
* evaluation.
* <p>
* This is the default behavior if unchanged.
*
* @return this dynamic context
*/
@NonNull
public DynamicContext enablePredicateEvaluation() {
this.sharedState.configuration.enableFeature(MetapathEvaluationFeature.METAPATH_EVALUATE_PREDICATES);
return this;
}
/**
* Get the Metapath evaluation configuration.
*
* @return the configuration
*/
@NonNull
public IConfiguration<MetapathEvaluationFeature<?>> getConfiguration() {
return sharedState.configuration;
}
/**
* Get the sequence value assigned to a let variable with the provided qualified
* name.
*
* @param name
* the variable qualified name
* @return the non-null variable value
* @throws MetapathException
* of the variable has not been assigned or if the variable value is
* {@code null}
*/
@NonNull
public ISequence<?> getVariableValue(@NonNull QName name) {
ISequence<?> retval = letVariableMap.get(name);
if (retval == null) {
if (!letVariableMap.containsKey(name)) {
throw new MetapathException(String.format("Variable '%s' not defined in context.", name));
}
throw new MetapathException(String.format("Variable '%s' has null contents.", name));
}
return retval;
}
/**
* Bind the variable {@code name} to the sequence {@code value}.
*
* @param name
* the name of the variable to bind
* @param boundValue
* the value to bind to the variable
* @return this dynamic context
*/
@NonNull
public DynamicContext bindVariableValue(@NonNull QName name, @NonNull ISequence<?> boundValue) {
letVariableMap.put(name, boundValue);
return this;
}
private class CachingLoader implements IDocumentLoader {
@NonNull
private final IDocumentLoader proxy;
public CachingLoader(@NonNull IDocumentLoader proxy) {
this.proxy = proxy;
}
@Override
public IUriResolver getUriResolver() {
return new ContextUriResolver();
}
@Override
public void setUriResolver(@NonNull IUriResolver resolver) {
// we delegate to the document loader proxy, so the resolver should be set there
throw new UnsupportedOperationException("Set the resolver on the proxy");
}
@NonNull
protected IDocumentLoader getProxiedDocumentLoader() {
return proxy;
}
@Override
public IDocumentNodeItem loadAsNodeItem(URI uri) throws IOException {
IDocumentNodeItem retval = sharedState.availableDocuments.get(uri);
if (retval == null) {
retval = getProxiedDocumentLoader().loadAsNodeItem(uri);
sharedState.availableDocuments.put(uri, retval);
}
return retval;
}
public class ContextUriResolver implements IUriResolver {
/**
* {@inheritDoc}
* <p>
* This method first resolves the provided URI against the static context's base
* URI.
*/
@Override
public URI resolve(URI uri) {
URI baseUri = getStaticContext().getBaseUri();
URI resolvedUri;
if (baseUri == null) {
resolvedUri = uri;
} else {
resolvedUri = ObjectUtils.notNull(baseUri.resolve(uri));
}
IUriResolver resolver = getProxiedDocumentLoader().getUriResolver();
return resolver == null ? resolvedUri : resolver.resolve(resolvedUri);
}
}
}
}