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