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