001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (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.InputStream;
024import java.io.OutputStream;
025import java.io.Serializable;
026import java.util.ArrayList;
027import java.util.HashSet;
028import java.util.Hashtable;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
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.Node;
042import org.nuxeo.ecm.platform.relations.api.QueryResult;
043import org.nuxeo.ecm.platform.relations.api.RelationManager;
044import org.nuxeo.ecm.platform.relations.api.Resource;
045import org.nuxeo.ecm.platform.relations.api.ResourceAdapter;
046import org.nuxeo.ecm.platform.relations.api.Statement;
047import org.nuxeo.ecm.platform.relations.descriptors.GraphTypeDescriptor;
048import org.nuxeo.ecm.platform.relations.descriptors.ResourceAdapterDescriptor;
049import org.nuxeo.runtime.api.Framework;
050import org.nuxeo.runtime.model.ComponentContext;
051import org.nuxeo.runtime.model.ComponentInstance;
052import org.nuxeo.runtime.model.ComponentName;
053import org.nuxeo.runtime.model.DefaultComponent;
054import org.nuxeo.runtime.transaction.TransactionHelper;
055
056/**
057 * Relation service.
058 * <p>
059 * It handles a registry of graph instances through extension points.
060 */
061public class RelationService extends DefaultComponent implements RelationManager {
062
063    public static final ComponentName NAME = new ComponentName(
064            "org.nuxeo.ecm.platform.relations.services.RelationService");
065
066    private static final long serialVersionUID = -4778456059717447736L;
067
068    private static final Log log = LogFactory.getLog(RelationService.class);
069
070    /** Graph type -> class. */
071    protected final Map<String, Class<?>> graphTypes;
072
073    /** Graph name -> description */
074    protected final Map<String, GraphDescription> graphDescriptions;
075
076    /** Graph name -> factory. */
077    public final Map<String, GraphFactory> graphFactories;
078
079    /** Graph name -> graph instance. */
080    public final Map<String, Graph> graphRegistry;
081
082    protected final Map<String, String> resourceAdapterRegistry;
083
084    public RelationService() {
085        // Hashtable to get implicit synchronization
086        graphTypes = new Hashtable<String, Class<?>>();
087        graphDescriptions = new Hashtable<String, GraphDescription>();
088        graphRegistry = new Hashtable<String, Graph>();
089        graphFactories = new Hashtable<String, GraphFactory>();
090        resourceAdapterRegistry = new Hashtable<String, String>();
091    }
092
093    @Override
094    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
095        if (extensionPoint.equals("graphtypes")) {
096            registerGraphType(contribution);
097        } else if (extensionPoint.equals("graphs")) {
098            registerGraph(contribution);
099        } else if (extensionPoint.equals("resourceadapters")) {
100            registerResourceAdapter(contribution);
101        } else {
102            log.error(String.format("Unknown extension point %s, can't register !", extensionPoint));
103        }
104    }
105
106    @Override
107    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
108        if (extensionPoint.equals("graphtypes")) {
109            unregisterGraphType(contribution);
110        } else if (extensionPoint.equals("graphs")) {
111            unregisterGraph(contribution);
112        } else if (extensionPoint.equals("resourceadapters")) {
113            unregisterResourceAdapter(contribution);
114        } else {
115            log.error(String.format("Unknown extension point %s, can't unregister !", extensionPoint));
116        }
117    }
118
119    @Override
120    @SuppressWarnings("unchecked")
121    public <T> T getAdapter(Class<T> adapter) {
122        if (adapter.isAssignableFrom(RelationManager.class)) {
123            return (T) this;
124        } else if (adapter.isAssignableFrom(DocumentRelationManager.class)) {
125            return (T) new DocumentRelationService();
126        }
127        return null;
128    }
129
130    // Graph types
131
132    /**
133     * Registers a graph type, saving a name and a class name.
134     * <p>
135     * The name will be used when registering graphs.
136     */
137    private void registerGraphType(Object contribution) {
138        GraphTypeDescriptor graphTypeDescriptor = (GraphTypeDescriptor) contribution;
139        String graphType = graphTypeDescriptor.getName();
140        String className = graphTypeDescriptor.getClassName();
141
142        if (graphTypes.containsKey(graphType)) {
143            log.error(String.format("Graph type %s already registered using %s", graphType, graphTypes.get(graphType)));
144            return;
145        }
146        Class<?> klass;
147        try {
148            klass = getClass().getClassLoader().loadClass(className);
149        } catch (ClassNotFoundException e) {
150            throw new RuntimeException(String.format("Cannot register unknown class for graph type %s: %s", graphType,
151                    className), e);
152        }
153        if (!Graph.class.isAssignableFrom(klass) && !GraphFactory.class.isAssignableFrom(klass)) {
154            throw new RuntimeException("Invalid graph class/factory type: " + className);
155        }
156        graphTypes.put(graphType, klass);
157        log.info(String.format("Registered graph type: %s (%s)", graphType, className));
158    }
159
160    /**
161     * Unregisters a graph type.
162     */
163    private void unregisterGraphType(Object contrib) {
164        GraphTypeDescriptor graphTypeExtension = (GraphTypeDescriptor) contrib;
165        String graphType = graphTypeExtension.getName();
166        List<GraphDescription> list = new ArrayList<GraphDescription>(graphDescriptions.values()); // copy
167        for (GraphDescription graphDescription : list) {
168            if (graphType.equals(graphDescription.getGraphType())) {
169                String name = graphDescription.getName();
170                graphFactories.remove(name);
171                graphRegistry.remove(name);
172                graphDescriptions.remove(name);
173                log.info("Unregistered graph: " + name);
174            }
175        }
176        graphTypes.remove(graphType);
177        log.info("Unregistered graph type: " + graphType);
178    }
179
180    public List<String> getGraphTypes() {
181        List<String> res = new ArrayList<String>();
182        for (String type : graphTypes.keySet()) {
183            res.add(type);
184        }
185        return res;
186    }
187
188    /**
189     * Registers a graph instance.
190     * <p>
191     * The graph has to be declared as using a type already registered in the graph type registry.
192     */
193    protected void registerGraph(Object contribution) {
194        GraphDescription graphDescription = (GraphDescription) contribution;
195        String name = graphDescription.getName();
196        if (graphDescriptions.containsKey(name)) {
197            log.info(String.format("Overriding graph %s definition", name));
198            graphDescriptions.remove(name);
199        }
200        graphDescriptions.put(name, graphDescription);
201        log.info("Registered graph: " + name);
202
203        // remove any existing graph instance in case its definition changed
204        graphRegistry.remove(name);
205    }
206
207    /**
208     * Unregisters a graph.
209     */
210    protected void unregisterGraph(Object contribution) {
211        GraphDescription graphDescription = (GraphDescription) contribution;
212        String name = graphDescription.getName();
213        if (graphDescriptions.containsKey(name)) {
214            graphFactories.remove(name);
215            graphRegistry.remove(name);
216            graphDescriptions.remove(name);
217            log.info("Unregistered graph: " + name);
218        }
219    }
220
221    // Resource adapters
222
223    private void registerResourceAdapter(Object contribution) {
224        ResourceAdapterDescriptor adapter = (ResourceAdapterDescriptor) contribution;
225        String ns = adapter.getNamespace();
226        String adapterClassName = adapter.getClassName();
227        if (resourceAdapterRegistry.containsKey(ns)) {
228            log.info("Overriding resource adapter config for namespace " + ns);
229        }
230        resourceAdapterRegistry.put(ns, adapterClassName);
231        log.info(String.format("%s namespace registered using adapter %s", ns, adapterClassName));
232    }
233
234    private void unregisterResourceAdapter(Object contribution) {
235        ResourceAdapterDescriptor adapter = (ResourceAdapterDescriptor) contribution;
236        String ns = adapter.getNamespace();
237        String adapterClassName = adapter.getClassName();
238        String registered = resourceAdapterRegistry.get(ns);
239        if (registered == null) {
240            log.error(String.format("Namespace %s not found", ns));
241        } else if (!registered.equals(adapterClassName)) {
242            log.error(String.format("Namespace %s: wrong class %s", ns, registered));
243        } else {
244            resourceAdapterRegistry.remove(ns);
245            log.info(String.format("%s unregistered, was using %s", ns, adapterClassName));
246        }
247    }
248
249    private ResourceAdapter getResourceAdapterForNamespace(String namespace) {
250        String adapterClassName = resourceAdapterRegistry.get(namespace);
251        if (adapterClassName == null) {
252            log.error(String.format("Cannot find adapter for namespace: %s", namespace));
253            return null;
254        } else {
255            try {
256                // Thread context loader is not working in isolated EARs
257                ResourceAdapter adapter = (ResourceAdapter) RelationService.class.getClassLoader().loadClass(
258                        adapterClassName).newInstance();
259                adapter.setNamespace(namespace);
260                return adapter;
261            } catch (ReflectiveOperationException e) {
262                String msg = String.format("Cannot instantiate generator with namespace '%s': %s", namespace, e);
263                log.error(msg);
264                return null;
265            }
266        }
267    }
268
269    // RelationManager interface
270
271    @Override
272    public Graph getGraphByName(String name) {
273        return getGraph(name, null);
274    }
275
276    @Override
277    public Graph getGraph(String name, CoreSession session) {
278        GraphDescription graphDescription = graphDescriptions.get(name);
279        if (graphDescription == null) {
280            throw new RuntimeException("No such graph: " + name);
281        }
282
283        Graph graph = getGraphFromRegistries(graphDescription, session);
284        if (graph != null) {
285            return graph;
286        }
287
288        // check what we have for the graph type
289        Class<?> klass = graphTypes.get(graphDescription.getGraphType());
290        if (Graph.class.isAssignableFrom(klass)) {
291            // instance
292            try {
293                graph = (Graph) klass.newInstance();
294            } catch (ReflectiveOperationException e) {
295                throw new RuntimeException(e);
296            }
297            graphRegistry.put(name, graph);
298        } else { // GraphFactory.class.isAssignableFrom(klass)
299            // factory
300            GraphFactory factory;
301            try {
302                factory = (GraphFactory) klass.newInstance();
303            } catch (ReflectiveOperationException e) {
304                throw new RuntimeException(e);
305            }
306            graphFactories.put(name, factory);
307        }
308
309        return getGraphFromRegistries(graphDescription, session);
310    }
311
312    /** Gets the graph from the registries. */
313    protected Graph getGraphFromRegistries(GraphDescription graphDescription, CoreSession session) {
314        String name = graphDescription.getName();
315        // check instances
316        Graph graph = graphRegistry.get(name);
317        if (graph != null) {
318            graph.setDescription(graphDescription);
319            return graph;
320        }
321
322        // check factories
323        GraphFactory factory = graphFactories.get(name);
324        if (factory != null) {
325            return factory.createGraph(graphDescription, session);
326        }
327
328        return null;
329    }
330
331    protected Graph newGraph(String className) {
332        try {
333            Class<?> klass = getClass().getClassLoader().loadClass(className);
334            return (Graph) klass.newInstance();
335        } catch (ReflectiveOperationException e) {
336            throw new RuntimeException(e);
337        }
338    }
339
340    @Override
341    public Graph getTransientGraph(String type) {
342        Class<?> klass = graphTypes.get(type);
343        if (Graph.class.isAssignableFrom(klass)) {
344            try {
345                return (Graph) klass.newInstance();
346            } catch (ReflectiveOperationException e) {
347                throw new RuntimeException(e);
348            }
349        }
350        throw new RuntimeException("Graph type cannot be transient: " + type);
351    }
352
353    @Override
354    public Resource getResource(String namespace, Serializable object, Map<String, Object> context)
355            {
356        ResourceAdapter adapter = getResourceAdapterForNamespace(namespace);
357        if (adapter == null) {
358            log.error("Cannot find adapter for namespace: " + namespace);
359            return null;
360        } else {
361            return adapter.getResource(object, context);
362        }
363    }
364
365    @Override
366    public Set<Resource> getAllResources(Serializable object, Map<String, Object> context) {
367        // TODO OPTIM implement reverse map in registerContribution
368        Set<Resource> res = new HashSet<Resource>();
369        for (String ns : resourceAdapterRegistry.keySet()) {
370            ResourceAdapter adapter = getResourceAdapterForNamespace(ns);
371            if (adapter == null) {
372                continue;
373            }
374            Class<?> klass = adapter.getKlass();
375            if (klass == null) {
376                continue;
377            }
378            if (klass.isAssignableFrom(object.getClass())) {
379                res.add(adapter.getResource(object, context));
380            }
381        }
382        return res;
383    }
384
385    @Override
386    public Serializable getResourceRepresentation(String namespace, Resource resource, Map<String, Object> context)
387            {
388        ResourceAdapter adapter = getResourceAdapterForNamespace(namespace);
389        if (adapter == null) {
390            log.error("Cannot find adapter for namespace: " + namespace);
391            return null;
392        } else {
393            return adapter.getResourceRepresentation(resource, context);
394        }
395    }
396
397    @Override
398    @Deprecated
399    public void add(String graphName, List<Statement> statements) {
400        getGraphByName(graphName).add(statements);
401    }
402
403    @Override
404    @Deprecated
405    public void clear(String graphName) {
406        getGraphByName(graphName).clear();
407    }
408
409    @Override
410    @Deprecated
411    public List<Node> getObjects(String graphName, Node subject, Node predicate) {
412        return getGraphByName(graphName).getObjects(subject, predicate);
413    }
414
415    @Override
416    @Deprecated
417    public List<Node> getPredicates(String graphName, Node subject, Node object) {
418        return getGraphByName(graphName).getPredicates(subject, object);
419    }
420
421    @Override
422    @Deprecated
423    public List<Statement> getStatements(String graphName, Statement statement) {
424        return getGraphByName(graphName).getStatements(statement);
425    }
426
427    @Override
428    @Deprecated
429    public List<Statement> getStatements(String graphName) {
430        return getGraphByName(graphName).getStatements();
431    }
432
433    @Override
434    @Deprecated
435    public List<Node> getSubjects(String graphName, Node predicate, Node object) {
436        return getGraphByName(graphName).getSubjects(predicate, object);
437    }
438
439    @Override
440    @Deprecated
441    public boolean hasResource(String graphName, Resource resource) {
442        return getGraphByName(graphName).hasResource(resource);
443    }
444
445    @Override
446    @Deprecated
447    public boolean hasStatement(String graphName, Statement statement) {
448        return getGraphByName(graphName).hasStatement(statement);
449    }
450
451    @Override
452    @Deprecated
453    public QueryResult query(String graphName, String queryString, String language, String baseURI)
454            {
455        return getGraphByName(graphName).query(queryString, language, baseURI);
456    }
457
458    @Override
459    @Deprecated
460    public boolean read(String graphName, InputStream in, String lang, String base) {
461        return getGraphByName(graphName).read(in, lang, base);
462    }
463
464    @Override
465    @Deprecated
466    public void remove(String graphName, List<Statement> statements) {
467        getGraphByName(graphName).remove(statements);
468    }
469
470    @Override
471    @Deprecated
472    public Long size(String graphName) {
473        return getGraphByName(graphName).size();
474    }
475
476    @Override
477    @Deprecated
478    public boolean write(String graphName, OutputStream out, String lang, String base) {
479        return getGraphByName(graphName).write(out, lang, base);
480    }
481
482    @Override
483    public List<String> getGraphNames() {
484        return new ArrayList<String>(graphDescriptions.keySet());
485    }
486
487    @Override
488    public void applicationStarted(ComponentContext context) {
489        RepositoryService repositoryService = Framework.getService(RepositoryService.class);
490        if (repositoryService == null) {
491            // RepositoryService failed to start, no need to go further
492            return;
493        }
494        Thread t = new Thread("relation-service-init") {
495            @Override
496            public void run() {
497                Thread.currentThread().setContextClassLoader(RelationService.class.getClassLoader());
498                log.info("Relation Service initialization");
499
500                // init jena Graph outside of Tx
501                for (String graphName : graphDescriptions.keySet()) {
502                    GraphDescription desc = graphDescriptions.get(graphName);
503                    if (desc.getGraphType().equalsIgnoreCase("jena")) {
504                        log.info("create RDF Graph " + graphName);
505                        Graph graph = getGraphByName(graphName);
506                        graph.size();
507                    }
508                }
509
510                // init non jena Graph inside a Tx
511                TransactionHelper.startTransaction();
512                try {
513                    for (String graphName : graphDescriptions.keySet()) {
514                        GraphDescription desc = graphDescriptions.get(graphName);
515                        if (!desc.getGraphType().equalsIgnoreCase("jena")) {
516                            log.info("create RDF Graph " + graphName);
517                            Graph graph = getGraphByName(graphName);
518                            graph.size();
519                        }
520                    }
521                } finally {
522                    TransactionHelper.commitOrRollbackTransaction();
523                }
524            }
525        };
526        t.start();
527        try {
528            t.join();
529        } catch (InterruptedException e) {
530            log.error("Cannot join init thread", e);
531        }
532    }
533
534}