001/*
002 * (C) Copyright 2006-2007 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 *     <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
018 *
019 * $Id: IORelationAdapter.java 26168 2007-10-18 11:21:21Z dmihalache $
020 */
021
022package org.nuxeo.ecm.platform.relations.io;
023
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.io.Serializable;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.Date;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.nuxeo.ecm.core.api.CloseableCoreSession;
041import org.nuxeo.ecm.core.api.CoreInstance;
042import org.nuxeo.ecm.core.api.CoreSession;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.DocumentNotFoundException;
045import org.nuxeo.ecm.core.api.DocumentRef;
046import org.nuxeo.ecm.core.api.IdRef;
047import org.nuxeo.ecm.core.io.DocumentTranslationMap;
048import org.nuxeo.ecm.platform.io.api.AbstractIOResourceAdapter;
049import org.nuxeo.ecm.platform.io.api.IOResources;
050import org.nuxeo.ecm.platform.relations.api.Graph;
051import org.nuxeo.ecm.platform.relations.api.Literal;
052import org.nuxeo.ecm.platform.relations.api.Node;
053import org.nuxeo.ecm.platform.relations.api.QNameResource;
054import org.nuxeo.ecm.platform.relations.api.RelationManager;
055import org.nuxeo.ecm.platform.relations.api.Resource;
056import org.nuxeo.ecm.platform.relations.api.ResourceAdapter;
057import org.nuxeo.ecm.platform.relations.api.Statement;
058import org.nuxeo.ecm.platform.relations.api.Subject;
059import org.nuxeo.ecm.platform.relations.api.impl.RelationDate;
060import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl;
061import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl;
062import org.nuxeo.runtime.api.Framework;
063
064/**
065 * Adapter for import/export of relations
066 *
067 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
068 */
069public class IORelationAdapter extends AbstractIOResourceAdapter {
070
071    private static final Log log = LogFactory.getLog(IORelationAdapter.class);
072
073    private static final long serialVersionUID = -3661302796286246086L;
074
075    @Override
076    public void setProperties(Map<String, Serializable> properties) {
077        if (properties != null) {
078            for (Map.Entry<String, Serializable> prop : properties.entrySet()) {
079                String propName = prop.getKey();
080                Serializable propValue = prop.getValue();
081                if (IORelationAdapterProperties.GRAPH.equals(propName)) {
082                    setStringProperty(propName, propValue);
083                }
084                if (IORelationAdapterProperties.IMPORT_GRAPH.equals(propName)) {
085                    setStringProperty(propName, propValue);
086                }
087                if (IORelationAdapterProperties.IGNORE_EXTERNAL.equals(propName)) {
088                    setBooleanProperty(propName, propValue);
089                }
090                if (IORelationAdapterProperties.IGNORE_LITERALS.equals(propName)) {
091                    setBooleanProperty(propName, propValue);
092                }
093                if (IORelationAdapterProperties.IGNORE_SIMPLE_RESOURCES.equals(propName)) {
094                    setBooleanProperty(propName, propValue);
095                }
096                if (IORelationAdapterProperties.FILTER_PREDICATES.equals(propName)) {
097                    setStringArrayProperty(propName, propValue);
098                }
099                if (IORelationAdapterProperties.IGNORE_PREDICATES.equals(propName)) {
100                    setStringArrayProperty(propName, propValue);
101                }
102                if (IORelationAdapterProperties.FILTER_METADATA.equals(propName)) {
103                    setStringArrayProperty(propName, propValue);
104                }
105                if (IORelationAdapterProperties.IGNORE_METADATA.equals(propName)) {
106                    setStringArrayProperty(propName, propValue);
107                }
108                if (IORelationAdapterProperties.IGNORE_ALL_METADATA.equals(propName)) {
109                    setBooleanProperty(propName, propValue);
110                }
111                if (IORelationAdapterProperties.UPDATE_DATE_METADATA.equals(propName)) {
112                    setStringArrayProperty(propName, propValue);
113                }
114            }
115        }
116        if (this.properties == null || getStringProperty(IORelationAdapterProperties.GRAPH) == null) {
117            log.warn("No graph name given for relations adapter, " + "no IO will be performed with this adapter");
118        }
119    }
120
121    protected RelationManager getRelationManager() {
122        return Framework.getService(RelationManager.class);
123    }
124
125    protected List<Statement> getMatchingStatements(Graph graph, Resource resource) {
126        // TODO filter using properties
127        List<Statement> matching = new ArrayList<Statement>();
128        Statement incomingPattern = new StatementImpl(null, null, resource);
129        matching.addAll(graph.getStatements(incomingPattern));
130        Statement outgoingPattern = new StatementImpl(resource, null, null);
131        matching.addAll(graph.getStatements(outgoingPattern));
132        return filterMatchingStatements(matching);
133    }
134
135    protected Statement getFilteredStatement(Statement statement) {
136        Subject subject = statement.getSubject();
137        Resource predicate = statement.getPredicate();
138        Node object = statement.getObject();
139        if (getBooleanProperty(IORelationAdapterProperties.IGNORE_LITERALS) && object.isLiteral()) {
140            return null;
141        }
142        if (getBooleanProperty(IORelationAdapterProperties.IGNORE_SIMPLE_RESOURCES)) {
143            if (!subject.isQNameResource() || !object.isQNameResource()) {
144                return null;
145            }
146        }
147        String[] filteredPredicates = getStringArrayProperty(IORelationAdapterProperties.FILTER_PREDICATES);
148        if (filteredPredicates != null) {
149            if (!Arrays.asList(filteredPredicates).contains(predicate.getUri())) {
150                return null;
151            }
152        }
153        String[] ignoredPredicates = getStringArrayProperty(IORelationAdapterProperties.IGNORE_PREDICATES);
154        if (ignoredPredicates != null) {
155            if (Arrays.asList(ignoredPredicates).contains(predicate.getUri())) {
156                return null;
157            }
158        }
159        if (getBooleanProperty(IORelationAdapterProperties.IGNORE_ALL_METADATA)) {
160            Statement newStatement = (Statement) statement.clone();
161            newStatement.deleteProperties();
162            return newStatement;
163        }
164        String[] filterMetadata = getStringArrayProperty(IORelationAdapterProperties.FILTER_METADATA);
165        if (filterMetadata != null) {
166            Statement newStatement = (Statement) statement.clone();
167            Map<Resource, Node[]> props = newStatement.getProperties();
168            List<String> filter = Arrays.asList(filterMetadata);
169            for (Map.Entry<Resource, Node[]> prop : props.entrySet()) {
170                Resource propKey = prop.getKey();
171                if (!filter.contains(propKey.getUri())) {
172                    newStatement.deleteProperty(propKey);
173                }
174            }
175            return newStatement;
176        }
177        String[] ignoreMetadata = getStringArrayProperty(IORelationAdapterProperties.IGNORE_METADATA);
178        if (ignoreMetadata != null) {
179            Statement newStatement = (Statement) statement.clone();
180            Map<Resource, Node[]> props = newStatement.getProperties();
181            List<String> filter = Arrays.asList(ignoreMetadata);
182            for (Map.Entry<Resource, Node[]> prop : props.entrySet()) {
183                Resource propKey = prop.getKey();
184                if (filter.contains(propKey.getUri())) {
185                    newStatement.deleteProperty(propKey);
186                }
187            }
188            return newStatement;
189        }
190        return statement;
191    }
192
193    protected List<Statement> filterMatchingStatements(List<Statement> statements) {
194        List<Statement> newStatements = null;
195        if (statements != null) {
196            newStatements = new ArrayList<Statement>();
197            for (Statement stmt : statements) {
198                Statement newStmt = getFilteredStatement(stmt);
199                if (newStmt != null) {
200                    newStatements.add(newStmt);
201                }
202            }
203        }
204        return newStatements;
205    }
206
207    protected DocumentRef getDocumentRef(RelationManager relManager, QNameResource resource) {
208        String ns = resource.getNamespace();
209        if ("http://www.nuxeo.org/document/uid/".equals(ns)) {
210            // BS: Avoid using default resource resolver since it is not working
211            // when
212            // the resource document is not currently existing in the target
213            // repository.
214            // TODO This is a hack and should be fixed in the lower layers or by
215            // changing
216            // import logic.
217            String id = resource.getLocalName();
218            int p = id.indexOf('/');
219            if (p > -1) {
220                id = id.substring(p + 1);
221            }
222            return new IdRef(id);
223        }
224        return null;
225    }
226
227    /**
228     * Extract relations involving given documents.
229     * <p>
230     * The adapter properties will filter which relations must be taken into account.
231     */
232    @Override
233    public IOResources extractResources(String repo, Collection<DocumentRef> sources) {
234        if (sources == null || sources.isEmpty()) {
235            return null;
236        }
237        String graphName = getStringProperty(IORelationAdapterProperties.GRAPH);
238        if (graphName == null) {
239            log.error("Cannot extract resources, no graph supplied");
240            return null;
241        }
242        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(repo)) {
243            RelationManager relManager = getRelationManager();
244            Graph graph = relManager.getGraphByName(graphName);
245            if (graph == null) {
246                log.error("Cannot resolve graph " + graphName);
247                return null;
248            }
249            Map<DocumentRef, Set<Resource>> docResources = new HashMap<DocumentRef, Set<Resource>>();
250            List<Statement> statements = new ArrayList<Statement>();
251            Set<Resource> allResources = new HashSet<Resource>();
252            for (DocumentRef docRef : sources) {
253                DocumentModel doc = session.getDocument(docRef);
254                Map<String, Object> context = Collections.<String, Object> singletonMap(
255                        ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session);
256                Set<Resource> resources = relManager.getAllResources(doc, context);
257                docResources.put(docRef, resources);
258                allResources.addAll(resources);
259                for (Resource resource : resources) {
260                    statements.addAll(getMatchingStatements(graph, resource));
261                }
262            }
263            Map<String, String> namespaces = graph.getNamespaces();
264            // filter duplicate statements + statements involving external
265            // resources
266            IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, statements);
267            Graph memoryGraph = graphHelper.getGraph();
268            List<Statement> toRemove = new ArrayList<Statement>();
269            if (getBooleanProperty(IORelationAdapterProperties.IGNORE_EXTERNAL)) {
270                for (Statement stmt : memoryGraph.getStatements()) {
271                    Subject subject = stmt.getSubject();
272                    if (subject.isQNameResource()) {
273                        if (!allResources.contains(subject)) {
274                            toRemove.add(stmt);
275                            continue;
276                        }
277                    }
278                    Node object = stmt.getObject();
279                    if (object.isQNameResource()) {
280                        if (!allResources.contains(subject)) {
281                            toRemove.add(stmt);
282                            continue;
283                        }
284                    }
285                }
286            }
287            memoryGraph.remove(toRemove);
288            return new IORelationResources(namespaces, docResources, memoryGraph.getStatements());
289        }
290    }
291
292    @Override
293    public void getResourcesAsXML(OutputStream out, IOResources resources) {
294        if (!(resources instanceof IORelationResources)) {
295            return;
296        }
297        IORelationResources relResources = (IORelationResources) resources;
298        Map<String, String> namespaces = relResources.getNamespaces();
299        List<Statement> statements = relResources.getStatements();
300        IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, statements);
301        graphHelper.write(out);
302    }
303
304    private void addResourceEntry(RelationManager relManager, Map<DocumentRef, Set<Resource>> map, Node node) {
305        if (!node.isQNameResource()) {
306            return;
307        }
308        QNameResource resource = (QNameResource) node;
309        DocumentRef docRef = getDocumentRef(relManager, resource);
310        if (docRef == null) {
311            return;
312        }
313        if (map.containsKey(docRef)) {
314            map.get(docRef).add(resource);
315        } else {
316            Set<Resource> set = new HashSet<Resource>();
317            set.add(resource);
318            map.put(docRef, set);
319        }
320    }
321
322    @Override
323    public IOResources loadResourcesFromXML(InputStream in) {
324        RelationManager relManager = getRelationManager();
325        String graphName = getStringProperty(IORelationAdapterProperties.IMPORT_GRAPH);
326        if (graphName == null) {
327            graphName = getStringProperty(IORelationAdapterProperties.GRAPH);
328        }
329        // XXX find target graph to retrieve namespaces
330        Map<String, String> namespaces = null;
331        if (graphName != null) {
332            Graph graph = relManager.getGraphByName(graphName);
333            if (graph != null) {
334                namespaces = graph.getNamespaces();
335            }
336        }
337        IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, null);
338        graphHelper.read(in);
339        // find documents related to given statements
340        List<Statement> statements = filterMatchingStatements(graphHelper.getStatements());
341        Map<DocumentRef, Set<Resource>> docResources = new HashMap<DocumentRef, Set<Resource>>();
342        for (Statement statement : statements) {
343            Subject subject = statement.getSubject();
344            addResourceEntry(relManager, docResources, subject);
345            Node object = statement.getObject();
346            addResourceEntry(relManager, docResources, object);
347        }
348        return new IORelationResources(namespaces, docResources, statements);
349    }
350
351    @Override
352    public void storeResources(IOResources resources) {
353        if (!(resources instanceof IORelationResources)) {
354            return;
355        }
356        IORelationResources relResources = (IORelationResources) resources;
357        String graphName = getStringProperty(IORelationAdapterProperties.IMPORT_GRAPH);
358        if (graphName == null) {
359            graphName = getStringProperty(IORelationAdapterProperties.GRAPH);
360        }
361        if (graphName == null) {
362            log.error("Cannot find graph name");
363            return;
364        }
365        RelationManager relManager = getRelationManager();
366        Graph graph = relManager.getGraphByName(graphName);
367        if (graph == null) {
368            log.error("Cannot find graph with name " + graphName);
369            return;
370        }
371        graph.add(relResources.getStatements());
372    }
373
374    protected static Statement updateDate(Statement statement, Literal newDate, List<Resource> properties) {
375        for (Resource property : properties) {
376            // do not update if not present
377            if (statement.getProperty(property) != null) {
378                statement.setProperty(property, newDate);
379            }
380        }
381        return statement;
382    }
383
384    @Override
385    public IOResources translateResources(String repo, IOResources resources, DocumentTranslationMap map) {
386        if (map == null) {
387            return null;
388        }
389        if (!(resources instanceof IORelationResources)) {
390            return resources;
391        }
392        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(repo)) {
393            IORelationResources relResources = (IORelationResources) resources;
394            Map<String, String> namespaces = relResources.getNamespaces();
395            IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, relResources.getStatements());
396            Graph graph = graphHelper.getGraph();
397            RelationManager relManager = getRelationManager();
398            // variables for date update
399            Literal newDate = RelationDate.getLiteralDate(new Date());
400            String[] dateUris = getStringArrayProperty(IORelationAdapterProperties.UPDATE_DATE_METADATA);
401            List<Resource> dateProperties = new ArrayList<Resource>();
402            if (dateUris != null) {
403                for (String dateUri : dateUris) {
404                    dateProperties.add(new ResourceImpl(dateUri));
405                }
406            }
407            for (Map.Entry<DocumentRef, Set<Resource>> entry : relResources.getResourcesMap().entrySet()) {
408                DocumentRef oldRef = entry.getKey();
409                DocumentRef newRef = map.getDocRefMap().get(oldRef);
410                Set<Resource> docResources = relResources.getDocumentResources(oldRef);
411                for (Resource resource : docResources) {
412                    if (!resource.isQNameResource() || oldRef.equals(newRef)) {
413                        // cannot translate or no change => keep same
414                        continue;
415                    }
416                    Statement pattern = new StatementImpl(resource, null, null);
417                    List<Statement> outgoing = graph.getStatements(pattern);
418                    pattern = new StatementImpl(null, null, resource);
419                    List<Statement> incoming = graph.getStatements(pattern);
420
421                    // remove old statements
422                    graph.remove(outgoing);
423                    graph.remove(incoming);
424
425                    if (newRef == null) {
426                        // do not replace
427                        continue;
428                    }
429
430                    DocumentModel newDoc;
431                    try {
432                        newDoc = session.getDocument(newRef);
433                    } catch (DocumentNotFoundException e) {
434                        // do not replace
435                        continue;
436                    }
437                    QNameResource qnameRes = (QNameResource) resource;
438                    Map<String, Object> context = Collections.<String, Object> singletonMap(
439                            ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session);
440                    Resource newResource = relManager.getResource(qnameRes.getNamespace(), newDoc, context);
441                    Statement newStatement;
442                    List<Statement> newOutgoing = new ArrayList<Statement>();
443                    for (Statement stmt : outgoing) {
444                        newStatement = (Statement) stmt.clone();
445                        newStatement.setSubject(newResource);
446                        if (dateProperties != null) {
447                            newStatement = updateDate(newStatement, newDate, dateProperties);
448                        }
449                        newOutgoing.add(newStatement);
450                    }
451                    graph.add(newOutgoing);
452                    List<Statement> newIncoming = new ArrayList<Statement>();
453                    for (Statement stmt : incoming) {
454                        newStatement = (Statement) stmt.clone();
455                        newStatement.setObject(newResource);
456                        if (dateProperties != null) {
457                            newStatement = updateDate(newStatement, newDate, dateProperties);
458                        }
459                        newIncoming.add(newStatement);
460                    }
461                    graph.add(newIncoming);
462                }
463            }
464            return new IORelationResources(namespaces, relResources.getResourcesMap(), graph.getStatements());
465        }
466    }
467}