blob: 7bfd68f71446caa8846054af2d9b0f572aa81e04 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015-2016 The University of York.
*
* 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.hawk.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hawk.core.IModelIndexer;
import org.hawk.core.graph.IGraphEdge;
import org.hawk.core.graph.IGraphNode;
import org.hawk.graph.updater.GraphModelUpdater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Read-only abstraction of a model element within the graph populated by this
* updater. Note: this class does not deal with transactions at all - the caller
* should handle it.
*/
public class ModelElementNode {
public static final String OBJECT_VERTEX_LABEL = "eobject";
public static final String EDGE_PROPERTY_CONTAINER = "isContainer";
public static final String EDGE_PROPERTY_CONTAINMENT = "isContainment";
/**
* Label for the transient edge from the model element node to its file. Changed
* in Hawk 1.2.0 to avoid conflict with MoDisco metamodel.
*/
public static final String EDGE_LABEL_FILE = "_hawkFile";
/**
* Label for the transient edge from the model element node to its type and all
* its supertypes.
*/
public static final String EDGE_LABEL_OFKIND = "_hawkOfKind";
/** Label for the transient edge from the node to its immediate type. */
public static final String EDGE_LABEL_OFTYPE = "_hawkOfType";
/** Labels for all the transient edges from a model element node. */
public static final List<String> TRANSIENT_EDGE_LABELS = Arrays.asList(
EDGE_LABEL_FILE, EDGE_LABEL_OFKIND, EDGE_LABEL_OFTYPE
);
public static final Set<String> TRANSIENT_ATTRIBUTES = new HashSet<>(Arrays.asList(IModelIndexer.IDENTIFIER_PROPERTY, IModelIndexer.SIGNATURE_PROPERTY));
private static final Logger LOGGER = LoggerFactory.getLogger(ModelElementNode.class);
private final IGraphNode node;
// never access this field directly: always call getTypeNode(),
// as we use lazy initialization.
private TypeNode typeNode;
private FileNode fileNode;
/** Prefix for any derived edges (e.g. computed through EOL). */
public static final String DERIVED_EDGE_PREFIX = "de";
public ModelElementNode(IGraphNode node) {
this.node = node;
}
/**
* Returns the type node for this model element node.
*/
public TypeNode getTypeNode() {
if (typeNode == null) {
final IGraphNode rawTypeNode = getFirstEndNode(EDGE_LABEL_OFTYPE);
typeNode = new TypeNode(rawTypeNode);
}
return typeNode;
}
/**
* Returns the type nodes for the supertypes of this model element node,
* excluding its own type (which can be obtained through {@link #getTypeNode()}.
*/
public List<TypeNode> getKindNodes() {
final List<TypeNode> nodes = new ArrayList<>();
for (IGraphEdge outEdge : node.getOutgoingWithType(EDGE_LABEL_OFKIND)) {
nodes.add(new TypeNode(outEdge.getEndNode()));
}
return nodes;
}
/**
* Returns the file node for this model element node.
*/
public FileNode getFileNode() {
if (fileNode == null) {
final IGraphNode rawFileNode = getFirstEndNode(EDGE_LABEL_FILE);
fileNode = new FileNode(rawFileNode);
}
return fileNode;
}
/**
* Returns the file nodes for this model element node. Model element nodes
* with more than one file node can happen with models based on global UUIDs
* (e.g. Modelio models).
*/
public List<FileNode> getFileNodes() {
final List<FileNode> nodes = new ArrayList<>();
for (IGraphEdge outEdge : node.getOutgoingWithType(EDGE_LABEL_FILE)) {
nodes.add(new FileNode(outEdge.getEndNode()));
}
return nodes;
}
/**
* Fills in the <code>attributeValues</code> and
* <code>referenceValues</code> maps with the contents of the slots of the
* <code>modelElementNode</code>. Derived attributes and reverse references
* are *not* included either. If a <code>null</code> value is passed, that
* type of slot will not be read.
*
* @throws Exception
* Could not begin the transaction on the graph.
*/
public void getSlotValues(Map<String, Object> attributeValues, Map<String, Object> referenceValues, Map<String, Object> mixedValues, Map<String, Object> derivedValues) {
final Collection<Slot> slots = getTypeNode().getSlots().values();
for (Slot s : slots) {
if (s.isAttribute() && attributeValues != null) {
final Object value = getSlotValue(s);
if (value == null) continue;
attributeValues.put(s.getName(), value);
} else if (s.isReference() && referenceValues != null) {
final Object value = getSlotValue(s);
if (value == null) continue;
referenceValues.put(s.getName(), value);
} else if (s.isMixed() && mixedValues != null) {
final Object value = getSlotValue(s);
if (value == null) continue;
mixedValues.put(s.getName(), value);
} else if (s.isDerived()) {
final Object value = getSlotValue(s);
if (value == null) continue;
derivedValues.put(s.getName(), value);
}
}
}
/**
* Returns the value of the non-derived <code>slot</code> for this model element node.
* The slot should be among those returned by {@link TypeNode#getSlots()}
* for this model element node.
*/
public Object getSlotValue(Slot slot) {
Object rawValue = null;
if (slot.isDerived()) {
// It's a regular derived property
for (IGraphEdge r : node.getOutgoingWithType(slot.getName())) {
if (rawValue == null) {
final IGraphNode dpNode = r.getEndNode();
rawValue = dpNode.getProperty(slot.getName());
if (rawValue == null) {
// Derived edges are not stored as a property, but rather
// as actual edges from the derived property node to the target
Iterator<IGraphEdge> dEdges = dpNode.getOutgoingWithType(DERIVED_EDGE_PREFIX + slot.getName()).iterator();
if (dEdges.hasNext()) {
List<IGraphNode> targets = new ArrayList<>();
while (dEdges.hasNext()) {
targets.add(dEdges.next().getEndNode());
}
return targets;
}
}
} else {
throw new IllegalStateException("WARNING: a derived property node (arity 1) -- ( " + slot.getName()
+ " ) has more than 1 links in store!");
}
}
} else {
rawValue = node.getProperty(slot.getName());
}
return decodeRawValue(slot, rawValue);
}
protected Object decodeRawValue(Slot slot, final Object rawValue) {
final Collection<Object> collection = slot.getCollection();
if (slot.isMany() && rawValue != null && (slot.isAttribute() || slot.isMixed() || slot.isDerived())) {
final Class<?> componentType = rawValue.getClass().getComponentType();
if (componentType == null) {
// Derived reference that has not been computed yet, or there was an error evaluating it - no value for now
return null;
} else if (!componentType.isPrimitive()) {
// non-primitive arrays can be cast to Object[]
collection.addAll(Arrays.asList((Object[]) rawValue));
} else if (componentType == double.class) {
// primitive arrays need to be explicitly cast, and then we have to box the values
for (double v : (double[])rawValue) collection.add(v);
} else if (componentType == float.class) {
for (float v : (float[])rawValue) collection.add(v);
} else if (componentType == long.class) {
for (long v : (long[])rawValue) collection.add(v);
} else if (componentType == int.class) {
for (int v : (int[])rawValue) collection.add(v);
} else if (componentType == short.class) {
for (int v : (short[])rawValue) collection.add(v);
} else if (componentType == byte.class) {
for (byte v : (byte[])rawValue) collection.add(v);
} else if (componentType == char.class) {
for (char v : (char[])rawValue) collection.add(v);
} else if (componentType == byte.class) {
for (byte v : (byte[])rawValue) collection.add(v);
} else if (componentType == boolean.class) {
for (boolean v : (boolean[])rawValue) collection.add(v);
}
}
if (slot.isReference() || slot.isMixed()) {
for (IGraphEdge r : node.getOutgoingWithType(slot.getName())) {
final Object id = r.getEndNode().getId();
collection.add(id);
}
}
if (slot.isMany()) {
return collection;
} else if (slot.isAttribute()) {
return rawValue;
} else if (collection.size() == 1) {
return collection.iterator().next();
} else if (collection.isEmpty()) {
return null;
} else {
throw new IllegalArgumentException(String.format(
"A relationship with arity 1 (%s) had %d links",
slot.getName(), collection.size()));
}
}
public IGraphNode getNode() {
return node;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((node == null) ? 0 : node.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ModelElementNode other = (ModelElementNode) obj;
if (node == null) {
if (other.node != null)
return false;
} else if (!node.equals(other.node))
return false;
return true;
}
/**
* Returns <code>true</code> if this model element is the root of the
* {@link FileNode} it is part of. Note that a root model element might be
* still contained within another model element in a different file node.
*
* TODO: check into why some non-root model elements in Modelio do not have
* a container.
*/
public boolean isRoot() {
return getFileNode().isRoot(this);
}
public ModelElementNode getContainer() {
for (IGraphEdge edge : node.getOutgoing()) {
if (edge.getProperty(EDGE_PROPERTY_CONTAINER) != null) {
return new ModelElementNode(edge.getEndNode());
}
}
for (IGraphEdge edge : node.getIncoming()) {
if (edge.getProperty(EDGE_PROPERTY_CONTAINMENT) != null) {
return new ModelElementNode(edge.getStartNode());
}
}
return null;
}
public boolean isContainment(String featureName) {
return outgoingEdgeWithTypeHasProperty(featureName, EDGE_PROPERTY_CONTAINMENT);
}
public boolean isContainer(String featureName) {
return outgoingEdgeWithTypeHasProperty(featureName, EDGE_PROPERTY_CONTAINER);
}
protected IGraphNode getFirstEndNode(final String edgeLabel) {
final IGraphNode rawTypeNode = node
.getOutgoingWithType(edgeLabel).iterator().next()
.getEndNode();
return rawTypeNode;
}
private boolean outgoingEdgeWithTypeHasProperty(String featureName, final String propertyName) {
Iterable<IGraphEdge> edges = getNode().getOutgoingWithType(featureName);
for (IGraphEdge edge : edges) {
if (featureName.equals(edge.getType())) {
return edge.getProperty(propertyName) != null;
}
}
return false;
}
/**
* Returns the graph node identifier for this model element. For Neo4j
* databases, this will be usually an integer.
*/
public String getNodeId() {
return getNode().getId().toString();
}
/**
* Returns the model element identifier for this model element. For EMF
* models, this will usually be the URI fragment within the resource.
*/
public String getElementId() {
return getNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString();
}
/**
* Returns <code>true</code> if this model element is contained by any other,
* regardless of whether the container is on a different file.
*/
public boolean isContained() {
return getContainer() != null;
}
public boolean hasChildren() {
for (IGraphEdge edge : node.getOutgoing()) {
if (edge.getProperty(EDGE_PROPERTY_CONTAINMENT) != null) {
return true;
}
}
for (IGraphEdge edge : node.getIncoming()) {
if (edge.getProperty(EDGE_PROPERTY_CONTAINER) != null) {
return true;
}
}
return false;
}
/**
* Returns <code>true</code> if this model element is contained directly or
* indirectly by the provided file within the provided repository. Null values
* can be provided to check only the repository or only the file.
*/
public boolean isContainedWithin(final String containerRepository, final String containerPath) {
for (ModelElementNode men = this; men != null; men = men.getContainer()) {
final FileNode fn = men.getFileNode();
final String repositoryURL = fn.getRepositoryURL();
final String filePath = fn.getFilePath();
if ((containerRepository == null || repositoryURL.equals(containerRepository))
&& (containerPath == null || filePath.equals(containerPath))) {
return true;
}
}
return false;
}
public boolean isOfKind(String metaClass) {
return isOf(metaClass, EDGE_LABEL_OFKIND) || isOf(metaClass, EDGE_LABEL_OFTYPE);
}
public boolean isOfType(String metaClass) {
return isOf(metaClass, EDGE_LABEL_OFTYPE);
}
public boolean isOfKind(IGraphNode typeNode) {
return isOf(typeNode, EDGE_LABEL_OFKIND) || isOf(typeNode, EDGE_LABEL_OFTYPE);
}
public boolean isOfType(IGraphNode typeNode) {
return isOf(typeNode, EDGE_LABEL_OFTYPE);
}
public boolean isOfKind(TypeNode targetTypeNode) {
return isOfKind(targetTypeNode.getNode());
}
public boolean isOfType(TypeNode typeNode) {
return isOfType(typeNode.getNode());
}
protected boolean isOf(IGraphNode typeNode, String edgeLabelOftype) {
for (IGraphEdge edge : node.getOutgoingWithType(edgeLabelOftype)) {
if (edge.getEndNode().getId().equals(typeNode.getId())) {
return true;
}
}
return false;
}
protected boolean isOf(String metaClass, final String edgeLabel) {
for (IGraphEdge edge : node.getOutgoingWithType(edgeLabel)) {
TypeNode tn = new TypeNode(edge.getEndNode());
if (metaClass.equals(tn.getTypeName())) {
return true;
}
}
return false;
}
/**
* Returns the 'local root' that contains this model element node: the topmost
* element in the containment tree of the same file as this element.
*/
public ModelElementNode getLocalRoot() {
final FileNode fileNode = getFileNode();
ModelElementNode localRoot = this;
ModelElementNode container = localRoot.getContainer();
while (container != null && container.getFileNode().equals(fileNode)) {
localRoot = container;
container = localRoot.getContainer();
}
return localRoot;
}
/**
* Returns all the unresolved proxy reference lists for this model element node.
* Each list contains all the proxy references to a missing model file from this
* model element.
*/
public List<ProxyReferenceList> getProxies() {
List<ProxyReferenceList> proxies = new ArrayList<>();
for (String propertyKey : node.getPropertyKeys()) {
if (propertyKey.startsWith(GraphModelUpdater.PROXY_REFERENCE_PREFIX)) {
final String[] propertyValue = (String[]) node.getProperty(propertyKey);
if (propertyValue.length > 0) {
proxies.add(new ProxyReferenceList(node, propertyValue));
} else {
// TODO debug and fix?
LOGGER.warn("Proxy ref list is empty: node {}, key {}", node, propertyKey);
}
}
}
return proxies;
}
}