blob: 96dfb3a957afb10bcbdcbaa0dfae11ce5a9bfd83 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018-2019 Aston University.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
*
* Contributors:
* Antonio Garcia-Dominguez - initial API and implementation
******************************************************************************/
package org.eclipse.hawk.timeaware.queries;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.epsilon.eol.IEolModule;
import org.eclipse.epsilon.eol.exceptions.models.EolModelLoadingException;
import org.eclipse.epsilon.eol.execute.operations.contributors.OperationContributorRegistry;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.IStateListener.HawkState;
import org.eclipse.hawk.core.graph.IGraphEdge;
import org.eclipse.hawk.core.graph.IGraphIterable;
import org.eclipse.hawk.core.graph.IGraphNode;
import org.eclipse.hawk.core.graph.IGraphTransaction;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphDatabase;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNode;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNodeIndex;
import org.eclipse.hawk.core.query.InvalidQueryException;
import org.eclipse.hawk.core.query.QueryExecutionException;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine;
import org.eclipse.hawk.epsilon.emc.contextful.AllOf;
import org.eclipse.hawk.epsilon.emc.contextful.TypeFirstAllOf;
import org.eclipse.hawk.epsilon.emc.pgetters.GraphPropertyGetter;
import org.eclipse.hawk.epsilon.emc.wrappers.FileNodeWrapper;
import org.eclipse.hawk.graph.FileNode;
import org.eclipse.hawk.graph.GraphWrapper;
import org.eclipse.hawk.graph.ModelElementNode;
import org.eclipse.hawk.graph.ProxyReferenceList;
import org.eclipse.hawk.graph.updater.GraphModelBatchInjector;
import org.eclipse.hawk.graph.updater.GraphModelUpdater;
import org.eclipse.hawk.timeaware.graph.VCSManagerIndex;
import org.eclipse.hawk.timeaware.graph.VCSManagerIndex.RepositoryNode;
import org.eclipse.hawk.timeaware.queries.operations.reflective.TimeAwareNodeHistoryOperationContributor;
import org.eclipse.hawk.timeaware.queries.operations.reflective.TypeHistoryOperationContributor;
import org.eclipse.hawk.timeaware.queries.operations.scopes.IScopingTimeAwareGraphNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Variant of {@link EOLQueryEngine} that exposes the time-aware nature of the
* index through EOL. Both types and model elements have the same set of
* time-aware properties and references: <code>.time</code>,
* <code>.earliest</code>, <code>.latest</code>, <code>.versions</code>.
*
* TODO: integrate a "human time to seconds since epoch UTC" library.
*
* TODO: allow for revision-based times as well.
*/
public class TimeAwareEOLQueryEngine extends EOLQueryEngine {
/**
* Wraps a standalone node while remembering its original time, so we can
* retrieve the exact same node later if the weak reference has been GC'ed.
*/
public class TimeAwareGraphNodeWrapper extends GraphNodeWrapper {
private long time;
protected TimeAwareGraphNodeWrapper(IGraphNode n, TimeAwareEOLQueryEngine containerModel) {
super(n, containerModel);
this.time = ((ITimeAwareGraphNode) n).getTime();
}
@Override
public IGraphNode getNode() {
IGraphNode ret = node.get();
if (ret == null) {
TimeAwareEOLQueryEngine taQuery = (TimeAwareEOLQueryEngine)containerModel;
ret = taQuery.getBackend().getNodeById(id).travelInTime(time);
node = new WeakReference<IGraphNode>(ret);
}
return ret;
}
}
/**
* Variant of {@link GraphNodeWrapper} which keeps a strong reference, in case
* we cannot be sure that we can simply retrieve the exact original node through
* ID and optionally time. This is needed for the scoping wrappers in the
* time-aware query DSL, as recreating those would essentially require
* re-running the query itself.
*/
public class StrongGraphNodeWrapper extends GraphNodeWrapper {
private IGraphNode node;
protected StrongGraphNodeWrapper(IGraphNode n, EOLQueryEngine containerModel) {
super(n, containerModel);
this.node = n;
}
@Override
public IGraphNode getNode() {
return node;
}
}
/**
* This is the default 'all of' provider. It will go to the type node and follow
* all 'is of type' and/or 'is of kind' edges in reverse, with no filtering.
*/
protected class ContextlessAllOf implements AllOf {
public void addAllOf(IGraphNode typeNode, final String typeorkind, Collection<Object> nodes) {
for (IGraphEdge n : typeNode.getIncomingWithType(typeorkind)) {
nodes.add(new TimeAwareGraphNodeWrapper(n.getStartNode(), TimeAwareEOLQueryEngine.this));
}
broadcastAllOfXAccess(nodes);
}
}
/**
* This is the default 'all files' provider. Without a source node, it will
* return all available file nodes just like the regular EOL query engine. With
* a source node, it will return the file nodes on the same instant as that
* node.
*/
protected class ContextlessTimeAwareAllFiles implements Function<IGraphNode, Iterable<? extends IGraphNode>> {
public Iterable<? extends ITimeAwareGraphNode> apply(IGraphNode sourceNode) {
final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
if (sourceNode == null) {
return taGraph.allNodes(FileNode.FILE_NODE_LABEL);
} else {
final ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) sourceNode;
return taGraph.allNodes(FileNode.FILE_NODE_LABEL, taNode.getTime());
}
}
}
/**
* This is a contextful 'all files' provider, which will use glob patterns to
* filter the files to be returned. If there is a context node, the files to be
* filtered will be those that existed at the same instant.
*/
protected class GlobPatternTimeAwareAllFiles implements Function<IGraphNode, Iterable<? extends IGraphNode>> {
private final List<String> rplist;
private final List<String> fplist;
protected GlobPatternTimeAwareAllFiles(List<String> rplist, List<String> fplist) {
this.rplist = rplist;
this.fplist = fplist;
}
@Override
public Iterable<? extends ITimeAwareGraphNode> apply(IGraphNode sourceNode) {
final ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) sourceNode;
final ITimeAwareGraphDatabase tadb = (ITimeAwareGraphDatabase) graph;
final GraphWrapper gw = new GraphWrapper(tadb);
try (IGraphTransaction tx = tadb.beginTransaction()) {
Set<FileNode> fileNodes;
if (sourceNode == null) {
fileNodes = gw.getFileNodes(rplist, fplist);
} else {
final ITimeAwareGraphNodeIndex fileIndex = tadb.getFileIndex().travelInTime(taNode.getTime());
fileNodes = gw.getFileNodes(fileIndex, rplist, fplist);
}
final Set<ITimeAwareGraphNode> rawFileNodes = new HashSet<>();
for (FileNode fn : fileNodes) {
rawFileNodes.add((ITimeAwareGraphNode) fn.getNode());
}
tx.success();
return rawFileNodes;
} catch (Exception e) {
LOGGER.error("Failed to find matching files", e);
return Collections.emptySet();
}
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(TimeAwareEOLQueryEngine.class);
/**
* Provides a set of file nodes, based on some other node. {@link #getFiles()}
* and other similar methods rely on this, so we may use composition rather than
* inheritance for this type of behaviour.
*/
private Function<IGraphNode, Iterable<? extends IGraphNode>> allFiles;
/**
* Provides on demand a list of instances of a type.
*/
private AllOf allOf;
public TimeAwareEOLQueryEngine() {
this.allFiles = new ContextlessTimeAwareAllFiles();
this.allOf = new ContextlessAllOf();
}
@Override
public String getType() {
return getClass().getCanonicalName();
}
@Override
protected GraphPropertyGetter createContextlessPropertyGetter() {
return new TimeAwareGraphPropertyGetter(graph, this);
}
/**
* Extends "allInstances" with the concept of time. It is likely to be used
* through the {@link #allInstancesNow()} convenience function.
*
* The regular "allInstances" can still be used in timeline queries, where the
* global timepoint is changed.
*/
public Collection<?> allInstancesAt(long timepoint) {
final Set<Object> allContents = new HashSet<Object>();
final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
for (IGraphNode node : taGraph.allNodes(ModelElementNode.OBJECT_VERTEX_LABEL, timepoint)) {
GraphNodeWrapper wrapper = new TimeAwareGraphNodeWrapper(node, this);
allContents.add(wrapper);
}
return allContents;
}
/**
* Version of {@link #allInstancesAt(long)} required to work around a limitation
* in Epsilon's reflective operation lookup, which does not seem to consider the
* promotion of integer values into long values.
*/
public Collection<?> allInstancesAt(int timepoint) {
return allInstancesAt((long) timepoint);
}
/**
* Returns all the instances in the model at this current moment, by visiting
* the type nodes at the current point in time.
*/
public Collection<?> allInstancesNow() {
return allInstancesAt(System.currentTimeMillis());
}
/**
* Returns the proxy reference lists active at a certain point in time, within
* repositories whose URL matches the specified prefix.
*
* @param repositoryPrefix URL prefix to filter repositories with.
* @param timepoint Timepoint in milliseconds at which proxies should be listed.
* @throws Exception There was a problem querying the graph.
*/
public List<ProxyReferenceList> proxiesAt(String repositoryPrefix, long timepoint) throws Exception {
final Set<Object> visitedIDs = new HashSet<>();
final List<ProxyReferenceList> proxyReferenceLists = new ArrayList<>();
final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
try (IGraphTransaction tx = graph.beginTransaction()) {
ITimeAwareGraphNodeIndex idxProxies = taGraph
.getOrCreateNodeIndex(GraphModelBatchInjector.PROXY_DICT_NAME)
.travelInTime(timepoint);
IGraphIterable<? extends IGraphNode> proxies = idxProxies
.query(GraphModelUpdater.PROXY_REFERENCE_PREFIX, repositoryPrefix + "*");
for (IGraphNode n : proxies) {
if (visitedIDs.add(n.getId())) {
ModelElementNode men = new ModelElementNode(n);
proxyReferenceLists.addAll(men.getProxies());
}
}
tx.success();
}
return proxyReferenceLists;
}
/**
* Convenience version of {@link #proxiesAt(String, long)} focused on the
* current timepoint, while allowing the user to specify a repository URL
* prefix.
*/
public List<ProxyReferenceList> proxiesNow(String repositoryPrefix) throws Exception {
return proxiesAt(repositoryPrefix, System.currentTimeMillis());
}
/**
* Convenience version of {@link #proxiesAt(String, long)} which goes through
* all repositories at the current timepoint.
*/
public List<ProxyReferenceList> proxiesNow() throws Exception {
return proxiesNow("");
}
/**
* Returns all the files indexed at a specific point in time.
*
* @param timepoint Timepoint whose files we want to list.
*/
public Set<FileNodeWrapper> getFilesAt(long timepoint) {
final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
Set<FileNodeWrapper> allFNW = new HashSet<>();
for (IGraphNode n : taGraph.allNodes(FileNode.FILE_NODE_LABEL, timepoint)) {
allFNW.add(new FileNodeWrapper(new FileNode(n), this));
}
return allFNW;
}
/**
* Returns all the files indexed in the latest version of the graph.
*/
public Set<FileNodeWrapper> getFilesNow() {
return getFilesAt(System.currentTimeMillis());
}
@Override
public Object query(IModelIndexer m, String query, Map<String, Object> context)
throws InvalidQueryException, QueryExecutionException {
final HawkState currentState = m.getCompositeStateListener().getCurrentState();
if (currentState != HawkState.RUNNING) {
throw new QueryExecutionException(
String.format("Cannot run the query, as the indexer is not in the RUNNING state: it is %s instead.",
currentState));
}
if (!(m.getGraph() instanceof ITimeAwareGraphDatabase)) {
throw new QueryExecutionException(getClass().getName() + " can only be used with time-aware backends");
}
String defaultnamespaces = null;
if (context != null) {
defaultnamespaces = (String) context.get(PROPERTY_DEFAULTNAMESPACES);
}
try (TimeAwareEOLQueryEngine q = new TimeAwareEOLQueryEngine()) {
q.load(m);
q.setDefaultNamespaces(defaultnamespaces);
if (context == null || context.isEmpty()) {
// nothing to do!
} else {
q.setContext(context);
}
final IEolModule module = createModule();
module.getContext().setOperationFactory(new TimeAwareEOLOperationFactory(q));
final OperationContributorRegistry opcRegistry = module.getContext().getOperationContributorRegistry();
opcRegistry.add(new TimeAwareNodeHistoryOperationContributor(q));
opcRegistry.add(new TypeHistoryOperationContributor(q));
parseQuery(query, context, q, module);
return q.runQuery(module);
} catch (EolModelLoadingException e) {
throw new QueryExecutionException("Loading of EOLQueryEngine failed");
}
}
@Override
public String getHumanReadableName() {
return "Time Aware " + super.getHumanReadableName();
}
@Override
public Collection<Object> getAllOf(IGraphNode typeNode, String typeorkind) {
final Collection<Object> nodes = createAllOfCollection(typeNode);
allOf.addAllOf(typeNode, typeorkind, nodes);
return nodes;
}
@Override
public Set<FileNodeWrapper> getFiles() {
Set<FileNodeWrapper> allFNW = new HashSet<>();
for (IGraphNode rawNode : allFiles.apply(null)) {
allFNW.add(new FileNodeWrapper(new FileNode(rawNode), this));
}
return allFNW;
}
@Override
public ITimeAwareGraphDatabase getBackend() {
return (ITimeAwareGraphDatabase) super.getBackend();
}
/**
* Returns the {@link RepositoryNode} associated with this instance node.
*
* @throws Exception Error while travelling in time to the matching instant on
* the repository node.
*/
public RepositoryNode getRepository(Object oInstanceNode) throws Exception {
if (oInstanceNode instanceof GraphNodeWrapper) {
GraphNodeWrapper gnw = (GraphNodeWrapper) oInstanceNode;
if (gnw.getNode() instanceof ITimeAwareGraphNode) {
ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) gnw.getNode();
ModelElementNode me = new ModelElementNode(taNode);
String repoURL = me.getFileNode().getRepositoryURL();
final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
final RepositoryNode repoNode = new VCSManagerIndex(taGraph).getOrCreateRepositoryNode(repoURL);
return repoNode.travelInTime(taNode.getTime());
}
}
return null;
}
private void setAllFiles(Function<IGraphNode, Iterable<? extends IGraphNode>> allFiles) {
this.allFiles = allFiles;
}
private void setAllOf(AllOf allOf) {
this.allOf = allOf;
}
/**
* Loads up the specified context into this object, changing its 'all of' and
* 'all files' providers.
*/
private void setContext(Map<String, Object> context) {
// TODO use composition over existing property getter for context restriction -
// needed for traversal scoping
if (context.containsKey(PROPERTY_FILECONTEXT) || context.containsKey(PROPERTY_REPOSITORYCONTEXT)) {
// Set up file/repository pattern lists
final String sFilePatterns = (String) context.get(PROPERTY_FILECONTEXT);
final String[] filePatterns = (sFilePatterns != null && sFilePatterns.trim().length() != 0)
? sFilePatterns.split(",")
: null;
final List<String> fplist = (filePatterns != null) ? Arrays.asList(filePatterns) : null;
final String sRepoPatterns = (String) context.get(PROPERTY_REPOSITORYCONTEXT);
final String[] repoPatterns = (sRepoPatterns != null && sRepoPatterns.trim().length() != 0)
? sRepoPatterns.split(",")
: null;
final List<String> rplist = (repoPatterns != null) ? Arrays.asList(repoPatterns) : null;
/*
* Create suppliers for allFiles and allOf, so we may support file/repository
* contexts.
*
* TODO - currently limited to file first (no subtree, no derived allOf support
* yet).
*/
final GlobPatternTimeAwareAllFiles innerAllFiles = new GlobPatternTimeAwareAllFiles(rplist, fplist);
setAllFiles(innerAllFiles);
setAllOf(new TypeFirstAllOf(innerAllFiles, this));
}
}
@Override
public GraphNodeWrapper wrap(IGraphNode node) {
/*
* If we're wrapping a scoping node, then it has to be a strong reference-based one.
* Otherwise, use a weak reference-based one to save memory.
*/
if (node instanceof IScopingTimeAwareGraphNode) {
return new StrongGraphNodeWrapper(node, this);
} else {
return new TimeAwareGraphNodeWrapper(node, this);
}
}
}