/******************************************************************************* * Copyright (c) 2007, 2010 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.db.layer0.genericrelation; 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.UUID; import org.simantics.databoard.Bindings; import org.simantics.databoard.util.ObjectUtils; import org.simantics.datatypes.literal.GUID; import org.simantics.db.ChangeSet; import org.simantics.db.ChangeSet.StatementChange; import org.simantics.db.MetadataI; import org.simantics.db.ReadGraph; import org.simantics.db.RequestProcessor; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.Statement; import org.simantics.db.WriteGraph; import org.simantics.db.common.Indexing; import org.simantics.db.common.changeset.GenericChangeListener; import org.simantics.db.common.request.IndexRoot; import org.simantics.db.common.request.ReadRequest; import org.simantics.db.common.request.SuperTypeString; import org.simantics.db.common.request.TypeString; import org.simantics.db.common.request.UnaryRead; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.event.ChangeListener; import org.simantics.db.exception.DatabaseException; import org.simantics.db.exception.NoSingleResultException; import org.simantics.db.layer0.adapter.GenericRelation; import org.simantics.db.layer0.adapter.GenericRelationIndex; import org.simantics.db.layer0.genericrelation.DependencyChanges.Change; import org.simantics.db.layer0.genericrelation.DependencyChanges.ComponentAddition; import org.simantics.db.layer0.genericrelation.DependencyChanges.ComponentModification; import org.simantics.db.layer0.genericrelation.DependencyChanges.ComponentRemoval; import org.simantics.db.layer0.genericrelation.DependencyChanges.LinkChange; import org.simantics.db.procedure.SyncContextMultiProcedure; import org.simantics.db.procedure.SyncContextProcedure; import org.simantics.db.service.CollectionSupport; import org.simantics.db.service.DirectQuerySupport; import org.simantics.db.service.GraphChangeListenerSupport; import org.simantics.db.service.ManagementSupport; import org.simantics.db.service.SerialisationSupport; import org.simantics.layer0.Layer0; import org.simantics.operation.Layer0X; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.logging.TimeLogger; import org.slf4j.LoggerFactory; public class DependenciesRelation extends UnsupportedRelation implements GenericRelationIndex { private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(DependenciesRelation.class); private static final boolean DEBUG = false; static final boolean DEBUG_LISTENERS = false; private static final boolean PROFILE = false; @SuppressWarnings("unchecked") private final static Pair[] fields = new Pair[] { Pair.make(Dependencies.FIELD_MODEL, "Long"), Pair.make(Dependencies.FIELD_PARENT, "Long"), Pair.make(Dependencies.FIELD_RESOURCE, "Long"), Pair.make(Dependencies.FIELD_NAME, "String"), Pair.make(Dependencies.FIELD_TYPES, "Text"), Pair.make(Dependencies.FIELD_GUID, "Text"), Pair.make(Dependencies.FIELD_NAME_SEARCH, "Text"), Pair.make(Dependencies.FIELD_TYPES_SEARCH, "Text"), Pair.make(Dependencies.FIELD_TYPE_RESOURCE, "Text") }; final Resource resource; public DependenciesRelation(ReadGraph graph, Resource resource) { this.resource = resource; synchronized(this) { Session session = graph.getSession(); DependenciesListenerStore store = session.peekService(DependenciesListenerStore.class); if(store == null) session.registerService(DependenciesListenerStore.class, new DependenciesListenerStore()); } } class Process { final ArrayList result = new ArrayList(); final SyncContextMultiProcedure structure; final SyncContextProcedure names; final SyncContextProcedure type; Process(ReadGraph graph, final Resource resource) throws DatabaseException { final Layer0 L0 = Layer0.getInstance(graph); final DirectQuerySupport dqs = graph.getService(DirectQuerySupport.class); final CollectionSupport cs = graph.getService(CollectionSupport.class); names = dqs.compilePossibleRelatedValue(graph, L0.HasName, new SyncContextProcedure() { @Override public void execute(ReadGraph graph, Entry entry, String name) { entry.name = name; } @Override public void exception(ReadGraph graph, Throwable throwable) { LOGGER.error("Could not compile possible related value for resource {}", resource, throwable); } }); type = new SyncContextProcedure() { @Override public void execute(ReadGraph graph, Entry entry, Resource type) { entry.principalType = type; } @Override public void exception(ReadGraph graph, Throwable throwable) { LOGGER.error("Could not find type for resource {}", resource, throwable); } }; structure = dqs.compileForEachObject(graph, L0.ConsistsOf, new SyncContextMultiProcedure() { @Override public void execute(ReadGraph graph, Resource parent, Resource child) { // WORKAROUND: don't browse virtual child resources if(!child.isPersistent()) return; Entry entry = new Entry(parent, child, "", "", ""); result.add(entry); dqs.forEachObjectCompiled(graph, child, child, structure); dqs.forPossibleRelatedValueCompiled(graph, child, entry, names); dqs.forPossibleDirectType(graph, child, entry, type); } @Override public void finished(ReadGraph graph, Resource parent) { } @Override public void exception(ReadGraph graph, Throwable throwable) { if (throwable instanceof NoSingleResultException) { // Ignore if (LOGGER.isDebugEnabled()) LOGGER.debug("Could not compile for resource {}", resource, throwable); } else { LOGGER.error("Could not compile for resource {}", resource, throwable); } } }); result.add(new Entry(graph, resource)); graph.syncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { dqs.forEachObjectCompiled(graph, resource, resource, structure); } }); Map> typeStrings = cs.createMap(String.class); for(Entry e : result) { if(e.principalType != null) { Pair typeString = typeStrings.get(e.principalType); if(typeString == null) { String superTypeString = graph.syncRequest(new SuperTypeString(e.principalType)); if (superTypeString.isEmpty()) { LOGGER.error("No name for type", new DatabaseException("No name for type " + NameUtils.getURIOrSafeNameInternal(graph, e.resource) + " (" + e.resource + ")")); } String superTypeIds = IndexQueries.toResourceIdString(e.principalType, graph.getSupertypes(e.principalType)); typeString = Pair.make(superTypeString, superTypeIds); typeStrings.put(e.principalType, typeString); } e.types = typeString.first; e.typeId = typeString.second; } else { Set typeSet = graph.getTypes(e.resource); e.types = graph.syncRequest(new TypeString(L0, typeSet)); e.typeId = IndexQueries.toResourceIdString(typeSet); } e.id = IndexQueries.idFromGUID( graph.getPossibleRelatedValue(e.resource, L0.identifier, GUID.BINDING) ); } //SessionGarbageCollection.gc(null, graph.getSession(), false, null); } } public ArrayList find(ReadGraph graph, final Resource model) throws DatabaseException { return new Process(graph, model).result; } @Override public GenericRelation select(String bindingPattern, Object[] constants) { checkSelectionArguments(bindingPattern, constants, new String[] { Dependencies.getBindingPattern() }); final long subjectId = (Long)constants[0]; return new UnsupportedRelation() { @Override public boolean isRealizable() { return true; } @Override final public List realize(ReadGraph graph) throws DatabaseException { long time = System.nanoTime(); SerialisationSupport ss = graph.getService(SerialisationSupport.class); Resource subject = ss.getResource(subjectId); Collection entries = find(graph, subject); long time2 = System.nanoTime(); if (PROFILE) LOGGER.info("Found " + entries.size() + " dependencies in " + 1e-6 * (time2 - time) + "ms for " + graph.getPossibleURI(subject) + "."); ArrayList result = new ArrayList<>(); for (Entry entry : entries) { if(entry.name == null) continue; result.add(new Object[] { ss.getRandomAccessId(entry.parent), ss.getRandomAccessId(entry.resource), entry.name, entry.types, entry.id, entry.name, entry.types, entry.typeId }); } return result; } }; } @Override public Pair[] getFields() { return fields; } @Override public List> query(RequestProcessor session, String search, String bindingPattern, Object[] constants, int maxResultCount) { if(!Dependencies.getBindingPattern().equals(bindingPattern)) throw new IllegalArgumentException("DependenciesRelation supports indexing only with 'bfffffff'"); IndexedRelations indexer = session.getService(IndexedRelations.class); return indexer.query(null, search, session, resource, (Resource)constants[0], maxResultCount); } @Override public List queryResources(RequestProcessor session, String search, String bindingPattern, Object[] constants, int maxResultCount) { if(!Dependencies.getBindingPattern().equals(bindingPattern)) throw new IllegalArgumentException("DependenciesRelation supports indexing only with 'bfffffff'"); IndexedRelations indexer = session.getService(IndexedRelations.class); return indexer.queryResources(null, search, session, resource, (Resource)constants[0], maxResultCount); } @Override public List> list(RequestProcessor session, String bindingPattern, Object[] constants, int maxResultCount) { if(!Dependencies.getBindingPattern().equals(bindingPattern)) throw new IllegalArgumentException("DependenciesRelation supports indexing only with 'bfffffff'"); IndexedRelations indexer = session.getService(IndexedRelations.class); return indexer.query(null, null, session, resource, (Resource)constants[0], maxResultCount); } public static class DependencyChangesRequest extends UnaryRead { @SuppressWarnings("unused") final private static boolean LOG = false; public DependencyChangesRequest(ChangeSet parameter) { super(parameter); } @Override public DependencyChanges perform(ReadGraph graph) throws DatabaseException { DependencyChangesWriter w = new DependencyChangesWriter(graph); Layer0 l0 = w.l0; Resource changeInformation = graph.getPossibleResource("http://www.simantics.org/Modeling-1.2/changeInformation/Inverse"); for (Resource value : parameter.changedValues()) { if(!value.isPersistent()) continue; Statement modifiedComponent = graph.getPossibleStatement(value, l0.PropertyOf); if (modifiedComponent == null || modifiedComponent.getPredicate().equals(changeInformation)) continue; if (DEBUG) { LOGGER.info("+comp modi " + NameUtils.getSafeName(graph, modifiedComponent.getObject(), true)); LOGGER.info(" +value " + NameUtils.getSafeName(graph, value, true)); } w.addComponentModification(modifiedComponent.getObject()); } for (Resource value : parameter.changedResources()) { // No more info => need to check further if(!graph.isImmutable(value)) w.addComponentModification(value); } for (StatementChange change : parameter.changedStatements()) { if (DEBUG) LOGGER.info("-stm " + NameUtils.getSafeName(graph, change.getSubject(), true) + " " + NameUtils.getSafeName(graph, change.getPredicate(), true) + " " + NameUtils.getSafeName(graph, change.getObject(), true)); Resource subject = change.getSubject(); Resource predicate = change.getPredicate(); Resource object = change.getObject(); if(!object.isPersistent()) continue; if (predicate.equals(l0.ConsistsOf)) { if (change.isClaim()) w.addComponentAddition(subject, object); else w.addComponentRemoval(subject, object); } else if (predicate.equals(l0.IsLinkedTo)) { w.addLinkChange(subject); } else /*if (graph.isSubrelationOf(predicate, l0.DependsOn))*/ { if (DEBUG) LOGGER.info("-modi " + NameUtils.getSafeName(graph, subject, true)); w.addComponentModification(subject); } } return w.getResult(); } }; private static int trackers = 0; private static ChangeListener listener; public static void assertFinishedTracking() { if(trackers != 0) throw new IllegalStateException("Trackers should be 0 (was " + trackers + ")"); } @Override public synchronized void untrack(RequestProcessor processor, final Resource model) { trackers--; if(trackers < 0) throw new IllegalStateException("Dependency tracking reference count is broken"); if(trackers == 0) { if(listener == null) throw new IllegalStateException("Dependency tracking was not active"); GraphChangeListenerSupport changeSupport = processor.getService(GraphChangeListenerSupport.class); changeSupport.removeMetadataListener(listener); listener = null; } } @Override public synchronized void trackAndIndex(RequestProcessor processor, Resource model__) { if(trackers == 0) { if(listener != null) throw new IllegalStateException("Dependency tracking was active"); listener = new GenericChangeListener() { @Override public boolean preEventRequest() { return !Indexing.isDependenciesIndexingDisabled(); } @Override public void onEvent(ReadGraph graph, MetadataI metadata, DependencyChanges event) throws DatabaseException { TimeLogger.log(DependenciesRelation.class, "trackAndIndex.onEvent: starting index update processing"); if(DEBUG) LOGGER.info("Adding metadata " + event + " in revision " + graph.getService(ManagementSupport.class).getHeadRevisionId()); WriteGraph w = (WriteGraph)graph; if(!event.isEmpty()) w.addMetadata(event); final Session session = graph.getSession(); final IndexedRelations indexer = session.getService(IndexedRelations.class); Layer0 L0 = Layer0.getInstance(graph); SerialisationSupport ss = graph.getService(SerialisationSupport.class); for(Map.Entry modelEntry : event.get().entrySet()) { final Resource model = modelEntry.getKey(); final Change[] changes = modelEntry.getValue(); boolean linkChange = false; Collection _additions = Collections.emptyList(); Collection _removals = Collections.emptyList(); Collection _replacementKeys = Collections.emptyList(); Collection _replacementObjects = Collections.emptyList(); Collection> _typeChanges = Collections.emptyList(); if(DEBUG) LOGGER.info("MODEL: " + NameUtils.getSafeLabel(graph, model)); if (changes != null) { if (DEBUG) { LOGGER.info(" CHANGE COUNT: " + changes.length); for (Change c : changes) LOGGER.info(" CHANGE: " + c.toString(graph)); } _additions = new ArrayList<>(); _removals = new ArrayList<>(); _replacementKeys = new ArrayList<>(); _replacementObjects = new ArrayList<>(); _typeChanges = new HashSet<>(); for (Change _entry : changes) { if (_entry instanceof ComponentAddition) { ComponentAddition entry = (ComponentAddition)_entry; final String name = graph.getPossibleRelatedValue(entry.component, L0.HasName, Bindings.STRING); Set typeSet = graph.getTypes(entry.component); if (name != null && typeSet != null) { if (!entry.isValid(graph)) continue; Resource parent = graph.getPossibleObject(entry.component, L0.PartOf); if (parent != null) { final GUID id = graph.getPossibleRelatedValue(entry.component, L0.identifier, GUID.BINDING); final String types = graph.syncRequest(new TypeString(L0, typeSet)); final String typeIds = IndexQueries.toResourceIdString(typeSet); _additions.add(new Object[] { ss.getRandomAccessId(parent), ss.getRandomAccessId(entry.component), name, types, IndexQueries.idFromGUID(id), name, types, typeIds}); } else { //LOGGER.info("resource " + entry.component + ": no parent for entry " + name + " " + types); } } else { //LOGGER.info("resource " + entry.component + ": " + name + " " + types); } } else if(_entry instanceof ComponentModification) { ComponentModification entry = (ComponentModification)_entry; final String name = graph.getPossibleRelatedValue(entry.component, L0.HasName, Bindings.STRING); if(graph.isInstanceOf(entry.component, L0.Type)) { SerialisationSupport support = session.getService(SerialisationSupport.class); _typeChanges.add(new Pair(name, String.valueOf(support.getRandomAccessId((Resource) entry.component)))); } else { Set typeSet = graph.getTypes(entry.component); if (name != null && !typeSet.isEmpty()) { Resource part = graph.getPossibleObject(entry.component, L0.PartOf); if(part != null) { final GUID id = graph.getPossibleRelatedValue(entry.component, L0.identifier, GUID.BINDING); final String types = graph.syncRequest(new TypeString(L0, typeSet)); final String typeIds = IndexQueries.toResourceIdString(typeSet); _replacementKeys.add(ss.getRandomAccessId(entry.component)); _replacementObjects.add(new Object[] { ss.getRandomAccessId(part), ss.getRandomAccessId(entry.component), name, types, IndexQueries.idFromGUID(id), name, types, typeIds}); } } } } else if (_entry instanceof ComponentRemoval) { ComponentRemoval entry = (ComponentRemoval)_entry; if(!entry.isValid(graph)) continue; _removals.add(ss.getRandomAccessId(((ComponentRemoval)_entry).component)); } else if (_entry instanceof LinkChange) { linkChange = true; } } } final boolean reset = linkChange || event.hasUnresolved; //LOGGER.info("dependencies(" + NameUtils.getSafeLabel(graph, model) + "): reset=" + reset + " linkChange=" + linkChange + " unresolved=" + event.hasUnresolved ); if (reset || !_additions.isEmpty() || !_removals.isEmpty() || !_replacementKeys.isEmpty() || !_typeChanges.isEmpty()) { TimeLogger.log(DependenciesRelation.class, "trackAndIndex.onEvent: starting index update"); final Collection additions = _additions; final Collection removals = _removals; final Collection replacementKeys = _replacementKeys; final Collection replacementObjects = _replacementObjects; final boolean typeNameChanges = typeNameChanges(graph, indexer, model, _typeChanges); final UUID pending = Indexing.makeIndexPending(); try { boolean didChange = false; // Unresolved and linkChanges are not relevant any more boolean doReset = typeNameChanges; if (doReset) { if(DEBUG) { LOGGER.info("resetIndex " + reset + " " + typeNameChanges); } indexer.removeAll(null, graph, DependenciesRelation.this, resource, model); didChange = true; } else { if (!replacementKeys.isEmpty() && (replacementKeys.size() == replacementObjects.size())) { if(DEBUG) { LOGGER.info(replacementKeys.size() + " index replacements: " + replacementKeys); } didChange |= indexer.replace(null, graph, DependenciesRelation.this, resource, model, Dependencies.FIELD_RESOURCE, replacementKeys, replacementObjects); } if (!removals.isEmpty()) { if(DEBUG) { LOGGER.info(removals.size() + " index removals: " + removals); } indexer.remove(null, graph, DependenciesRelation.this, resource, model, Dependencies.FIELD_RESOURCE, removals); didChange = true; } if (!additions.isEmpty()) { if(DEBUG) { for(Object[] os : additions) LOGGER.info("Adding to index " + model + ": " + Arrays.toString(os)); } //LOGGER.info(additions.size() + " index insertions"); indexer.insert(null, graph, DependenciesRelation.this, resource, model, additions); didChange = true; } } if (didChange) // TODO: because this data is ran with // ThreadUtils.getBlockingWorkExecutor() // fireListeners needs to use peekService, // not getService since there is no // guarantee that the session isn't being // disposed while this method is executing. fireListeners(graph, model); } catch (Throwable t) { // Just to know if something unexpected happens here. LOGGER.error("Dependencies index update failed for model " + model + " and relation " + resource + ".", t); // NOTE: Last resort: failure to update index // properly results in removal of the whole index. // This is the only thing that can be done // at this point to ensure that the index will // return correct results in the future, through // complete reinitialization. //indexer.removeAll(null, session, DependenciesRelation.this, resource, model); } finally { Indexing.releaseIndexPending(pending); Indexing.clearCaches(model); } TimeLogger.log(DependenciesRelation.class, "trackAndIndex.onEvent: index update done"); } } } }; GraphChangeListenerSupport changeSupport = processor.getService(GraphChangeListenerSupport.class); changeSupport.addMetadataListener(listener); } trackers++; } private boolean typeNameChanges(ReadGraph graph, IndexedRelations indexer, Resource model, final Collection> typeChanges) throws DatabaseException { if (typeChanges.isEmpty()) return false; for (Pair nr : typeChanges) { String query = Dependencies.FIELD_RESOURCE + ":[" + nr.second + " TO " + nr.second + "]"; //LOGGER.info("query: " + query); List> results = indexer.query(null, query, graph, resource, model, Integer.MAX_VALUE); if (results.size() != 1) { return true; } else { Map result = results.get(0); if (!ObjectUtils.objectEquals(result.get(Dependencies.FIELD_NAME), nr.first)) { return true; } } // LOGGER.info("Type " + nr.first + " was unchanged."); } return false; } @Override public void addListener(RequestProcessor processor, Resource model, Runnable observer) { DependenciesListenerStore store = processor.getSession().getService(DependenciesListenerStore.class); store.addListener(model, observer); } @Override public void removeListener(RequestProcessor processor, Resource model, Runnable observer) { DependenciesListenerStore store = processor.getSession().getService(DependenciesListenerStore.class); store.removeListener(model, observer); } void fireListeners(RequestProcessor processor, Resource model) { DependenciesListenerStore store = processor.getSession().peekService(DependenciesListenerStore.class); if (store != null) store.fireListeners(model); } @Override public void reset(RequestProcessor processor, Resource input) { if (DEBUG) { LOGGER.info("DependenciesRelation.reset: " + input); new Exception("DependenciesRelation.reset(" + listener + ")").printStackTrace(System.out); } DependenciesListenerStore store = processor.getSession().getService(DependenciesListenerStore.class); store.fireListeners(input); } public static void addSubtree(ReadGraph graph, Resource root) throws DatabaseException { Resource indexRoot = graph.syncRequest(new IndexRoot(root)); addSubtree(graph, indexRoot, root); } public static void addSubtree(ReadGraph graph, Resource indexRoot, Resource subtreeRoot) throws DatabaseException { DependenciesRelation dr = new DependenciesRelation(graph, indexRoot); SerialisationSupport ss = graph.getService(SerialisationSupport.class); ArrayList entries = dr.find(graph, subtreeRoot); entries.add(new Entry(graph, subtreeRoot)); ArrayList result = new ArrayList(entries.size()); for (Entry entry : entries) { result.add(new Object[] { ss.getRandomAccessId(entry.parent), ss.getRandomAccessId(entry.resource), entry.name, entry.types, entry.id, entry.name, entry.types, entry.typeId }); } Layer0X L0X = Layer0X.getInstance(graph); IndexedRelations indexer = graph.getService(IndexedRelations.class); indexer.insert(null, graph, dr, L0X.DependenciesRelation, indexRoot, result); } }