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<>();
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<>();
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        CoreSession session = CoreInstance.getCoreSessionSystem(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<>();
249        List<Statement> statements = new ArrayList<>();
250        Set<Resource> allResources = new HashSet<>();
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<>();
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    @Override
291    public void getResourcesAsXML(OutputStream out, IOResources resources) {
292        if (!(resources instanceof IORelationResources)) {
293            return;
294        }
295        IORelationResources relResources = (IORelationResources) resources;
296        Map<String, String> namespaces = relResources.getNamespaces();
297        List<Statement> statements = relResources.getStatements();
298        IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, statements);
299        graphHelper.write(out);
300    }
301
302    private void addResourceEntry(RelationManager relManager, Map<DocumentRef, Set<Resource>> map, Node node) {
303        if (!node.isQNameResource()) {
304            return;
305        }
306        QNameResource resource = (QNameResource) node;
307        DocumentRef docRef = getDocumentRef(relManager, resource);
308        if (docRef == null) {
309            return;
310        }
311        if (map.containsKey(docRef)) {
312            map.get(docRef).add(resource);
313        } else {
314            Set<Resource> set = new HashSet<>();
315            set.add(resource);
316            map.put(docRef, set);
317        }
318    }
319
320    @Override
321    public IOResources loadResourcesFromXML(InputStream in) {
322        RelationManager relManager = getRelationManager();
323        String graphName = getStringProperty(IORelationAdapterProperties.IMPORT_GRAPH);
324        if (graphName == null) {
325            graphName = getStringProperty(IORelationAdapterProperties.GRAPH);
326        }
327        // XXX find target graph to retrieve namespaces
328        Map<String, String> namespaces = null;
329        if (graphName != null) {
330            Graph graph = relManager.getGraphByName(graphName);
331            if (graph != null) {
332                namespaces = graph.getNamespaces();
333            }
334        }
335        IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, null);
336        graphHelper.read(in);
337        // find documents related to given statements
338        List<Statement> statements = filterMatchingStatements(graphHelper.getStatements());
339        Map<DocumentRef, Set<Resource>> docResources = new HashMap<>();
340        for (Statement statement : statements) {
341            Subject subject = statement.getSubject();
342            addResourceEntry(relManager, docResources, subject);
343            Node object = statement.getObject();
344            addResourceEntry(relManager, docResources, object);
345        }
346        return new IORelationResources(namespaces, docResources, statements);
347    }
348
349    @Override
350    public void storeResources(IOResources resources) {
351        if (!(resources instanceof IORelationResources)) {
352            return;
353        }
354        IORelationResources relResources = (IORelationResources) resources;
355        String graphName = getStringProperty(IORelationAdapterProperties.IMPORT_GRAPH);
356        if (graphName == null) {
357            graphName = getStringProperty(IORelationAdapterProperties.GRAPH);
358        }
359        if (graphName == null) {
360            log.error("Cannot find graph name");
361            return;
362        }
363        RelationManager relManager = getRelationManager();
364        Graph graph = relManager.getGraphByName(graphName);
365        if (graph == null) {
366            log.error("Cannot find graph with name " + graphName);
367            return;
368        }
369        graph.add(relResources.getStatements());
370    }
371
372    protected static Statement updateDate(Statement statement, Literal newDate, List<Resource> properties) {
373        for (Resource property : properties) {
374            // do not update if not present
375            if (statement.getProperty(property) != null) {
376                statement.setProperty(property, newDate);
377            }
378        }
379        return statement;
380    }
381
382    @Override
383    public IOResources translateResources(String repo, IOResources resources, DocumentTranslationMap map) {
384        if (map == null) {
385            return null;
386        }
387        if (!(resources instanceof IORelationResources)) {
388            return resources;
389        }
390        CoreSession session = CoreInstance.getCoreSessionSystem(repo);
391        IORelationResources relResources = (IORelationResources) resources;
392        Map<String, String> namespaces = relResources.getNamespaces();
393        IORelationGraphHelper graphHelper = new IORelationGraphHelper(namespaces, relResources.getStatements());
394        Graph graph = graphHelper.getGraph();
395        RelationManager relManager = getRelationManager();
396        // variables for date update
397        Literal newDate = RelationDate.getLiteralDate(new Date());
398        String[] dateUris = getStringArrayProperty(IORelationAdapterProperties.UPDATE_DATE_METADATA);
399        List<Resource> dateProperties = new ArrayList<>();
400        if (dateUris != null) {
401            for (String dateUri : dateUris) {
402                dateProperties.add(new ResourceImpl(dateUri));
403            }
404        }
405        for (Map.Entry<DocumentRef, Set<Resource>> entry : relResources.getResourcesMap().entrySet()) {
406            DocumentRef oldRef = entry.getKey();
407            DocumentRef newRef = map.getDocRefMap().get(oldRef);
408            Set<Resource> docResources = relResources.getDocumentResources(oldRef);
409            for (Resource resource : docResources) {
410                if (!resource.isQNameResource() || oldRef.equals(newRef)) {
411                    // cannot translate or no change => keep same
412                    continue;
413                }
414                Statement pattern = new StatementImpl(resource, null, null);
415                List<Statement> outgoing = graph.getStatements(pattern);
416                pattern = new StatementImpl(null, null, resource);
417                List<Statement> incoming = graph.getStatements(pattern);
418
419                // remove old statements
420                graph.remove(outgoing);
421                graph.remove(incoming);
422
423                if (newRef == null) {
424                    // do not replace
425                    continue;
426                }
427
428                DocumentModel newDoc;
429                try {
430                    newDoc = session.getDocument(newRef);
431                } catch (DocumentNotFoundException e) {
432                    // do not replace
433                    continue;
434                }
435                QNameResource qnameRes = (QNameResource) resource;
436                Map<String, Object> context = Collections.<String, Object> singletonMap(
437                        ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session);
438                Resource newResource = relManager.getResource(qnameRes.getNamespace(), newDoc, context);
439                Statement newStatement;
440                List<Statement> newOutgoing = new ArrayList<>();
441                for (Statement stmt : outgoing) {
442                    newStatement = (Statement) stmt.clone();
443                    newStatement.setSubject(newResource);
444                    if (dateProperties != null) {
445                        newStatement = updateDate(newStatement, newDate, dateProperties);
446                    }
447                    newOutgoing.add(newStatement);
448                }
449                graph.add(newOutgoing);
450                List<Statement> newIncoming = new ArrayList<>();
451                for (Statement stmt : incoming) {
452                    newStatement = (Statement) stmt.clone();
453                    newStatement.setObject(newResource);
454                    if (dateProperties != null) {
455                        newStatement = updateDate(newStatement, newDate, dateProperties);
456                    }
457                    newIncoming.add(newStatement);
458                }
459                graph.add(newIncoming);
460            }
461        }
462        return new IORelationResources(namespaces, relResources.getResourcesMap(), graph.getStatements());
463    }
464}