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