blob: 254421be27a22d2db1342a4083b70f143bbd5f2c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2019 The University of York, 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:
* Konstantinos Barmpis - initial API and implementation
* Antonio Garcia-Dominguez - collect errors in list instead of printing them
******************************************************************************/
package org.hawk.graph.syncValidationListener;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.hawk.core.IModelIndexer;
import org.hawk.core.VcsCommitItem;
import org.hawk.core.graph.IGraphChangeListener;
import org.hawk.core.graph.IGraphDatabase;
import org.hawk.core.graph.IGraphEdge;
import org.hawk.core.graph.IGraphNode;
import org.hawk.core.graph.IGraphNodeIndex;
import org.hawk.core.graph.IGraphTransaction;
import org.hawk.core.model.IHawkAttribute;
import org.hawk.core.model.IHawkClass;
import org.hawk.core.model.IHawkModelResource;
import org.hawk.core.model.IHawkObject;
import org.hawk.core.model.IHawkPackage;
import org.hawk.core.model.IHawkReference;
import org.hawk.core.runtime.ModelIndexerImpl;
import org.hawk.graph.ModelElementNode;
import org.hawk.graph.updater.GraphModelBatchInjector;
import org.hawk.graph.updater.GraphModelInserter;
import org.hawk.graph.updater.GraphModelUpdater;
public class SyncValidationListener implements IGraphChangeListener {
public class ValidationError {
private final String message;
public ValidationError(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
private ModelIndexerImpl hawk;
// stats
private List<ValidationError> errors = new ArrayList<>();
private int removedProxies, deleted, malformed, singletonCount, totalGraphSize, totalResourceSizes;
private Set<String> changed = new HashSet<>();
private IGraphNodeIndex singletonIndex;
private boolean singletonIndexIsEmpty;
public SyncValidationListener() {
// osgi constructor
}
@Override
public void setModelIndexer(IModelIndexer hawk) {
this.hawk = (ModelIndexerImpl) hawk;
if (this.hawk != null) {
System.err.println("SyncValidationListener: hawk.setSyncMetricsEnabled(true) called, performance will suffer!");
hawk.setSyncMetricsEnabled(true);
}
}
@Override
public String getName() {
return "Sync Validation Listener";
}
@Override
public void synchroniseStart() {
// nothing to do
}
@Override
public void synchroniseEnd() {
try {
if (hawk != null) {
validateChanges();
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
deleted = 0;
changed.clear();
}
public List<ValidationError> getErrors() {
return errors;
}
public int getTotalErrors() {
return errors.size();
}
private void validateChanges() throws URISyntaxException {
assert this.hawk != null : "validateChanges() should only be called if the indexer has been set";
System.err.println("sync metrics:");
System.err.println("interesting\t" + hawk.getInterestingFiles());
System.err.println("deleted\t\t" + hawk.getDeletedFiles());
System.err.println("changed\t\t" + hawk.getCurrChangedItems());
System.err.println("loaded\t\t" + hawk.getLoadedResources());
System.err.println("c elems\t\t" + latestChangedElements());
System.err.println("d elems\t\t" + latestDeletedElements());
System.err.println("time\t\t~" + hawk.getLatestSynctime() / 1000 + "s");
System.err.println("validating changes...");
errors.clear();
removedProxies = 0;
malformed = 0;
singletonCount = 0;
totalResourceSizes = 0;
totalGraphSize = 0;
final URI tempURI = new File(hawk.getGraph().getTempDir()).toURI();
// for all non-null resources
if (hawk.getFileToResourceMap() != null) {
for (VcsCommitItem c : hawk.getFileToResourceMap().keySet()) {
validateChanges(tempURI, c);
}
}
System.err.println("changed resource size: " + totalResourceSizes);
System.err.println("relevant graph size: "
+ totalGraphSize
+ (singletonCount > 0 ? (" + singleton count: " + singletonCount) : ""));
if (totalGraphSize + singletonCount != totalResourceSizes) {
errors.add(new ValidationError(
String.format("Mismatched resource size: %d + %d != %d", totalGraphSize, singletonCount, totalResourceSizes)
));
}
System.err.println("validated changes... "
+ (errors.isEmpty() ? "true"
: ((errors.size() == malformed) + " (with "
+ errors.size() + " total and "
+ malformed + " malformed errors)"))
+ (removedProxies == 0 ? "" : " [" + removedProxies
+ "] unresolved hawk proxies matched"));
}
protected void validateChanges(final URI tempURI, final VcsCommitItem c) throws URISyntaxException {
final String repository = c.getCommit().getDelta().getManager().getLocation();
final String repoURL = repository;
final IHawkModelResource r = hawk.getFileToResourceMap().get(c);
if (r == null) {
/*
* file didnt get parsed so no changes are made -- any way to verify this
* further?
*/
return;
}
System.out.println("validating file " + c.getChangeType() + " for " + c.getPath());
final IGraphDatabase graph = hawk.getGraph();
try (IGraphTransaction t = graph.beginTransaction()) {
singletonIndex = graph.getOrCreateNodeIndex(GraphModelBatchInjector.FRAGMENT_DICT_NAME);
singletonIndexIsEmpty = !singletonIndex.query("*", "*").iterator().hasNext();
String file = null;
IGraphNode filenode = null;
try {
file = repository + GraphModelUpdater.FILEINDEX_REPO_SEPARATOR + c.getPath();
filenode = graph.getFileIndex().get("id", file).getSingle();
} catch (Exception ee) {
errors.add(new ValidationError(
String.format(
"Expected file %s but it did not exist "
+ "(maybe metamodel not registered, if so expect +1 errors)",
file)
));
return;
}
// cache model elements in current resource
Map<String, IHawkObject> eObjectCache = new HashMap<>();
// cache of malformed object identifiers (to ignore references)
Set<String> malformedObjectCache = new HashSet<>();
cacheModelElements(c, r, eObjectCache, malformedObjectCache);
// go through all nodes in graph from the file the resource is in
for (IGraphEdge instanceEdge : filenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_FILE)) {
final IGraphNode instance = instanceEdge.getStartNode();
totalGraphSize++;
final IHawkObject eobject = eObjectCache.get(instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));
// if a node cannot be found in the model cache
if (eobject == null) {
// this triggers when a malformed model has 2 identical identifiers
errors.add(new ValidationError(
String.format("Graph contains node with identifier: %s but resource does not!", instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY))
));
} else {
eObjectCache.remove(instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));
if (!malformedObjectCache.contains(eobject.getUri())) {
compareAttributes(instance, eobject);
compareReferences(tempURI, repoURL, instance, eobject);
}
}
}
// if there are model elements not found in nodes
if (eObjectCache.size() > 0) {
errors.add(new ValidationError(
String.format("The following objects were not found in the graph:\n%s", eObjectCache.keySet())
));
}
t.success();
} catch (Exception e) {
System.err.println("syncValidationListener transaction error:");
e.printStackTrace();
}
}
protected void cacheModelElements(VcsCommitItem commitItem, IHawkModelResource modelResource,
Map<String, IHawkObject> eobjectCache, Set<String> malformedObjectCache) {
for (IHawkObject content : modelResource.getAllContents()) {
IHawkObject old = eobjectCache.put(content.getUriFragment(), content);
if (old != null) {
if (!singletonIndexIsEmpty && singletonIndex.get("id", content.getUriFragment()).iterator().hasNext()) {
singletonCount++;
} else {
System.err.println("warning (" + commitItem.getPath() + ") eobjectCache replaced:");
System.err.println(old.getUri() + " | " + old.getUriFragment() + " | ofType: " + old.getType().getName());
System.err.println("with:");
System.err.println(content.getUri() + " | " + content.getUriFragment() + " | ofType: " + content.getType().getName());
malformedObjectCache.add(old.getUri());
malformed++;
System.err
.println("WARNING: MALFORMED MODEL RESOURCE (multiple identical identifiers for:\n"
+ old.getUri()
+ "),\nexpect "
+ malformed
+ " objects in validation.");
}
}
totalResourceSizes++;
}
}
protected void compareReferences(final URI tempURI, final String repoURL, final IGraphNode instance, final IHawkObject eobject) throws URISyntaxException {
final Map<String, Set<String>> modelReferences = computeModelReferences(tempURI, repoURL, eobject);
final Map<String, Set<String>> nodereferences = computeNodeReferences(repoURL, instance);
// compare model and graph reference maps
final Iterator<Entry<String, Set<String>>> rci = modelReferences.entrySet().iterator();
while (rci.hasNext()) {
final Entry<String, Set<String>> modelRef = rci.next();
final String modelRefName = modelRef.getKey();
if (!nodereferences.containsKey(modelRefName)) {
continue;
}
final Set<String> noderefvalues = new HashSet<>(nodereferences.get(modelRefName));
final Set<String> modelrefvalues = new HashSet<>(modelReferences.get(modelRefName));
final Set<String> noderefvaluesclone = new HashSet<>(noderefvalues);
noderefvaluesclone.removeAll(modelrefvalues);
Set<String> modelrefvaluesclone = new HashSet<>(modelrefvalues);
modelrefvaluesclone.removeAll(noderefvalues);
modelrefvaluesclone = removeHawkProxies(instance, modelrefvaluesclone);
filterFragmentBasedReferences(noderefvaluesclone, modelrefvaluesclone);
if (noderefvaluesclone.size() > 0) {
final IGraphNode fileNode = instance.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE).iterator().next().getEndNode();
errors.add(new ValidationError(
String.format(
"Reference %s of node: %s\n"
+ "Located: %s\n%s\n"
+ "The above IDs were found in the graph but not the model",
modelRefName, instance.getProperty(IModelIndexer.IDENTIFIER_PROPERTY),
fileNode, noderefvaluesclone)
));
}
if (modelrefvaluesclone.size() > 0) {
errors.add(new ValidationError(
String.format("Reference %s of model element: %s\n"
+ "Located: %s\n%s\n"
+ "The above IDs were found in the model but not the graph",
modelRefName, eobject.getUriFragment(),
eobject.getUri(), modelrefvaluesclone)
));
}
nodereferences.remove(modelRefName);
// rci.remove();
}
if (nodereferences.size() > 0) {
errors.add(new ValidationError(String.format(
"References %s had targets in the graph but not in the model:\n%s",
nodereferences.keySet(), nodereferences)
));
}
}
protected void filterFragmentBasedReferences(final Set<String> noderefvaluesclone, Set<String> modelrefvaluesclone) {
// Take into account fragment-based references (Modelio)
modelRefs: for (Iterator<String> itModelRefs = modelrefvaluesclone.iterator(); itModelRefs.hasNext();) {
final String modelref = itModelRefs.next();
final int idxHash = modelref.indexOf("#");
final String path = modelref.substring(modelref.indexOf(GraphModelUpdater.FILEINDEX_REPO_SEPARATOR)
+ GraphModelUpdater.FILEINDEX_REPO_SEPARATOR.length(), idxHash);
if (path.equals("/*")) {
final String fragment = modelref.substring(idxHash + 1);
for (Iterator<String> itNodeRefs = noderefvaluesclone.iterator(); itNodeRefs.hasNext();) {
final String noderef = itNodeRefs.next();
if (noderef.endsWith("#" + fragment)) {
itModelRefs.remove();
itNodeRefs.remove();
continue modelRefs;
}
}
}
}
}
protected Map<String, Set<String>> computeNodeReferences(String repoURL, final IGraphNode instance) {
Map<String, Set<String>> nodereferences = new HashMap<>();
for (IGraphEdge reference : instance.getOutgoing()) {
if (reference.getType().equals(ModelElementNode.EDGE_LABEL_FILE)
|| reference.getType().equals(ModelElementNode.EDGE_LABEL_OFTYPE)
|| reference.getType().equals(ModelElementNode.EDGE_LABEL_OFKIND)
|| reference.getPropertyKeys().contains(GraphModelInserter.DERIVED_FEATURE_EDGEPROP)) {
continue;
}
final Set<String> refvals = new HashSet<>();
if (nodereferences.containsKey(reference.getType())) {
refvals.addAll(nodereferences.get(reference.getType()));
}
final IGraphNode refEndNode = reference.getEndNode();
final String refEndNodeId = refEndNode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString();
if (!singletonIndexIsEmpty && singletonIndex.get("id", refEndNodeId).iterator().hasNext()) {
refvals.add(refEndNodeId);
} else {
final IGraphNode targetFileNode = refEndNode.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE).iterator().next().getEndNode();
final Object targetFileID = targetFileNode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY);
refvals.add(repoURL + GraphModelUpdater.FILEINDEX_REPO_SEPARATOR + targetFileID + "#" + refEndNodeId);
}
nodereferences.put(reference.getType(), refvals);
}
return nodereferences;
}
@SuppressWarnings("unchecked")
protected Map<String, Set<String>> computeModelReferences(final URI tempURI, final String repoURL, final IHawkObject eobject) throws URISyntaxException {
final URI repoURI = new URI(repoURL);
// full reference uri in order to properly compare with hawk proxies
Map<String, Set<String>> modelReferences = new HashMap<>();
for (IHawkReference ref : ((IHawkClass) eobject.getType()).getAllReferences()) {
if (eobject.isSet(ref)) {
final Object refval = eobject.get(ref, false);
final Set<String> vals = new HashSet<>();
if (refval instanceof Iterable<?>) {
for (Object val : ((Iterable<IHawkObject>) refval)) {
vals.add(parseValue(val, repoURI, tempURI));
}
} else {
vals.add(parseValue(refval, repoURI, tempURI));
}
if (!vals.isEmpty()) {
modelReferences.put(ref.getName(), vals);
}
}
}
return modelReferences;
}
protected void compareAttributes(final IGraphNode node, final IHawkObject modelElement) {
// cache model element attributes and references by name
final Map<String, Object> modelAttributes = new HashMap<>();
for (IHawkAttribute a : ((IHawkClass) modelElement.getType()).getAllAttributes()) {
if (modelElement.isSet(a)) {
modelAttributes.put(a.getName(), modelElement.get(a));
}
}
for (String propertykey : node.getPropertyKeys()) {
if (!propertykey.equals(IModelIndexer.SIGNATURE_PROPERTY)
&& !propertykey.equals(IModelIndexer.IDENTIFIER_PROPERTY)
&& !propertykey.startsWith(GraphModelUpdater.PROXY_REFERENCE_PREFIX)
&& !propertykey.equals(GraphModelInserter.LAST_DERIVED_TSTAMP_NODEPROP)) {
Object dbattr = node.getProperty(propertykey);
Object attr = modelAttributes.get(propertykey);
if (!flattenedStringEquals(dbattr, attr)) {
final String dbJavaType = dbattr != null ? dbattr.getClass().toString() : "null attr";
final Object dbValue = dbattr instanceof Object[] ? (Arrays.asList((Object[]) dbattr)) : dbattr;
final String modelJavaType = attr != null ? attr.getClass().toString() : "null attr";
final Object modelValue = attr instanceof Object[] ? (Arrays.asList((Object[]) attr)) : attr;
errors.add(new ValidationError(String.format(
"Attribute %s has mismatched values:\n"
+ " * database:\t%s JAVATYPE: %s IN NODE: %s WITH ID: %s\n"
+ " * model:\t\t%s JAVATYPE: %s IN ELEMENT WITH ID %s",
propertykey,
dbValue, dbJavaType, node.getId(), node.getProperty(IModelIndexer.IDENTIFIER_PROPERTY),
modelValue, modelJavaType, modelElement.getUriFragment()
)));
}
modelAttributes.remove(propertykey);
}
}
if (modelAttributes.size() > 0) {
errors.add(new ValidationError(String.format(
"The following attributes were not found in the graph node %s: %s", node.getId(), modelAttributes.keySet()
)));
}
}
private String parseValue(Object val, URI repo, URI temp) throws URISyntaxException {
String ret;
IHawkObject o = (IHawkObject) val;
// System.err.println("-checking uniqueness of " + o.getUriFragment());
if (!singletonIndexIsEmpty
&& singletonIndex.get("id", o.getUriFragment()).iterator()
.hasNext()) {
// System.err.println("-singleton: " + o.getUriFragment()
// + " (isfragunique: " + o.isFragmentUnique() + ")");
ret = o.getUriFragment();
} else {
final URI objURI = new URI(o.getUri());
ret = objURI.getPath().replace(repo.getPath(), "").replace(temp.getPath(), "").replace("+", "%2B");
if (objURI.getFragment() != null) {
ret += "#" + objURI.getFragment();
}
if (!ret.startsWith("/")) {
ret = "/" + ret;
}
try {
ret = URLDecoder.decode(ret, "UTF-8");
} catch (Exception ex) {
// might not be decodable that way (Modelio can produce something like '#//%Objing%')
}
ret = (repo + GraphModelUpdater.FILEINDEX_REPO_SEPARATOR + ret);
}
return ret;
}
private int latestChangedElements() {
return changed.size();
}
private int latestDeletedElements() {
return deleted;
}
private Set<String> removeHawkProxies(IGraphNode instance,
Set<String> modelrefvaluesclone) {
// String repoURL = "";
// String destinationObjectRelativePathURI = "";
//
// String destinationObjectRelativeFileURI =
// destinationObjectRelativePathURI
// .substring(0, destinationObjectRelativePathURI.indexOf("#"));
// String destinationObjectFullFileURI = repoURL
// + GraphModelUpdater.FILEINDEX_REPO_SEPARATOR
// + destinationObjectRelativeFileURI;
for (String propertykey : instance.getPropertyKeys()) {
if (propertykey.startsWith(GraphModelUpdater.PROXY_REFERENCE_PREFIX)) {
String[] proxies = (String[]) instance.getProperty(propertykey);
for (int i = 0; i < proxies.length; i = i + 4)
if (modelrefvaluesclone.remove(proxies[i]))
removedProxies++;
}
}
return modelrefvaluesclone;
}
private boolean flattenedStringEquals(Object dbattr, Object attr) {
String newdbattr = dbattr == null ? "null" : dbattr.toString();
if (dbattr instanceof int[])
newdbattr = Arrays.toString((int[]) dbattr);
else if (dbattr instanceof long[])
newdbattr = Arrays.toString((long[]) dbattr);
else if (dbattr instanceof String[])
newdbattr = Arrays.toString((String[]) dbattr);
else if (dbattr instanceof boolean[])
newdbattr = Arrays.toString((boolean[]) dbattr);
else if (dbattr instanceof Object[])
newdbattr = Arrays.toString((Object[]) dbattr);
String newattr = attr == null ? "null" : attr.toString();
if (attr instanceof int[])
newattr = Arrays.toString((int[]) attr);
else if (attr instanceof long[])
newattr = Arrays.toString((long[]) attr);
else if (attr instanceof String[])
newattr = Arrays.toString((String[]) attr);
else if (attr instanceof boolean[])
newattr = Arrays.toString((boolean[]) attr);
else if (attr instanceof Object[])
newattr = Arrays.toString((Object[]) attr);
return newdbattr.equals(newattr);
}
@Override
public void changeStart() {
}
@Override
public void changeSuccess() {
}
@Override
public void changeFailure() {
}
@Override
public void metamodelAddition(IHawkPackage pkg, IGraphNode pkgNode) {
}
@Override
public void classAddition(IHawkClass cls, IGraphNode clsNode) {
}
@Override
public void fileAddition(VcsCommitItem s, IGraphNode fileNode) {
}
@Override
public void fileRemoval(VcsCommitItem s, IGraphNode fileNode) {
}
@Override
public void modelElementAddition(VcsCommitItem s, IHawkObject element,
IGraphNode elementNode, boolean isTransient) {
changed.add(elementNode.getId().toString());
}
@Override
public void modelElementRemoval(VcsCommitItem s, IGraphNode elementNode,
boolean isTransient) {
deleted++;
}
@Override
public void modelElementAttributeUpdate(VcsCommitItem s,
IHawkObject eObject, String attrName, Object oldValue,
Object newValue, IGraphNode elementNode, boolean isTransient) {
changed.add(elementNode.getId().toString());
}
@Override
public void modelElementAttributeRemoval(VcsCommitItem s,
IHawkObject eObject, String attrName, IGraphNode elementNode,
boolean isTransient) {
changed.add(elementNode.getId().toString());
}
@Override
public void referenceAddition(VcsCommitItem s, IGraphNode source,
IGraphNode destination, String edgelabel, boolean isTransient) {
changed.add(source.getId().toString());
}
@Override
public void referenceRemoval(VcsCommitItem s, IGraphNode source,
IGraphNode destination, String edgelabel, boolean isTransient) {
changed.add(source.getId().toString());
}
}