/******************************************************************************* * Copyright (c) 2007, 2011 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.issues.common; import gnu.trove.set.hash.THashSet; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import org.simantics.Simantics; import org.simantics.databoard.Bindings; import org.simantics.databoard.util.URIStringUtils; import org.simantics.db.Disposable; import org.simantics.db.Issue; import org.simantics.db.ReadGraph; import org.simantics.db.RequestProcessor; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.VirtualGraph; import org.simantics.db.WriteGraph; import org.simantics.db.common.primitiverequest.Objects; import org.simantics.db.common.procedure.adapter.DisposableSyncListener; import org.simantics.db.common.procedure.adapter.TransientCacheListener; import org.simantics.db.common.procedure.single.SingleSetSyncListener; import org.simantics.db.common.request.ResourceRead3; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.common.utils.ListUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.request.ActiveModels; import org.simantics.db.layer0.request.Model; import org.simantics.db.layer0.request.PossibleModel; import org.simantics.db.layer0.util.RemoverUtil; import org.simantics.db.layer0.variable.Variable; import org.simantics.db.service.VirtualGraphSupport; import org.simantics.issues.Severity; import org.simantics.issues.ontology.IssueResource; import org.simantics.layer0.Layer0; import org.simantics.operation.Layer0X; import org.simantics.scl.runtime.function.FunctionImpl2; import org.simantics.utils.datastructures.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Tuukka Lehtonen */ public class IssueUtils { private static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.class); public static Resource toSeverityResource(IssueResource ISSUE, Severity severity) { switch (severity) { case ERROR: return ISSUE.Severity_Error; case FATAL: return ISSUE.Severity_Fatal; case INFO: return ISSUE.Severity_Info; case WARNING: return ISSUE.Severity_Warning; case NOTE: return ISSUE.Severity_Note; default: return null; } } public static Severity toSeverity(IssueResource ISSUE, Resource severity) { if (severity == null) return null; if (severity.equals(ISSUE.Severity_Fatal)) return Severity.FATAL; if (severity.equals(ISSUE.Severity_Error)) return Severity.ERROR; if (severity.equals(ISSUE.Severity_Info)) return Severity.INFO; if (severity.equals(ISSUE.Severity_Warning)) return Severity.WARNING; if (severity.equals(ISSUE.Severity_Note)) return Severity.NOTE; return null; } private static class IssueSourceDirtyListener extends FunctionImpl2, Boolean> { private final IssueSource is; public IssueSourceDirtyListener(IssueSource is) { this.is = is; } @Override public Boolean apply(ReadGraph graph, final List resources) { VirtualGraphSupport support = graph.getService(VirtualGraphSupport.class); VirtualGraph vg = support.getWorkspacePersistent(IssueConstants.ISSUE_VG); if (graph instanceof WriteGraph) { try { if(is.needUpdate(graph, resources)) { graph.sync(new WriteRequest(vg) { @Override public void perform(WriteGraph graph) throws DatabaseException { is.update(graph, resources); } }); } } catch (DatabaseException e) { LOGGER.error("Updating issue source failed.", e); } } else { Session session = Simantics.getSession(); session.asyncRequest(new WriteRequest(vg) { @Override public void perform(WriteGraph graph) throws DatabaseException { is.update(graph, resources); } }); } return true; } } private static class IssueSourceManagedIssuesListener extends SingleSetSyncListener { private final HashMap listeners = new HashMap<>(); private final AtomicBoolean disposed; private final Resource source; private final Resource model; public IssueSourceManagedIssuesListener(AtomicBoolean disposed, Resource source, Resource model) { this.disposed = disposed; this.source = source; this.model = model; } class IssueValidityListener extends DisposableSyncListener { final private Resource issue; public IssueValidityListener(Resource issue) { this.issue = issue; } @Override public void execute(ReadGraph graph, Boolean valid) throws DatabaseException { if(!valid) { VirtualGraphSupport support = graph.getService(VirtualGraphSupport.class); VirtualGraph vg = support.getWorkspacePersistent(IssueConstants.ISSUE_VG); graph.asyncRequest(new WriteRequest(vg) { @Override public void perform(WriteGraph graph) throws DatabaseException { Issue desc = graph.sync(new StandardIssueDescription(issue)); if(desc == null) return; Resource context = (Resource)desc.getMainContext(); new DependencyIssueSynchronizer2(context, source).perform(graph); } }); } } @Override public void exception(ReadGraph graph, Throwable throwable) throws DatabaseException { LOGGER.error("IssueValidityListener received an exception.", throwable); } } @Override public void add(ReadGraph graph, final Resource issue) throws DatabaseException { IssueValidityListener listener = new IssueValidityListener(issue); graph.asyncRequest(new ResourceRead3(issue, model, source) { @Override public Boolean perform(ReadGraph graph) throws DatabaseException { Issue desc = graph.sync(new StandardIssueDescription(resource)); if(desc == null) return false; Resource context = (Resource)desc.getMainContext(); return graph.syncRequest(new DependencyIssueValidator2(context, resource2, resource3), TransientCacheListener.instance()); } }, listener); listeners.put(issue, listener); } @Override public void remove(ReadGraph graph, final Resource issue) throws DatabaseException { IssueValidityListener listener = listeners.remove(issue); if(listener != null) listener.dispose(); } @Override public void exception(ReadGraph graph, Throwable t) { LOGGER.error("IssueSourceManagedIssuesListener received an exception.", t); } @Override public boolean isDisposed() { boolean disp = disposed.get(); if (disp) { // Ensure validity listeners are cleared eventually. if (!listeners.isEmpty()) { for (IssueValidityListener listener : listeners.values()) { listener.dispose(); } listeners.clear(); } } return disp; } } private static class ActiveIssueSourceListener extends SingleSetSyncListener { private final AtomicBoolean disposed; private Map> sources = new HashMap<>(); public ActiveIssueSourceListener(AtomicBoolean disposed) { this.disposed = disposed; } @Override public void add(ReadGraph graph, final Resource source) throws DatabaseException { IssueResource ISSUE = IssueResource.getInstance(graph); boolean isListeningTracker = graph.isInstanceOf(source, ISSUE.Sources_ListeningDependencyTracker); IssueSource is = graph.adapt(source, IssueSource.class); final Resource model = isListeningTracker ? graph.syncRequest(new Model(source)) : null; IssueSourceDirtyListener listener = new IssueSourceDirtyListener(is); is.addDirtyListener(listener); sources.put(source, Pair.make(is, listener)); if (isListeningTracker) { graph.asyncRequest( new Objects(source, ISSUE.IssueSource_Manages), new IssueSourceManagedIssuesListener(disposed, source, model)); } } @Override public void remove(ReadGraph graph, final Resource source) throws DatabaseException { Pair is = sources.remove(source); if (is != null) is.first.removeDirtyListener(is.second); } @Override public void exception(ReadGraph graph, Throwable t) { LOGGER.error("ActiveIssueSourceListener received an exception.", t); } @Override public boolean isDisposed() { return disposed.get(); } } public static Disposable listenActiveProjectIssueSources(RequestProcessor processor, Resource project) throws DatabaseException { final AtomicBoolean disposed = new AtomicBoolean(false); processor.syncRequest( new ActiveProjectIssueSources(project), new ActiveIssueSourceListener(disposed)); return new Disposable() { @Override public void dispose() { disposed.set(true); } }; } public static List getContextsForProperty(ReadGraph graph, Variable property) throws DatabaseException { IssueResource ISSUE = IssueResource.getInstance(graph); Variable issueVariable = property.getParent(graph); Resource issueResource = issueVariable.getRepresents(graph); Resource list = graph.getPossibleObject(issueResource, ISSUE.Issue_HasContexts); if(list != null) return ListUtils.toList(graph, list); else return Collections.emptyList(); } public static void writeAdditionalContext(WriteGraph graph, Resource issue, List contexts) throws DatabaseException { if(contexts.isEmpty()) return; IssueResource IR = IssueResource.getInstance(graph); // The main context graph.claim(issue, IR.Issue_HasContext, contexts.get(0)); // A possible parent Layer0 L0 = Layer0.getInstance(graph); Resource parent = graph.getPossibleObject(contexts.get(0), L0.PartOf); if(parent != null) { graph.claim(issue, IR.Issue_HasContext, parent); } } public static void newUserIssue(WriteGraph graph, String label, Resource severity, List contexts) throws DatabaseException { Resource model = graph.sync(new PossibleModel(contexts.get(0))); if(model == null) throw new DatabaseException("No model for main context"); newUserIssueForModel(graph, model, label, severity, contexts); } public static Resource newUserIssueForModel(WriteGraph graph) throws DatabaseException { Resource project = Simantics.getProjectResource(); Collection activeModels = graph.syncRequest(new ActiveModels(project)); if (activeModels.size() != 1) return null; IssueResource ISSUE = IssueResource.getInstance(graph); Resource issue = null; for (Resource model : activeModels) { issue = newUserIssueForModel(graph, model, "New User Issue", ISSUE.Severity_Note, Collections.emptyList()); } return issue; } public static Resource newUserIssueForModel(WriteGraph graph, Resource model, String label, Resource severity, List contexts) throws DatabaseException { IssueResource ISSUE = IssueResource.getInstance(graph); return newUserIssueForModel(graph, model, ISSUE.Issue, label, severity, contexts); } public static Resource newUserIssueForModel(WriteGraph graph, Resource model, Resource type, String label, Resource severity, List contexts) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); IssueResource ISSUE = IssueResource.getInstance(graph); Resource issue = graph.newResource(); graph.claim(issue, ISSUE.UserIssue, ISSUE.UserIssue, issue); graph.claim(issue, L0.InstanceOf, null, type); graph.claim(issue, ISSUE.Issue_HasSeverity, null, severity); graph.claim(issue, ISSUE.Issue_HasContexts, ListUtils.create(graph, L0.List, contexts)); writeAdditionalContext(graph, issue, contexts); graph.claimLiteral(issue, L0.HasName, UUID.randomUUID().toString(), Bindings.STRING); graph.claimLiteral(issue, L0.HasLabel, label, Bindings.STRING); DateFormat format = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); Object time = format.format(new Date()); graph.claimLiteral(issue, ISSUE.Issue_creationTime, time, Bindings.STRING); graph.claim(model, L0.ConsistsOf, L0.PartOf, issue); return issue; } /** * Creates a new issue based on SimpleIssue structure. */ public static void newSimpleIssueForModel(WriteGraph graph, Resource model, Resource issueType, List contexts, SimpleIssue simpleIssue) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); IssueResource ISSUE = IssueResource.getInstance(graph); Resource issue = graph.newResource(); graph.claim(issue, L0.InstanceOf, null, issueType); graph.claim(issue, ISSUE.Issue_HasSeverity, null, toSeverityResource(ISSUE, simpleIssue.severity)); graph.claim(issue, ISSUE.Issue_HasContexts, ListUtils.create(graph, L0.List, contexts)); writeAdditionalContext(graph, issue, contexts); graph.claimLiteral(issue, L0.HasName, UUID.randomUUID().toString(), Bindings.STRING); graph.claimLiteral(issue, L0.HasLabel, simpleIssue.label, Bindings.STRING); DateFormat format = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); Object time = format.format(new Date()); graph.claimLiteral(issue, ISSUE.Issue_creationTime, time, Bindings.STRING); graph.claim(model, L0.ConsistsOf, L0.PartOf, issue); } /** * Returns the set of all issues with given type. */ public static Set getSimpleIssues(ReadGraph graph, List contexts, Resource issueType) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); IssueResource ISSUE = IssueResource.getInstance(graph); THashSet currentIssues = null; for(Resource issue : graph.getObjects(contexts.get(0), ISSUE.Issue_HasContext_Inverse)) { if(!graph.isInstanceOf(issue, issueType)) continue; List curContexts = ListUtils.toList(graph, graph.getSingleObject(issue, ISSUE.Issue_HasContexts)); if(!contexts.equals(curContexts)) continue; if(currentIssues == null) currentIssues = new THashSet(); currentIssues.add(new SimpleIssue( (String)graph.getRelatedValue(issue, L0.HasLabel), toSeverity(ISSUE, graph.getSingleObject(issue, ISSUE.Issue_HasSeverity)), issue)); } if(currentIssues == null) return Collections.emptySet(); else return currentIssues; } /** * Creates and removes issues so that after the operation, * the context has exactly the given issues with the given type. */ public static void setSimpleIssues(WriteGraph graph, Resource model, List contexts, Resource issueType, SimpleIssue ... issues) throws DatabaseException { Set currentIssues = getSimpleIssues(graph, contexts, issueType); for(SimpleIssue newIssue : issues) { if(currentIssues.contains(newIssue)) currentIssues.remove(newIssue); else newSimpleIssueForModel(graph, model, issueType, contexts, newIssue); } for(SimpleIssue oldIssue : currentIssues) RemoverUtil.remove(graph, oldIssue.issueResource); } /** * Creates and removes issues so that after the operation, * the context has exactly the given issues with the given type. */ public static void setSimpleIssues(WriteGraph graph, List contexts, Resource issueType, SimpleIssue ... issues) throws DatabaseException { Resource model = graph.sync(new PossibleModel(contexts.get(0))); if(model == null) throw new DatabaseException("No model for main context"); setSimpleIssues(graph, model, contexts, issueType, issues); } /** * Creates and removes issues so that after the operation, * the context has exactly the given issues with the given type. * Because this method is called in read transaction, it * makes a write transaction if necessary and continues the * operation there. */ public static void setSimpleIssuesAsync(ReadGraph graph, final List contexts, final Resource issueType, final SimpleIssue ... issues) throws DatabaseException { Resource model = graph.sync(new PossibleModel(contexts.get(0))); if(model == null) throw new DatabaseException("No model for main context"); setSimpleIssuesAsync(graph, model, contexts, issueType, issues); } /** * Creates and removes issues so that after the operation, * the context has exactly the given issues with the given type. * Because this method is called in read transaction, it * makes a write transaction if necessary and continues the * operation there. */ public static void setSimpleIssuesAsync(ReadGraph graph, final Resource model, final List contexts, final Resource issueType, final SimpleIssue ... issues) throws DatabaseException { Set oldIssues = getSimpleIssues(graph, contexts, issueType); boolean needsUpdating = false; if(issues.length != oldIssues.size()) needsUpdating = true; else for(SimpleIssue newIssue : issues) { if(!oldIssues.contains(newIssue)) { needsUpdating = true; break; } } if(needsUpdating) { VirtualGraphSupport support = graph.getService(VirtualGraphSupport.class); VirtualGraph vg = support.getWorkspacePersistent(IssueConstants.ISSUE_VG); Simantics.getSession().asyncRequest(new WriteRequest(vg) { @Override public void perform(WriteGraph graph) throws DatabaseException { setSimpleIssues(graph, model, contexts, issueType, issues); } }); } } public static Resource addIssueSource(WriteGraph g, Resource model, Resource sourceType, String name) throws DatabaseException { Layer0 L0 = Layer0.getInstance(g); Layer0X L0X = Layer0X.getInstance(g); Resource source = g.newResource(); g.claim(source, L0.InstanceOf, null, sourceType); g.addLiteral(source, L0.HasName, L0.NameOf, L0.String, name, Bindings.STRING); g.claim(source, L0X.IsActivatedBy, L0X.Activates, model); g.claim(source, L0.PartOf, L0.ConsistsOf, model); return source; } public static String pathString(String uri, int startIndex) { StringBuilder sb = new StringBuilder(uri.length() - startIndex + 1); sb.append(URIStringUtils.NAMESPACE_PATH_SEPARATOR); while (true) { int nextSlash = uri.indexOf(URIStringUtils.NAMESPACE_PATH_SEPARATOR, startIndex); if (nextSlash == -1) { sb.append(URIStringUtils.unescape(uri.substring(startIndex, uri.length()))); break; } sb.append(URIStringUtils.unescape(uri.substring(startIndex, nextSlash))).append(URIStringUtils.NAMESPACE_PATH_SEPARATOR); startIndex = nextSlash + 1; } return sb.toString(); } }