/** * Licensed to the Austrian Association for Software Tool Integration (AASTI) * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. The AASTI licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.openengsb.core.ekb.graph.orient.internal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.openengsb.core.api.model.ModelDescription; import org.openengsb.core.ekb.api.ModelGraph; import org.openengsb.core.ekb.api.transformation.TransformationDescription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.orientechnologies.orient.core.Orient; import com.orientechnologies.orient.core.db.graph.OGraphDatabase; import com.orientechnologies.orient.core.index.OIndexes; import com.orientechnologies.orient.core.record.impl.ODocument; import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery; /** * The EKB model graph is an implementation of the model graph. It uses a graph database as storage. * * It usually get filled by two components: the TransformationEngineService and the ModelRegistryService. The * TransformationEngineService inserts all transformations it get saved as new edges into the graph database. The * ModelRegistry notifies the graph whenever new models get available or models get unavailable. */ public final class OrientModelGraph implements ModelGraph { private static final Logger LOGGER = LoggerFactory.getLogger(OrientModelGraph.class); private OGraphDatabase graph; private Map<String, TransformationDescription> descriptions; private AtomicLong counter; private ReadWriteLock lockingMechanism; public OrientModelGraph() { startup(); descriptions = new HashMap<String, TransformationDescription>(); counter = new AtomicLong(0L); lockingMechanism = new ReentrantReadWriteLock(true); } private void startup() { ClassLoader origClassLoader = Thread.currentThread().getContextClassLoader(); try { ClassLoader orientClassLoader = OIndexes.class.getClassLoader(); Thread.currentThread().setContextClassLoader(orientClassLoader); graph = new OGraphDatabase("memory:ekbgraphdb").create(); } finally { Thread.currentThread().setContextClassLoader(origClassLoader); } Orient.instance().removeShutdownHook(); graph.createVertexType("Models"); } /** * Shuts down the graph database */ public void shutdown() { ClassLoader origClassLoader = Thread.currentThread().getContextClassLoader(); try { ClassLoader orientClassLoader = OIndexes.class.getClassLoader(); Thread.currentThread().setContextClassLoader(orientClassLoader); graph.close(); } finally { Thread.currentThread().setContextClassLoader(origClassLoader); } } /** * Deletes all edges and nodes from the graph database. This method is used in the tests to have at every test the * same start situation. */ public void cleanDatabase() { lockingMechanism.writeLock().lock(); try { for (ODocument edge : graph.browseEdges()) { edge.delete(); } for (ODocument node : graph.browseVertices()) { node.delete(); } } finally { lockingMechanism.writeLock().unlock(); } } @Override public void addModel(ModelDescription model) { lockingMechanism.writeLock().lock(); try { ODocument node = getModel(model.toString()); if (node == null) { node = graph.createVertex("Models"); OrientModelGraphUtils.setIdFieldValue(node, model.toString()); } OrientModelGraphUtils.setActiveFieldValue(node, true); node.save(); LOGGER.debug("Added model {} to the graph database", model); } finally { lockingMechanism.writeLock().unlock(); } } @Override public void removeModel(ModelDescription model) { lockingMechanism.writeLock().lock(); try { ODocument node = getModel(model.toString()); if (node == null) { LOGGER.warn("Couldn't remove model {} since it wasn't present in the graph database", model); return; } OrientModelGraphUtils.setActiveFieldValue(node, false); node.save(); LOGGER.debug("Removed model {} from the graph database", model); } finally { lockingMechanism.writeLock().unlock(); } } @Override public void addTransformation(TransformationDescription description) { lockingMechanism.writeLock().lock(); try { checkTransformationDescriptionId(description); ODocument source = getOrCreateModel(description.getSourceModel().toString()); ODocument target = getOrCreateModel(description.getTargetModel().toString()); ODocument edge = graph.createEdge(source, target); OrientModelGraphUtils.setIdFieldValue(edge, description.getId()); if (description.getFileName() != null) { OrientModelGraphUtils.setFilenameFieldValue(edge, description.getFileName()); } OrientModelGraphUtils.fillEdgeWithPropertyConnections(edge, description); edge.save(); descriptions.put(description.getId(), description); LOGGER.debug("Added transformation description {} to the graph database", description); } finally { lockingMechanism.writeLock().unlock(); } } @Override public void removeTransformation(TransformationDescription description) { lockingMechanism.writeLock().lock(); try { String source = description.getSourceModel().toString(); String target = description.getTargetModel().toString(); for (ODocument edge : getEdgesBetweenModels(source, target)) { String id = OrientModelGraphUtils.getIdFieldValue(edge); if (description.getId() == null && isInternalId(id)) { edge.delete(); descriptions.remove(id); LOGGER.debug("Removed transformation description {} from the graph database", id); } else if (id.equals(description.getId())) { edge.delete(); descriptions.remove(id); LOGGER.debug("Removed transformation description {} from the graph database", id); break; } } } finally { lockingMechanism.writeLock().unlock(); } } @Override public List<TransformationDescription> getTransformationsPerFileName(String filename) { lockingMechanism.readLock().lock(); try { String query = String.format("select from E where %s = ?", OrientModelGraphUtils.FILENAME); List<ODocument> edges = graph.query(new OSQLSynchQuery<ODocument>(query), filename); List<TransformationDescription> result = new ArrayList<TransformationDescription>(); for (ODocument edge : edges) { result.add(descriptions.get(OrientModelGraphUtils.getIdFieldValue(edge))); } return result; } finally { lockingMechanism.readLock().unlock(); } } @Override public List<TransformationDescription> getTransformationPath(ModelDescription source, ModelDescription target, List<String> ids) { lockingMechanism.readLock().lock(); try { if (ids == null) { ids = new ArrayList<String>(); } List<ODocument> path = recursivePathSearch(source.toString(), target.toString(), ids, new ODocument[0]); List<TransformationDescription> result = new ArrayList<TransformationDescription>(); if (path != null) { for (ODocument edge : path) { result.add(descriptions.get(OrientModelGraphUtils.getIdFieldValue(edge))); } return result; } throw new IllegalArgumentException("No transformation description found"); } finally { lockingMechanism.readLock().unlock(); } } @Override public Boolean isTransformationPossible(ModelDescription source, ModelDescription target, List<String> ids) { lockingMechanism.readLock().lock(); try { getTransformationPath(source, target, ids); return true; } catch (IllegalArgumentException e) { return false; } finally { lockingMechanism.readLock().unlock(); } } /** * Tests if a transformation description has an id and adds an unique id if it hasn't one. */ private void checkTransformationDescriptionId(TransformationDescription description) { if (description.getId() == null) { description.setId("EKBInternal-" + counter.incrementAndGet()); } } /** * Returns true if the given id is an automatically generated id, or a user defined one. */ private boolean isInternalId(String id) { return id.startsWith("EKBInternal-"); } /** * Returns all edges which start at the source model and end in the target model. */ private List<ODocument> getEdgesBetweenModels(String source, String target) { ODocument from = getModel(source); ODocument to = getModel(target); String query = "select from E where out = ? AND in = ?"; return graph.query(new OSQLSynchQuery<ODocument>(query), from, to); } /** * Returns all neighbors of a model. */ private List<ODocument> getNeighborsOfModel(String model) { String query = String.format("select from Models where in.out.%s in [?]", OGraphDatabase.LABEL); List<ODocument> neighbors = graph.query(new OSQLSynchQuery<ODocument>(query), model); return neighbors; } /** * Returns the model with the given name, or creates one if it isn't existing until then and returns the new one. */ private ODocument getOrCreateModel(String model) { ODocument node = getModel(model); if (node == null) { node = graph.createVertex("Models"); OrientModelGraphUtils.setIdFieldValue(node, model.toString()); OrientModelGraphUtils.setActiveFieldValue(node, false); node.save(); } return node; } /** * Returns the model with the given name. */ private ODocument getModel(String model) { String query = String.format("select from Models where %s = ?", OGraphDatabase.LABEL); List<ODocument> from = graph.query(new OSQLSynchQuery<ODocument>(query), model); if (from.size() > 0) { return from.get(0); } else { return null; } } /** * Recursive path search function. It performs a depth first search with integrated loop check to find a path from * the start model to the end model. If the id list is not empty, then the function only returns a path as valid if * all transformations defined with the id list are in the path. It also takes care of models which aren't * available. Returns null if there is no path found. */ private List<ODocument> recursivePathSearch(String start, String end, List<String> ids, ODocument... steps) { List<ODocument> neighbors = getNeighborsOfModel(start); for (ODocument neighbor : neighbors) { if (alreadyVisited(neighbor, steps) || !OrientModelGraphUtils.getActiveFieldValue(neighbor)) { continue; } ODocument nextStep = getEdgeWithPossibleId(start, OrientModelGraphUtils.getIdFieldValue(neighbor), ids); if (nextStep == null) { continue; } if (OrientModelGraphUtils.getIdFieldValue(neighbor).equals(end)) { List<ODocument> result = new ArrayList<ODocument>(); List<String> copyIds = new ArrayList<String>(ids); for (ODocument step : steps) { String id = OrientModelGraphUtils.getIdFieldValue(step); if (id != null && copyIds.contains(id)) { copyIds.remove(id); } result.add(step); } String id = OrientModelGraphUtils.getIdFieldValue(nextStep); if (id != null && copyIds.contains(id)) { copyIds.remove(id); } result.add(nextStep); if (copyIds.isEmpty()) { return result; } } ODocument[] path = Arrays.copyOf(steps, steps.length + 1); path[path.length - 1] = nextStep; List<ODocument> check = recursivePathSearch(OrientModelGraphUtils.getIdFieldValue(neighbor), end, ids, path); if (check != null) { return check; } } return null; } /** * Returns an edge between the start and the end model. If there is an edge which has an id which is contained in * the given id list, then this transformation is returned. If not, then the first found is returned. */ private ODocument getEdgeWithPossibleId(String start, String end, List<String> ids) { List<ODocument> edges = getEdgesBetweenModels(start, end); for (ODocument edge : edges) { if (ids.contains(OrientModelGraphUtils.getIdFieldValue(edge))) { return edge; } } return edges.size() != 0 ? edges.get(0) : null; } /** * Checks if a model is already visited in the path search algorithm. Needed for the loop detection. */ private boolean alreadyVisited(ODocument neighbor, ODocument[] steps) { for (ODocument step : steps) { ODocument out = graph.getOutVertex(step); if (out.equals(neighbor)) { return true; } } return false; } /** * Returns true if the model is currently active, returns false if not. */ public boolean isModelActive(ModelDescription model) { lockingMechanism.readLock().lock(); try { ODocument node = getModel(model.toString()); return OrientModelGraphUtils.getActiveFieldValue(node); } finally { lockingMechanism.readLock().unlock(); } } }