Tiry and I started playing a bit with arbor.js. It's a graph visualization library based on web workers and jQuery. Our goal is to be able to navigate in Nuxeo documents using their relations. I'm talking about any kind of relation. It could be a tag, the parent or the children of a document or classic RDF relations. As picture is better than a thousand words, here's a screenshot:

Nuxeo graph navigation

So what I currently have is a simple WebEngine module handling enough REST URLs so that I can easily get JSON objects representing document relations, tags, parent and children. This means I have all I need to create links between nodes of my graph.

[java]
@Path("/relationBrowser")
@Produces("text/html;charset=UTF-8")
@WebObject(type = "MyRoot")
public class RelationBrowser extends ModuleRoot {

@GET
public Object doGet() {
    return getView("index");
}

@GET
@Path("browse/id/{id}")
@Produces("text/html;charset=UTF-8")
public Object getIdBrowser(@PathParam("id") String id) throws Exception {
    return getView("index").arg("id", id);
}

@GET
@Path("browse/predicate/{predicate}")
@Produces("text/html;charset=UTF-8")
public Object getPredicateBrowser(@PathParam("predicate") String predicate)
        throws Exception {
    return getView("index").arg("predicate", predicate);
}

@GET
@Path("relation/predicate/{predicate}")
@Produces("application/json")
public Object getPredicateSatements(@PathParam("predicate") String predicate)
        throws Exception {
    RelationManager relationManager = Framework
            .getService(RelationManager.class);
    Graph graph = relationManager
            .getGraphByName(RelationConstants.GRAPH_NAME);
    Resource predicateNode = new ResourceImpl(predicate);

    List<Statement> stmts = graph.getStatements(null, predicateNode, null);
    JSONArray statementArray = new JSONArray();
    for (Statement statement : stmts) {
        DocumentModel subjectDoc = getDocumentModel(statement.getSubject(),
                relationManager);
        DocumentModel objectDoc = getDocumentModel(statement.getObject(),
                relationManager);
        String predicateURI = statement.getPredicate().getUri();

        JSONObject statementJSObject = new JSONObject();
        JSONObject subjectJSObject = createDocumentNode(
                subjectDoc.getTitle(), subjectDoc.getId(), "blue");
        JSONObject objectJSObject = createDocumentNode(
                objectDoc.getTitle(), objectDoc.getId(), "blue");
        JSONObject predicateJSObject = createPredicateNode(predicateURI,
                predicateURI);
        statementJSObject.put("subject", subjectJSObject);
        statementJSObject.put("object", objectJSObject);
        statementJSObject.put("predicate", predicateJSObject);
        statementArray.put(statementJSObject);
    }
    return statementArray.toString();
}

@GET
@Path("relation/uuid/{uuid}")
@Produces("application/json")
public Object getDocRelations(@PathParam("uuid") String uuid)
        throws Exception {
    RelationManager relationManager = Framework
            .getService(RelationManager.class);
    Graph graph = relationManager
            .getGraphByName(RelationConstants.GRAPH_NAME);
    CoreSession session = ctx.getCoreSession();
    DocumentRef idRef = new IdRef(uuid);
    DocumentModel subjectDoc = session.getDocument(idRef);
    Node subject = RelationHelper.getDocumentResource(subjectDoc);
    JSONObject subjectJSObject = createDocumentNode(subjectDoc.getTitle(),
            subjectDoc.getId(), "blue");
    List<Statement> stmts = graph.getStatements(subject, null, null);
    JSONArray statementArray = new JSONArray();
    for (Statement statement : stmts) {
        DocumentModel objectDoc = getDocumentModel(statement.getObject(),
                relationManager);
        String predicateURI = statement.getPredicate().getUri();
        JSONObject statementJSObject = new JSONObject();
        JSONObject objectJSObject = createDocumentNode(
                objectDoc.getTitle(), objectDoc.getId(), "blue");
        JSONObject predicateJSObject = createPredicateNode(predicateURI,
                predicateURI);
        statementJSObject.put("subject", subjectJSObject);
        statementJSObject.put("object", objectJSObject);
        statementJSObject.put("predicate", predicateJSObject);
        statementArray.put(statementJSObject);
    }
    return statementArray.toString();
}

@GET
@Path("children/{uuid}")
@Produces("application/json")
public Object getChildren(@PathParam("uuid") String uuid) throws Exception {
    CoreSession session = ctx.getCoreSession();
    DocumentRef idRef = new IdRef(uuid);
    DocumentModelList children = session.getChildren(idRef);
    DocumentModel parentDoc = session.getDocument(idRef);
    JSONObject subjectJSObject = createDocumentNode(parentDoc.getTitle(),
            parentDoc.getId(), "blue");
    JSONArray statementArray = new JSONArray();
    for (DocumentModel child : children) {
        JSONObject statementJSObject = new JSONObject();
        JSONObject objectJSObject = createDocumentNode(child.getTitle(),
                child.getId(), "blue");
        JSONObject predicateJSObject = createPredicateNode("parentOf",
                "parentOf");
        statementJSObject.put("subject", subjectJSObject);
        statementJSObject.put("object", objectJSObject);
        statementJSObject.put("predicate", predicateJSObject);
        statementArray.put(statementJSObject);
    }
    return statementArray.toString();
}

@GET
@Path("parent/{uuid}")
@Produces("application/json")
public Object getParent(@PathParam("uuid") String uuid) throws Exception {
    CoreSession session = ctx.getCoreSession();
    DocumentRef idRef = new IdRef(uuid);
    DocumentModel doc = session.getDocument(idRef);
    DocumentModel parent = session.getParentDocument(idRef);
    JSONArray statementArray = new JSONArray();
    JSONObject statementJSObject = new JSONObject();
    JSONObject subjectJSObject = createDocumentNode(doc.getTitle(), uuid,
            "blue");
    JSONObject objectJSObject = createDocumentNode(parent.getTitle(),
            parent.getId(), "blue");
    JSONObject predicateJSObject = createPredicateNode("childOf", "childOf");
    statementJSObject.put("subject", subjectJSObject);
    statementJSObject.put("object", objectJSObject);
    statementJSObject.put("predicate", predicateJSObject);
    statementArray.put(statementJSObject);
    return statementArray.toString();
}

@GET
@Path("tag/list/{uuid}")
@Produces("application/json")
public Object getDocTagList(@PathParam("uuid") String uuid)
        throws Exception {
    CoreSession session = ctx.getCoreSession();
    DocumentRef idRef = new IdRef(uuid);
    DocumentModel doc = session.getDocument(idRef);
    List<Tag> tags = getDocumentTags(doc);
    JSONObject subjectJSObject = createDocumentNode(doc.getTitle(),
            doc.getId(), "blue");
    JSONArray statementArray = new JSONArray();
    for (Tag tag : tags) {
        JSONObject statementJSObject = new JSONObject();
        JSONObject predicateJSObject = createTagNode(tag.getLabel(),
                tag.getWeight());
        statementJSObject.put("subject", subjectJSObject);
        statementJSObject.put("predicate", predicateJSObject);
        statementArray.put(statementJSObject);
    }
    return statementArray.toString();
}

@GET
@Path("tag/documents/{tagLabel}")
@Produces("application/json")
public Object getTagDocuments(@PathParam("tagLabel") String tagLabel)
        throws Exception {
    List<String> documentIds = getTagDocumentIds(tagLabel);
    if (documentIds != null && !documentIds.isEmpty()) {
        JSONObject predicateJSObject = createTagNode(tagLabel, 1);
        JSONArray statementArray = new JSONArray();
        for (String id : documentIds) {
            JSONObject statementJSObject = new JSONObject();
            CoreSession session = ctx.getCoreSession();
            DocumentRef idRef = new IdRef(id);
            DocumentModel object = session.getDocument(idRef);
            JSONObject objectJSObject = createDocumentNode(
                    object.getTitle(), object.getId(), "blue");
            statementJSObject.put("predicate", predicateJSObject);
            statementJSObject.put("object", objectJSObject);
            statementArray.put(statementJSObject);
        }
        return statementArray.toString();
    }
    return null;
}

@GET
@Path("choices/{uuid}")
@Produces("application/json")
public Object getChoices(@PathParam("uuid") String uuid) throws Exception {
    CoreSession session = ctx.getCoreSession();
    DocumentRef idRef = new IdRef(uuid);
    DocumentModel doc = session.getDocument(idRef);
    JSONObject subjectJSObject = createDocumentNode(doc.getTitle(),
            doc.getId(), "blue");
    JSONArray statementArray = new JSONArray();

    // add Parent Navigation
    JSONObject statementJSObject = new JSONObject();
    JSONObject predicateJSObject = createPredicateNode("childOf",
            doc.getId());
    predicateJSObject.put("relation", "childOf");
    statementJSObject.put("subject", subjectJSObject);
    statementJSObject.put("predicate", predicateJSObject);
    statementArray.put(statementJSObject);

    // add Children Navigation
    statementJSObject = new JSONObject();
    predicateJSObject = createPredicateNode("parentOf", doc.getId());
    predicateJSObject.put("relation", "parentOf");
    statementJSObject.put("subject", subjectJSObject);
    statementJSObject.put("predicate", predicateJSObject);
    statementArray.put(statementJSObject);

    // add tags Navigation
    statementJSObject = new JSONObject();
    predicateJSObject = createPredicateNode("hasTags", doc.getId());
    predicateJSObject.put("relation", "hasTags");
    statementJSObject.put("subject", subjectJSObject);
    statementJSObject.put("predicate", predicateJSObject);
    statementArray.put(statementJSObject);

    // add Relations Navigation
    statementJSObject = new JSONObject();
    predicateJSObject = createPredicateNode("hasRelations", doc.getId());
    predicateJSObject.put("relation", "hasRelations");
    statementJSObject.put("subject", subjectJSObject);
    statementJSObject.put("predicate", predicateJSObject);
    statementArray.put(statementJSObject);

    return statementArray.toString();
}

public List<String> getTagDocumentIds(String tagLabel)
        throws ClientException {
    List<String> documentIds = getTagService().getTagDocumentIds(
            ctx.getCoreSession(), tagLabel, null);
    return documentIds;
}

public List<Tag> getDocumentTags(DocumentModel document)
        throws ClientException {
    String docId = getDocIdForTag(document);
    List<Tag> tags = getTagService().getDocumentTags(ctx.getCoreSession(),
            docId, null);
    Collections.sort(tags, Tag.LABEL_COMPARATOR);
    return tags;
}

public static String getDocIdForTag(DocumentModel doc) {
    return doc.isProxy() ? doc.getSourceId() : doc.getId();
}

protected TagService getTagService() throws ClientException {
    TagService tagService;
    try {
        tagService = Framework.getService(TagService.class);
    } catch (Exception e) {
        throw new ClientException(e);
    }
    if (tagService == null) {
        return null;
    }
    return tagService.isEnabled() ? tagService : null;
}

protected DocumentModel getDocumentModel(Node node,
        RelationManager relationManager) throws ClientException {
    if (node.isQNameResource()) {
        QNameResource resource = (QNameResource) node;
        Map<String, Serializable> context = new HashMap<String, Serializable>();
        context.put(ResourceAdapter.CORE_SESSION_ID_CONTEXT_KEY, ctx
                .getCoreSession().getSessionId());
        Object o = relationManager.getResourceRepresentation(
                resource.getNamespace(), resource, context);
        if (o instanceof DocumentModel) {
            return (DocumentModel) o;
        }
    }
    return null;
}

private JSONObject createJsonNode(int mass, String color, String label,
        String type, int size, String uuid) {
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("mass", mass);
    map.put("color", color);
    map.put("label", label);
    map.put("type", type);
    map.put("size", size);
    map.put("uuid", uuid);
    JSONObject jso = new JSONObject(map);
    return jso;
}

private JSONObject createDocumentNode(String label, String uuid,
        String color) throws JSONException {
    JSONObject docNode = createJsonNode(50, color, label, "doc", 70, uuid);
    docNode.put("version", false);
    docNode.put("open", false);
    return docNode;
}

private JSONObject createPredicateNode(String label, String uuid)
        throws JSONException {
    JSONObject predicateNode = createJsonNode(1, "grey", label, "virtual",
            48, uuid);
    predicateNode.put("open", false);
    return predicateNode;
}

private JSONObject createTagNode(String label, long weight)
        throws JSONException {
    JSONObject predicateNode = createJsonNode(1, "grey", label, "virtual",
            48, label);
    predicateNode.put("relation", "tag");
    predicateNode.put("open", false);
    // predicateNode.put("size", 48 * weight);
    return predicateNode;
}

private JSONObject createDeadEnd() throws JSONException {
    JSONObject predicateNode = createJsonNode(1, "black", "Dead End",
            "virtual", 48, "Dead End");
    return predicateNode;
}

[/java]

I think I'll have to refactor this heavily :-) There's some duplicated code between this module and the Seam beans handling relations and tags. Hopefully we won't have that kind of issue after moving to CDI.

There are still many things I would like to do to improve this. The first thing that comes to mind is pruning. Having too many nodes makes the graph really hard to read. I would also like to have a resume of the last selected document on the right part of the screen. And I think I'll need some help and advice from Lise since this is quite rough/ugly right now.

As you can see this is still in early development so I'm not going to talk too much about this today. But be sure there will be more next week :) You can already checkout the source code on Github.

See ya' Friday