001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Anahide Tchertchian
018 *     Florent Guillaume
019 *     Remi Cattiau
020 */
021package org.nuxeo.ecm.platform.relations.services;
022
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.HashSet;
026import java.util.Hashtable;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import javax.transaction.Transaction;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.CoreSession;
036import org.nuxeo.ecm.core.repository.RepositoryService;
037import org.nuxeo.ecm.platform.relations.api.DocumentRelationManager;
038import org.nuxeo.ecm.platform.relations.api.Graph;
039import org.nuxeo.ecm.platform.relations.api.GraphDescription;
040import org.nuxeo.ecm.platform.relations.api.GraphFactory;
041import org.nuxeo.ecm.platform.relations.api.RelationManager;
042import org.nuxeo.ecm.platform.relations.api.Resource;
043import org.nuxeo.ecm.platform.relations.api.ResourceAdapter;
044import org.nuxeo.ecm.platform.relations.descriptors.GraphTypeDescriptor;
045import org.nuxeo.ecm.platform.relations.descriptors.ResourceAdapterDescriptor;
046import org.nuxeo.runtime.api.Framework;
047import org.nuxeo.runtime.model.ComponentContext;
048import org.nuxeo.runtime.model.ComponentInstance;
049import org.nuxeo.runtime.model.ComponentName;
050import org.nuxeo.runtime.model.DefaultComponent;
051import org.nuxeo.runtime.transaction.TransactionHelper;
052
053/**
054 * Relation service.
055 * <p>
056 * It handles a registry of graph instances through extension points.
057 */
058public class RelationService extends DefaultComponent implements RelationManager {
059
060    public static final ComponentName NAME = new ComponentName(
061            "org.nuxeo.ecm.platform.relations.services.RelationService");
062
063    private static final long serialVersionUID = -4778456059717447736L;
064
065    private static final Log log = LogFactory.getLog(RelationService.class);
066
067    /** Graph type -> class. */
068    protected final Map<String, Class<?>> graphTypes;
069
070    /** Graph name -> description */
071    protected final Map<String, GraphDescription> graphDescriptions;
072
073    /** Graph name -> factory. */
074    public final Map<String, GraphFactory> graphFactories;
075
076    /** Graph name -> graph instance. */
077    public final Map<String, Graph> graphRegistry;
078
079    protected final Map<String, String> resourceAdapterRegistry;
080
081    public RelationService() {
082        // Hashtable to get implicit synchronization
083        graphTypes = new Hashtable<>();
084        graphDescriptions = new Hashtable<>();
085        graphRegistry = new Hashtable<>();
086        graphFactories = new Hashtable<>();
087        resourceAdapterRegistry = new Hashtable<>();
088    }
089
090    @Override
091    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
092        if (extensionPoint.equals("graphtypes")) {
093            registerGraphType(contribution);
094        } else if (extensionPoint.equals("graphs")) {
095            registerGraph(contribution);
096        } else if (extensionPoint.equals("resourceadapters")) {
097            registerResourceAdapter(contribution);
098        } else {
099            log.error(String.format("Unknown extension point %s, can't register !", extensionPoint));
100        }
101    }
102
103    @Override
104    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
105        if (extensionPoint.equals("graphtypes")) {
106            unregisterGraphType(contribution);
107        } else if (extensionPoint.equals("graphs")) {
108            unregisterGraph(contribution);
109        } else if (extensionPoint.equals("resourceadapters")) {
110            unregisterResourceAdapter(contribution);
111        } else {
112            log.error(String.format("Unknown extension point %s, can't unregister !", extensionPoint));
113        }
114    }
115
116    @Override
117    @SuppressWarnings("unchecked")
118    public <T> T getAdapter(Class<T> adapter) {
119        if (adapter.isAssignableFrom(RelationManager.class)) {
120            return (T) this;
121        } else if (adapter.isAssignableFrom(DocumentRelationManager.class)) {
122            return (T) new DocumentRelationService();
123        }
124        return null;
125    }
126
127    // Graph types
128
129    /**
130     * Registers a graph type, saving a name and a class name.
131     * <p>
132     * The name will be used when registering graphs.
133     */
134    private void registerGraphType(Object contribution) {
135        GraphTypeDescriptor graphTypeDescriptor = (GraphTypeDescriptor) contribution;
136        String graphType = graphTypeDescriptor.getName();
137        String className = graphTypeDescriptor.getClassName();
138
139        if (graphTypes.containsKey(graphType)) {
140            log.error(String.format("Graph type %s already registered using %s", graphType, graphTypes.get(graphType)));
141            return;
142        }
143        Class<?> klass;
144        try {
145            klass = getClass().getClassLoader().loadClass(className);
146        } catch (ClassNotFoundException e) {
147            throw new RuntimeException(
148                    String.format("Cannot register unknown class for graph type %s: %s", graphType, className), e);
149        }
150        if (!Graph.class.isAssignableFrom(klass) && !GraphFactory.class.isAssignableFrom(klass)) {
151            throw new RuntimeException("Invalid graph class/factory type: " + className);
152        }
153        graphTypes.put(graphType, klass);
154        log.info(String.format("Registered graph type: %s (%s)", graphType, className));
155    }
156
157    /**
158     * Unregisters a graph type.
159     */
160    private void unregisterGraphType(Object contrib) {
161        GraphTypeDescriptor graphTypeExtension = (GraphTypeDescriptor) contrib;
162        String graphType = graphTypeExtension.getName();
163        List<GraphDescription> list = new ArrayList<>(graphDescriptions.values()); // copy
164        for (GraphDescription graphDescription : list) {
165            if (graphType.equals(graphDescription.getGraphType())) {
166                String name = graphDescription.getName();
167                graphFactories.remove(name);
168                graphRegistry.remove(name);
169                graphDescriptions.remove(name);
170                log.info("Unregistered graph: " + name);
171            }
172        }
173        graphTypes.remove(graphType);
174        log.info("Unregistered graph type: " + graphType);
175    }
176
177    public List<String> getGraphTypes() {
178        return new ArrayList<>(graphTypes.keySet());
179    }
180
181    /**
182     * Registers a graph instance.
183     * <p>
184     * The graph has to be declared as using a type already registered in the graph type registry.
185     */
186    protected void registerGraph(Object contribution) {
187        GraphDescription graphDescription = (GraphDescription) contribution;
188        String name = graphDescription.getName();
189        if (graphDescriptions.containsKey(name)) {
190            log.info(String.format("Overriding graph %s definition", name));
191            graphDescriptions.remove(name);
192        }
193        graphDescriptions.put(name, graphDescription);
194        log.info("Registered graph: " + name);
195
196        // remove any existing graph instance in case its definition changed
197        graphRegistry.remove(name);
198    }
199
200    /**
201     * Unregisters a graph.
202     */
203    protected void unregisterGraph(Object contribution) {
204        GraphDescription graphDescription = (GraphDescription) contribution;
205        String name = graphDescription.getName();
206        if (graphDescriptions.containsKey(name)) {
207            graphFactories.remove(name);
208            graphRegistry.remove(name);
209            graphDescriptions.remove(name);
210            log.info("Unregistered graph: " + name);
211        }
212    }
213
214    // Resource adapters
215
216    private void registerResourceAdapter(Object contribution) {
217        ResourceAdapterDescriptor adapter = (ResourceAdapterDescriptor) contribution;
218        String ns = adapter.getNamespace();
219        String adapterClassName = adapter.getClassName();
220        if (resourceAdapterRegistry.containsKey(ns)) {
221            log.info("Overriding resource adapter config for namespace " + ns);
222        }
223        resourceAdapterRegistry.put(ns, adapterClassName);
224        log.info(String.format("%s namespace registered using adapter %s", ns, adapterClassName));
225    }
226
227    private void unregisterResourceAdapter(Object contribution) {
228        ResourceAdapterDescriptor adapter = (ResourceAdapterDescriptor) contribution;
229        String ns = adapter.getNamespace();
230        String adapterClassName = adapter.getClassName();
231        String registered = resourceAdapterRegistry.get(ns);
232        if (registered == null) {
233            log.error(String.format("Namespace %s not found", ns));
234        } else if (!registered.equals(adapterClassName)) {
235            log.error(String.format("Namespace %s: wrong class %s", ns, registered));
236        } else {
237            resourceAdapterRegistry.remove(ns);
238            log.info(String.format("%s unregistered, was using %s", ns, adapterClassName));
239        }
240    }
241
242    private ResourceAdapter getResourceAdapterForNamespace(String namespace) {
243        String adapterClassName = resourceAdapterRegistry.get(namespace);
244        if (adapterClassName == null) {
245            log.error(String.format("Cannot find adapter for namespace: %s", namespace));
246            return null;
247        } else {
248            try {
249                // Thread context loader is not working in isolated EARs
250                ResourceAdapter adapter = (ResourceAdapter) RelationService.class.getClassLoader()
251                                                                                 .loadClass(adapterClassName)
252                                                                                 .newInstance();
253                adapter.setNamespace(namespace);
254                return adapter;
255            } catch (ReflectiveOperationException e) {
256                String msg = String.format("Cannot instantiate generator with namespace '%s': %s", namespace, e);
257                log.error(msg);
258                return null;
259            }
260        }
261    }
262
263    // RelationManager interface
264
265    @Override
266    public Graph getGraphByName(String name) {
267        return getGraph(name, null);
268    }
269
270    @Override
271    public Graph getGraph(String name, CoreSession session) {
272        GraphDescription graphDescription = graphDescriptions.get(name);
273        if (graphDescription == null) {
274            throw new RuntimeException("No such graph: " + name);
275        }
276
277        Graph graph = getGraphFromRegistries(graphDescription, session);
278        if (graph != null) {
279            return graph;
280        }
281
282        // check what we have for the graph type
283        Class<?> klass = graphTypes.get(graphDescription.getGraphType());
284        if (Graph.class.isAssignableFrom(klass)) {
285            // instance
286            try {
287                graph = (Graph) klass.newInstance();
288            } catch (ReflectiveOperationException e) {
289                throw new RuntimeException(e);
290            }
291            graphRegistry.put(name, graph);
292        } else { // GraphFactory.class.isAssignableFrom(klass)
293            // factory
294            GraphFactory factory;
295            try {
296                factory = (GraphFactory) klass.newInstance();
297            } catch (ReflectiveOperationException e) {
298                throw new RuntimeException(e);
299            }
300            graphFactories.put(name, factory);
301        }
302
303        return getGraphFromRegistries(graphDescription, session);
304    }
305
306    /** Gets the graph from the registries. */
307    protected Graph getGraphFromRegistries(GraphDescription graphDescription, CoreSession session) {
308        String name = graphDescription.getName();
309        // check instances
310        Graph graph = graphRegistry.get(name);
311        if (graph != null) {
312            graph.setDescription(graphDescription);
313            return graph;
314        }
315
316        // check factories
317        GraphFactory factory = graphFactories.get(name);
318        if (factory != null) {
319            return factory.createGraph(graphDescription, session);
320        }
321
322        return null;
323    }
324
325    protected Graph newGraph(String className) {
326        try {
327            Class<?> klass = getClass().getClassLoader().loadClass(className);
328            return (Graph) klass.newInstance();
329        } catch (ReflectiveOperationException e) {
330            throw new RuntimeException(e);
331        }
332    }
333
334    @Override
335    public Graph getTransientGraph(String type) {
336        Class<?> klass = graphTypes.get(type);
337        if (Graph.class.isAssignableFrom(klass)) {
338            try {
339                return (Graph) klass.newInstance();
340            } catch (ReflectiveOperationException e) {
341                throw new RuntimeException(e);
342            }
343        }
344        throw new RuntimeException("Graph type cannot be transient: " + type);
345    }
346
347    @Override
348    public Resource getResource(String namespace, Serializable object, Map<String, Object> context) {
349        ResourceAdapter adapter = getResourceAdapterForNamespace(namespace);
350        if (adapter == null) {
351            log.error("Cannot find adapter for namespace: " + namespace);
352            return null;
353        } else {
354            return adapter.getResource(object, context);
355        }
356    }
357
358    @Override
359    public Set<Resource> getAllResources(Serializable object, Map<String, Object> context) {
360        // TODO OPTIM implement reverse map in registerContribution
361        Set<Resource> res = new HashSet<>();
362        for (String ns : resourceAdapterRegistry.keySet()) {
363            ResourceAdapter adapter = getResourceAdapterForNamespace(ns);
364            if (adapter == null) {
365                continue;
366            }
367            Class<?> klass = adapter.getKlass();
368            if (klass == null) {
369                continue;
370            }
371            if (klass.isAssignableFrom(object.getClass())) {
372                res.add(adapter.getResource(object, context));
373            }
374        }
375        return res;
376    }
377
378    @Override
379    public Serializable getResourceRepresentation(String namespace, Resource resource, Map<String, Object> context) {
380        ResourceAdapter adapter = getResourceAdapterForNamespace(namespace);
381        if (adapter == null) {
382            log.error("Cannot find adapter for namespace: " + namespace);
383            return null;
384        } else {
385            return adapter.getResourceRepresentation(resource, context);
386        }
387    }
388
389    @Override
390    public List<String> getGraphNames() {
391        return new ArrayList<>(graphDescriptions.keySet());
392    }
393
394    @Override
395    public void start(ComponentContext context) {
396        RepositoryService repositoryService = Framework.getService(RepositoryService.class);
397        if (repositoryService == null) {
398            // RepositoryService failed to start, no need to go further
399            return;
400        }
401        Transaction tx = TransactionHelper.suspendTransaction();
402        try {
403            log.info("Relation Service initialization");
404
405            // init jena Graph outside of Tx
406            for (String graphName : graphDescriptions.keySet()) {
407                GraphDescription desc = graphDescriptions.get(graphName);
408                if (desc.getGraphType().equalsIgnoreCase("jena")) {
409                    log.info("create RDF Graph " + graphName);
410                    Graph graph = getGraphByName(graphName);
411                    graph.size();
412                }
413            }
414        } finally {
415            TransactionHelper.resumeTransaction(tx);
416        }
417
418        // init non jena Graph inside a Tx
419        if (tx == null) {
420            TransactionHelper.startTransaction();
421        }
422        boolean txErrors = true;
423        try {
424            for (String graphName : graphDescriptions.keySet()) {
425                GraphDescription desc = graphDescriptions.get(graphName);
426                if (!desc.getGraphType().equalsIgnoreCase("jena")) {
427                    log.info("create RDF Graph " + graphName);
428                    Graph graph = getGraphByName(graphName);
429                    graph.size();
430                }
431            }
432            txErrors = false;
433        } finally {
434            if (txErrors) {
435                TransactionHelper.setTransactionRollbackOnly();
436            }
437            if (tx == null) {
438                TransactionHelper.commitOrRollbackTransaction();
439            }
440        }
441    }
442
443}