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