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