What is the most efficient way to follow a chain of document relations?


I am considering a repository structure where there are two document types: let's call one “Base” and the other “Enhancements”. I would like to use the Relations functionality of Nuxeo to indicated that a given “Enhancements” requires a given “Base” using the standard Dublin Core relation included with the software.

This is working fine, and I am able to find the Base documents from the Enhancements documents that require them programmatically using the Relations.GetRelations operation. However, this only traverses one edge of the given relation. Is there already a way in Nuxeo to retrieve both documents “EnhancementsB” and “Base” in the following dependency chain, if I am querying the “EnhancementsA” document?

EnhancementsA ---requires--> EnhancementsB ---requires--> Base

Obviously I could take the output I have and query the document it returns, but my goal is to reduce the number of serial web requests needed to acquire this information, since the ultimate request is coming from a mobile client. I could also define a server-side operation to go through a fixed number of such relation edges, but maybe there is a better way?

Thanks for any help.

1 votes

2 answers



I should clarify that I don't need the file content for all documents in the single response, but only the Document entries as they appear in the response to Relations.GetRelations.

I ended up implementing the following solution. It appears to work for my use case.

package com.example;

import org.apache.commons.lang.StringUtils;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.annotations.Context;
import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.automation.core.annotations.Param;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.platform.relations.api.Graph;
import org.nuxeo.ecm.platform.relations.api.Node;
import org.nuxeo.ecm.platform.relations.api.QNameResource;
import org.nuxeo.ecm.platform.relations.api.RelationManager;
import org.nuxeo.ecm.platform.relations.api.Resource;
import org.nuxeo.ecm.platform.relations.api.ResourceAdapter;
import org.nuxeo.ecm.platform.relations.api.Statement;
import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl;
import org.nuxeo.ecm.platform.relations.api.util.RelationConstants;

import java.util.Collections;
import java.util.List;
import java.util.Map;

@Operation(id = GetAllLinkedDocuments.ID, category = Constants.CAT_SERVICES, label = "Get All Linked Documents", description = "Get all documents related to the input document.  Includes the input document and any linked documents found via recursive relation search.")
public class GetAllLinkedDocuments {
    public static final String ID = "Example.Document.GetAllLinkedDocuments";

    protected CoreSession session;

    protected RelationManager relations;

    @Param(name = "predicate")
    protected String predicate;

    @Param(name = "graphName", required = false)
    protected String graphName;

    public DocumentModelList run(DocumentModel doc) throws Exception {
        Resource pred = new ResourceImpl(predicate);
        Graph graph = relations.getGraphByName(getGraphName());
        DocumentModelList result = new DocumentModelListImpl();
        depthFirstSearch(graph, pred, doc, result);
        return result;

    private void depthFirstSearch(Graph graph, Resource pred,
                                  DocumentModel doc,
                                  DocumentModelList found) {
        for (DocumentModel relatedDoc : getRelatedDocs(graph, pred, doc)) {
            if (!found.contains(relatedDoc)) {
                depthFirstSearch(graph, pred, relatedDoc, found);

    private DocumentModelList getRelatedDocs(Graph graph, Resource pred,
                                             DocumentModel doc) {
        QNameResource resource = (QNameResource) relations.getResource(
                RelationConstants.DOCUMENT_NAMESPACE, doc, null);
        List<Statement> statements = graph.getStatements(resource, pred, null);
        DocumentModelList docs = new DocumentModelListImpl(statements.size());
        for (Statement st : statements) {
            DocumentModel dm = getDocumentModel(st.getObject());
            if (dm != null) {
        return docs;

    // copied from org.nuxeo.ecm.automation.core.operations.services.GetRelations
    protected DocumentModel getDocumentModel(Node node) throws
            ClientException {
        if (node.isQNameResource()) {
            QNameResource resource = (QNameResource) node;
            Map<String, Object> context = Collections.<String, Object> singletonMap(
                    ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session);
            Object o = relations.getResourceRepresentation(
                    resource.getNamespace(), resource, context);
            if (o instanceof DocumentModel) {
                return (DocumentModel) o;
        return null;

    // copied from org.nuxeo.ecm.automation.core.operations.services.GetRelation
    public String getGraphName() {
        if (StringUtils.isEmpty(graphName)) {
            return RelationConstants.GRAPH_NAME;
        return graphName;

0 votes

Instead of creating getRelatedDocs and copying the methods getDocumentModel and getGraphName, why not use the corresponding operation (Relations.GetRelations) ? Is there any performance issue?

For example, something like this (not tested):

private void depthFirstSearch(Graph graph, Resource pred,
                              DocumentModel doc,
                              DocumentModelList found) {

    OperationContext ctx = new OperationContext(session);

    Map<String, Object> params = new HashMap<String, Object>();
    params.put("predicate", "http://purl.org/dc/terms/References");
    params.put("outgoing", true);

    AutomationService service = Framework.getService(AutomationService.class);

    for (DocumentModel relatedDoc : (DocumentModelList)service.run(ctx, "Relations.GetRelations", params)) {
        if (!found.contains(relatedDoc)) {
            depthFirstSearch(graph, pred, relatedDoc, found);
1 votes

That looks like it would work in principle. I did notice that they were renaming the GetRelations/CreateRelation operation in the fasttrack release though. :)

Maybe some Nuxeo expert can weigh in on whether the above has significant overhead.