package org.simantics.modeling.scl.issue; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.simantics.Simantics; import org.simantics.db.Disposable; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.common.procedure.adapter.DisposableListener; import org.simantics.db.common.procedure.adapter.DisposableSyncListener; import org.simantics.db.common.procedure.adapter.SyncListenerAdapter; import org.simantics.db.common.request.TernaryRead; import org.simantics.db.common.request.UnaryRead; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.util.Layer0Utils; import org.simantics.db.layer0.variable.Variable; import org.simantics.db.layer0.variable.Variables; import org.simantics.layer0.Layer0; import org.simantics.modeling.ModelingUtils; import org.simantics.scl.compiler.errors.CompilationError; import org.simantics.scl.compiler.errors.Locations; import org.simantics.scl.osgi.issues.SCLIssueProviderFactory; import org.simantics.scl.osgi.issues.SCLIssueProviderFactory.SCLIssueProvider; import org.simantics.scl.osgi.issues.SCLIssuesTableEntry; import org.simantics.scl.runtime.SCLContext; import org.simantics.scl.runtime.function.Function1; import org.simantics.structural.stubs.StructuralResource2; import org.simantics.ui.workbench.action.DefaultActions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SCLExpressionIssueProvider implements SCLIssueProvider { public static class SCLExpressionIssueProviderFactory implements SCLIssueProviderFactory { @Override public SCLIssueProvider getSCLIssueProvider() { return new SCLExpressionIssueProvider(); } } private static final Logger LOGGER = LoggerFactory.getLogger(SCLExpressionIssueProvider.class); private boolean disposed = false; private ComponentSyncListenerAdapter listener; SCLExpressionIssueProvider() { } @Override public void listenIssues(Runnable callback) { listener = new ComponentSyncListenerAdapter(callback); Simantics.getSession().asyncRequest(new ComponentRequest(), listener); } @Override public List getIssues() { return listener.getIssues(); } @Override public void dispose() { listener.dispose(); disposed = true; } private static void openResource(Shell shell, Resource resource) { DefaultActions.performDefaultAction(shell, new StructuredSelection(resource)); } private static class ComponentRequest extends UniqueRead> { @Override public Set perform(ReadGraph graph) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); Set indexRoots = new TreeSet(); for(Resource ontology : Layer0Utils.listOntologies(graph)) { if (graph.isInstanceOf(ontology, L0.SharedOntology)) { indexRoots.add(ontology); } } for(Resource child : graph.getObjects(Simantics.getProjectResource(), L0.ConsistsOf)) { if (graph.isInstanceOf(child, L0.IndexRoot)) { indexRoots.add(child); } } StructuralResource2 STR = StructuralResource2.getInstance(graph); Set allComponents = new HashSet<>(); for (Resource ontology : indexRoots) { List components = ModelingUtils.searchByTypeShallow(graph, ontology, STR.Component); allComponents.addAll(components); } return allComponents; } } private static class SCLValueRequest extends UnaryRead> { public SCLValueRequest(Resource parameter) { super(parameter); } @Override public Set perform(ReadGraph graph) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); Set results = new HashSet<>(); for (Resource predicate : graph.getPredicates(parameter)) { if (graph.isSubrelationOf(predicate, L0.HasProperty)) { for (Resource object : graph.getObjects(parameter, predicate)) { if (graph.isInstanceOf(object, L0.SCLValue)) { results.add(new ResourceHolder(parameter, predicate, object)); } } } } return results; } } private static class SCLExpressionValidationRequest extends TernaryRead { public SCLExpressionValidationRequest(Resource component, Resource predicate, Resource object) { super(component, predicate, object); } @Override public SCLIssuesTableEntry perform(ReadGraph graph) throws DatabaseException { Resource type = graph.getPossibleType(parameter3, Layer0.getInstance(graph).SCLValue); if (type == null) { return null; } if (!graph.hasStatement(parameter)) return null; Variable componentVariable = Variables.getVariable(graph, parameter); Variable propertyVariable = componentVariable.getProperty(graph, parameter2); Variable typeVariable = Variables.getVariable(graph, type); Function1 func = typeVariable.getPossiblePropertyValue(graph, "validator"); if (func == null) { // No validator available if (LOGGER.isTraceEnabled()) LOGGER.trace("No validator available for " + typeVariable.getURI(graph)); return null; } SCLContext sclContext = SCLContext.getCurrent(); Object oldGraph = sclContext.get("graph"); try { sclContext.put("graph", graph); String validatorValue = func.apply(propertyVariable); if (validatorValue != null && !validatorValue.isEmpty()) { return new SCLIssuesTableEntry(propertyVariable.getURI(graph), new CompilationError(Locations.NO_LOCATION, validatorValue.replace("\n", " "))) { @Override public void openLocation() { openResource(Display.getCurrent().getActiveShell(), parameter); } }; } } catch (Throwable t) { LOGGER.error("Failed to invoke type validator function " + func, t); } finally { sclContext.put("graph", oldGraph); } return null; } } private static class ComponentSyncListenerAdapter extends SyncListenerAdapter> implements Disposable { private ConcurrentHashMap currentlyListening = new ConcurrentHashMap<>(); private boolean disposed; private Runnable callback; public ComponentSyncListenerAdapter(Runnable callback) { this.callback = callback; } @Override public void execute(ReadGraph graph, Set newComponents) { if (currentlyListening.isEmpty() && newComponents.isEmpty()) { // we can stop here as nothing will change return; } Set removedComponents = new HashSet<>(currentlyListening.keySet()); removedComponents.removeAll(newComponents); Set addedComponents = new HashSet<>(newComponents); addedComponents.removeAll(currentlyListening.keySet()); for (Resource removedComponent : removedComponents) { // stop listening DisposableSyncListener listener = currentlyListening.remove(removedComponent); listener.dispose(); } for (Resource addedComponent : addedComponents) { SCLValueDisposableSyncListener listener = new SCLValueDisposableSyncListener(callback); currentlyListening.put(addedComponent, listener); graph.asyncRequest(new SCLValueRequest(addedComponent), listener); } } public List getIssues() { List issues = new ArrayList<>(); for (SCLValueDisposableSyncListener listener : currentlyListening.values()) { List listenerIssues = listener.getIssues(); if (listenerIssues != null && !listenerIssues.isEmpty()) issues.addAll(listenerIssues); } return issues; } @Override public void dispose() { currentlyListening.values().forEach(l -> l.dispose()); currentlyListening.clear(); this.disposed = true; } @Override public boolean isDisposed() { return disposed; } } private static class SCLValueDisposableSyncListener extends DisposableSyncListener> { private Runnable callback; private ConcurrentHashMap currentlyListeningSCLValues = new ConcurrentHashMap<>(); public SCLValueDisposableSyncListener(Runnable callback) { this.callback = callback; } @Override public void execute(ReadGraph graph, Set newComponents) throws DatabaseException { if (currentlyListeningSCLValues.isEmpty() && newComponents.isEmpty()) { // we can stop here as nothing will change return; } Set removedComponents = new HashSet<>(currentlyListeningSCLValues.keySet()); removedComponents.removeAll(newComponents); Set addedComponents = new HashSet<>(newComponents); addedComponents.removeAll(currentlyListeningSCLValues.keySet()); for (ResourceHolder removedComponent : removedComponents) { // stop listening DisposableListener listener = currentlyListeningSCLValues.remove(removedComponent); listener.dispose(); } for (ResourceHolder sclValue : addedComponents) { SCLIssuesTableEntryDisposableListener listener = new SCLIssuesTableEntryDisposableListener(callback); currentlyListeningSCLValues.put(sclValue, listener); graph.syncRequest(new SCLExpressionValidationRequest(sclValue.component, sclValue.predicate, sclValue.object), listener); } if (callback != null) { callback.run(); } } public List getIssues() { if (currentlyListeningSCLValues.isEmpty()) return null; List issues = new ArrayList<>(); for (SCLIssuesTableEntryDisposableListener listener : currentlyListeningSCLValues.values()) { if (listener.getResult() != null) issues.add(listener.getResult()); } return issues; } @Override public void exception(ReadGraph graph, Throwable throwable) throws DatabaseException { LOGGER.error("Could not listen", throwable); } @Override public void dispose() { currentlyListeningSCLValues.values().forEach(l -> l.dispose()); currentlyListeningSCLValues.clear(); super.dispose(); } } private static class SCLIssuesTableEntryDisposableListener extends DisposableListener { private SCLIssuesTableEntry result; private Runnable callback; public SCLIssuesTableEntryDisposableListener(Runnable callback) { this.callback = callback; } @Override public void execute(SCLIssuesTableEntry result) { if (!Objects.equals(this.result, result)) { this.result = result; if (callback != null) { callback.run(); } } } @Override public void exception(Throwable t) { LOGGER.error("", t); } public SCLIssuesTableEntry getResult() { return result; } } private static class ResourceHolder { Resource component; Resource predicate; Resource object; public ResourceHolder(Resource component, Resource predicate, Resource object) { this.component = Objects.requireNonNull(component); this.predicate = Objects.requireNonNull(predicate); this.object = Objects.requireNonNull(object); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + component.hashCode(); result = prime * result + object.hashCode(); result = prime * result + predicate.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; ResourceHolder other = (ResourceHolder) obj; if (!component.equals(other.component)) return false; if (!object.equals(other.object)) return false; if (!predicate.equals(other.predicate)) return false; return true; } } }