view src/main/java/de/mpiwg/itgroup/annotations/neo4j/AnnotationStore.java @ 81:3be57c18c693

more fixing bugs with transactions thanks to neo4j 2 :-(
author casties
date Wed, 23 Jul 2014 15:09:54 +0200
parents 4e2dc67997a0
children 5a764c625290
line wrap: on
line source

/**
 * 
 */
package de.mpiwg.itgroup.annotations.neo4j;

/*
 * #%L
 * AnnotationManager
 * %%
 * Copyright (C) 2012 - 2014 MPIWG Berlin
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 * Author: Robert Casties (casties@mpiwg-berlin.mpg.de)
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;

import de.mpiwg.itgroup.annotations.Actor;
import de.mpiwg.itgroup.annotations.Annotation;
import de.mpiwg.itgroup.annotations.Annotation.FragmentTypes;
import de.mpiwg.itgroup.annotations.Group;
import de.mpiwg.itgroup.annotations.Person;
import de.mpiwg.itgroup.annotations.Resource;
import de.mpiwg.itgroup.annotations.Tag;
import de.mpiwg.itgroup.annotations.Target;
import de.mpiwg.itgroup.annotations.Uri;

/**
 * Neo4J based Annotation store.
 * 
 * @author casties
 * 
 */
public class AnnotationStore {

    protected static Logger logger = Logger.getLogger("de.mpiwg.itgroup.annotations.neo4j.AnnotationStore");

    protected GraphDatabaseService graphDb;

    public static enum NodeTypes {
        ANNOTATION, PERSON, TARGET, GROUP, TAG, RESOURCE
    }

    // types of nodes that should not be automatically deleted.
    public Set<String> permanentNodeTypes = new HashSet<String>(Arrays.asList("PERSON", "GROUP", "TAG"));

    protected List<Index<Node>> nodeIndexes;

    public static enum RelationTypes implements RelationshipType {
        ANNOTATES, CREATED, PERMITS_ADMIN, PERMITS_DELETE, PERMITS_UPDATE, PERMITS_READ, MEMBER_OF, HAS_TAG, PART_OF
    }

    public static String ANNOTATION_URI_PREFIX = "";

    public AnnotationStore(GraphDatabaseService graphDb) {
        super();
        this.graphDb = graphDb;
        nodeIndexes = new ArrayList<Index<Node>>(5);
        // List.set(enum.ordinal(), val) seems not to work.
        try (Transaction tx = graphDb.beginTx()) {
            nodeIndexes.add(NodeTypes.ANNOTATION.ordinal(), graphDb.index().forNodes("annotations"));
            nodeIndexes.add(NodeTypes.PERSON.ordinal(), graphDb.index().forNodes("persons"));
            nodeIndexes.add(NodeTypes.TARGET.ordinal(), graphDb.index().forNodes("targets"));
            nodeIndexes.add(NodeTypes.GROUP.ordinal(), graphDb.index().forNodes("groups"));
            nodeIndexes.add(NodeTypes.TAG.ordinal(), graphDb.index().forNodes("tags"));
            nodeIndexes.add(NodeTypes.RESOURCE.ordinal(), graphDb.index().forNodes("resources"));
            tx.success();
        }
    }

    protected Index<Node> getNodeIndex(NodeTypes type) {
        return nodeIndexes.get(type.ordinal());
    }

    /**
     * @param userUri
     * @return
     */
    public Node getPersonNodeByUri(String userUri) {
        return getNodeFromIndex("uri", userUri, NodeTypes.PERSON);
    }

    /**
     * @param tagUri
     * @return
     */
    public Node getTagNodeByUri(String tagUri) {
        return getNodeFromIndex("uri", tagUri, NodeTypes.TAG);
    }

    /**
     * @param resourceUri
     * @return
     */
    public Node getResourceNodeByUri(String resourceUri) {
        return getNodeFromIndex("uri", resourceUri, NodeTypes.RESOURCE);
    }

    /**
     * @param targetUri
     * @return
     */
    public Node getTargetNodeByUri(String targetUri) {
        return getNodeFromIndex("uri", targetUri, NodeTypes.RESOURCE);
    }

    /**
     * Returns the Node with the given key and value. Key has to be indexed.
     * 
     * @param key
     * @param value
     * @param type
     * @return
     */
    public Node getNodeFromIndex(String key, String value, NodeTypes type) {
        if (key == null || value == null)
            return null;
        Node node = null;
        try (Transaction tx = graphDb.beginTx()) {
            node = getNodeIndex(type).get(key, value).getSingle();
            tx.success();
        }
        return node;
    }

    /**
     * Returns list of Actors of given type (Group or Person). Key has to be
     * indexed.
     * 
     * @param key
     * @param query
     * @param type
     * @return
     */
    @SuppressWarnings("unchecked")
    protected <T extends Actor> List<T> getActors(String key, String query, NodeTypes type) {
        ArrayList<T> actors = new ArrayList<T>();
        Index<Node> idx = getNodeIndex(type);
        if (key == null) {
            key = "uri";
            query = "*";
        }
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> actorNodes = idx.query(key, query);
            for (Node actorNode : actorNodes) {
                Actor actor = createActorFromNode(actorNode);
                actors.add((T) actor);
            }
            tx.success();            
        }
        return actors;
    }

    /**
     * Returns list of groups. Key has to be indexed.
     * 
     * @param key
     * @param query
     * @return
     */
    public List<Group> getGroups(String key, String query) {
        List<Group> groups = getActors(key, query, NodeTypes.GROUP);
        return groups;
    }

    /**
     * Returns list of Persons. Key has to be indexed.
     * 
     * @param key
     * @param query
     * @return
     */
    public List<Person> getPersons(String key, String query) {
        List<Person> persons = getActors(key, query, NodeTypes.PERSON);
        return persons;
    }

    /**
     * Returns list of uri-like objects of given type (Target or Resource). Key
     * has to be indexed.
     * 
     * @param key
     * @param query
     * @param type
     * @return
     */
    @SuppressWarnings("unchecked")
    protected <T extends Uri> List<T> getUris(String key, String query, NodeTypes type) {
        ArrayList<T> uris = new ArrayList<T>();
        Index<Node> idx = getNodeIndex(type);
        if (key == null) {
            key = "uri";
            query = "*";
        }
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> actorNodes = idx.query(key, query);
            for (Node actorNode : actorNodes) {
                Uri uri = createUriFromNode(actorNode);
                uris.add((T) uri);
            }
            tx.success();
        }
        return uris;
    }

    /**
     * Returns list of Targets. Key has to be indexed.
     * 
     * @param key
     * @param query
     * @return
     */
    public List<Target> getTargets(String key, String query) {
        List<Target> targets = getUris(key, query, NodeTypes.TARGET);
        return targets;
    }

    /**
     * Returns list of Resources. Key has to be indexed.
     * 
     * @param key
     * @param query
     * @return
     */
    public List<Resource> getResources(String key, String query) {
        List<Resource> targets = getUris(key, query, NodeTypes.RESOURCE);
        return targets;
    }

    /**
     * Returns List of Annotations. Key has to be indexed.
     * 
     * @param key
     * @param query
     * @return
     */
    public List<Annotation> getAnnotations(String key, String query) {
        ArrayList<Annotation> annotations = new ArrayList<Annotation>();
        Index<Node> idx = getNodeIndex(NodeTypes.ANNOTATION);
        if (key == null) {
            key = "id";
            query = "*";
        }
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> annotNodes = idx.query(key, query);
            for (Node annotNode : annotNodes) {
                Annotation annotation = createAnnotationFromNode(annotNode);
                annotations.add(annotation);
            }
            tx.success();
        }
        return annotations;
    }

    /**
     * Returns List of Tags. Key has to be indexed.
     * 
     * @param key
     * @param query
     * @return
     */
    public List<Tag> getTags(String key, String query) {
        ArrayList<Tag> tags = new ArrayList<Tag>();
        Index<Node> idx = getNodeIndex(NodeTypes.TAG);
        if (key == null) {
            key = "uri";
            query = "*";
        }
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> groupNodes = idx.query(key, query);
            for (Node groupNode : groupNodes) {
                Tag tag = createTagFromNode(groupNode);
                tags.add(tag);
            }
            tx.success();
        }
        return tags;
    }

    /**
     * Returns List of Groups the person is member of.
     * 
     * @param person
     * @return
     */
    public List<Group> getGroupsForPersonNode(Node person) {
        ArrayList<Group> groups = new ArrayList<Group>();
        try (Transaction tx = graphDb.beginTx()) {
        	Iterable<Relationship> rels = person.getRelationships(RelationTypes.MEMBER_OF);
        	for (Relationship rel : rels) {
        		Node groupNode = rel.getEndNode();
        		Actor group = createActorFromNode(groupNode);
        		// make sure we're getting a group
        		if (!(group instanceof Group)) {
        			logger.severe("target of MEMBER_OF is not GROUP! rel=" + rel);
        			continue;
        		}
        		groups.add((Group) group);
        	}
        	tx.success();
        }
        return groups;
    }

    /**
     * Returns if person with uri is in Group group.
     * 
     * @param person
     * @param group
     * @return
     */
    public boolean isPersonInGroup(Person person, Group group) {
        Node pn = getPersonNodeByUri(person.getUriString());
        if (pn == null)
            return false;
        // optimized version of getGroupsForPersonNode
        try (Transaction tx = graphDb.beginTx()) {
            Iterable<Relationship> rels = pn.getRelationships(RelationTypes.MEMBER_OF);
            for (Relationship rel : rels) {
                Node gn = rel.getEndNode();
                if (gn.getProperty("uri", "").equals(group.getUriString()) 
                        || gn.getProperty("id", "").equals(group.getId())) {
                    tx.success();
                    return true;
                }
            }
            tx.success();
        }
        return false;
    }

    /**
     * Returns the members of the group.
     * 
     * @param group
     * @return
     */
    public List<Person> getMembersOfGroup(Group group) {
        ArrayList<Person> members = new ArrayList<Person>();
        Node gn = getActorNode(group);
        try (Transaction tx = graphDb.beginTx()) {
        	Iterable<Relationship> rels = gn.getRelationships(RelationTypes.MEMBER_OF);
        	for (Relationship rel : rels) {
        		Node memberNode = rel.getStartNode();
        		Actor member = createActorFromNode(memberNode);
        		// make sure we're getting a group
        		if (!(member instanceof Person)) {
        			logger.severe("source of MEMBER_OF is not PERSON! rel=" + rel);
        			continue;
        		}
        		members.add((Person) member);
        	}
        	tx.success();
        }
        return members;
    }

    /**
     * Add Person newMember to Group group.
     * 
     * @param group
     * @param member
     */
    public Person addGroupMember(Group group, Person member) {
        Node gn = getActorNode(group);
        Node pn = getActorNode(member);
        Person addedMember = null;
        if (gn != null && pn != null) {
            getOrCreateRelation(pn, RelationTypes.MEMBER_OF, gn);
            addedMember = member;
        }
        return addedMember;
    }

    /**
     * Delete Person oldMember from Group group.
     * 
     * @param group
     * @param member
     */
    public void deleteGroupMember(Group group, Person member) {
        Node gn = getActorNode(group);
        Node pn = getActorNode(member);
        try (Transaction tx = graphDb.beginTx()) {
        	Iterable<Relationship> rels = gn.getRelationships(RelationTypes.MEMBER_OF);
        	for (Relationship rel : rels) {
        		Node mn = rel.getStartNode();
        		if (mn.equals(pn)) {
                    rel.delete();
                }
                // there should be only one
                break;
            }
            tx.success();
        }
    }

    /**
     * Returns the stored Actor matching the given one.
     * 
     * @param actor
     * @return
     */
    public Actor getActor(Actor actor) {
        Node actorNode = getActorNode(actor);
        Actor storedActor = createActorFromNode(actorNode);
        return storedActor;
    }

    /**
     * Stores an Actor (Person or Group). Creates a new actor Node or update an
     * existing one.
     * 
     * @param actor
     * @return
     */
    public Actor storeActor(Actor actor) {
        Node actorNode = getOrCreateActorNode(actor);
        try (Transaction tx = graphDb.beginTx()) {
            // id
            String id = actor.getId();
            if (id != null) {
                actorNode.setProperty("id", id);
            }
            // name
            String name = actor.getName();
            if (name != null) {
                actorNode.setProperty("name", name);
            }
            // uri
            String uri = actor.getUri();
            if (uri != null) {
                actorNode.setProperty("uri", uri);
            }
            tx.success();
        }
        Actor storedActor = createActorFromNode(actorNode);
        return storedActor;
    }

    /**
     * Deletes the given Actor.
     * 
     * @param actor
     */
    public void deleteActor(Actor actor) {
        String uri = actor.getUriString();
        Index<Node> idx;
        if (actor.isGroup()) {
            idx = getNodeIndex(NodeTypes.GROUP);
        } else {
            idx = getNodeIndex(NodeTypes.PERSON);
        }
        try (Transaction tx = graphDb.beginTx()) {
        	Node actorNode = idx.get("uri", uri).getSingle();
        	if (actorNode != null) {
        		// delete relations
            	for (Relationship rel : actorNode.getRelationships()) {
            		rel.delete();
            	}
            	if (!actorNode.hasRelationship()) {
                    // this shouldn't happen
                    deleteNode(actorNode);
                } else {
                    logger.severe("deleteActor: unable to delete: Node still has relations.");
                }
            }
            tx.success();
        }
    }

    /**
     * Returns the Annotation with the given id.
     * 
     * @param id
     * @return
     */
    public Annotation getAnnotationById(String id) {
        Annotation annot = null;
        try (Transaction tx = graphDb.beginTx()) {
            Node annotNode = getNodeIndex(NodeTypes.ANNOTATION).get("id", id).getSingle();
            annot = createAnnotationFromNode(annotNode);
            tx.success();
        }
        return annot;
    }

    /**
     * Returns an Annotation object from an annotation-Node.
     * 
     * @param annotNode
     * @return
     */
    public Annotation createAnnotationFromNode(Node annotNode) {
        Annotation annot = new Annotation();
        try (Transaction tx = graphDb.beginTx()) {
            annot.setUri((String) annotNode.getProperty("id", null));
            annot.setBodyText((String) annotNode.getProperty("bodyText", null));
            annot.setBodyUri((String) annotNode.getProperty("bodyUri", null));
            annot.setQuote((String) annotNode.getProperty("quote", null));
            /*
             * get annotation target and resource from relation
             */
            for (Relationship rel : annotNode.getRelationships(RelationTypes.ANNOTATES)) {
                Node target = rel.getEndNode();
                String type = (String) target.getProperty("TYPE");
                if (type.equals("TARGET")) {
                    annot.setTarget(new Target((String) target.getProperty("uri", null)));
                } else if (type.equals("RESOURCE")) {
                    annot.setResource(new Resource((String) target.getProperty("uri", null)));
                }
            }
            if (annot.getTarget() == null) {
                logger.warning("annotation " + annotNode + " has no target node!");
            }
            // get fragment from attribute
            annot.setTargetFragment((String) annotNode.getProperty("targetFragment", null));
            String ft = (String) annotNode.getProperty("fragmentType", null);
            if (ft != null) {
                annot.setFragmentType(FragmentTypes.valueOf(ft));
            }
            /*
             * get creator from relation
             */
            Relationship creatorRel = getRelation(annotNode, RelationTypes.CREATED, null);
            if (creatorRel != null) {
                Node creatorNode = creatorRel.getStartNode();
                Actor creator = createActorFromNode(creatorNode);
                annot.setCreator(creator);
            } else {
                logger.warning("annotation " + annotNode + " has no creator node!");
            }
            /*
             * get creation date
             */
            annot.setCreated((String) annotNode.getProperty("created", null));
            /*
             * get permissions
             */
            Relationship adminRel = getRelation(annotNode, RelationTypes.PERMITS_ADMIN, null);
            if (adminRel != null) {
                Node adminNode = adminRel.getEndNode();
                Actor admin = createActorFromNode(adminNode);
                annot.setAdminPermission(admin);
            }
            Relationship deleteRel = getRelation(annotNode, RelationTypes.PERMITS_DELETE, null);
            if (deleteRel != null) {
                Node deleteNode = deleteRel.getEndNode();
                Actor delete = createActorFromNode(deleteNode);
                annot.setDeletePermission(delete);
            }
            Relationship updateRel = getRelation(annotNode, RelationTypes.PERMITS_UPDATE, null);
            if (updateRel != null) {
                Node updateNode = updateRel.getEndNode();
                Actor update = createActorFromNode(updateNode);
                annot.setUpdatePermission(update);
            }
            Relationship readRel = getRelation(annotNode, RelationTypes.PERMITS_READ, null);
            if (readRel != null) {
                Node readNode = readRel.getEndNode();
                Actor read = createActorFromNode(readNode);
                annot.setReadPermission(read);
            }
            /*
             * get tags
             */
            Set<String> tags = new HashSet<String>();
            for (Relationship rel : annotNode.getRelationships(RelationTypes.HAS_TAG)) {
                String tag = (String) rel.getEndNode().getProperty("name", null);
                if (tag != null) {
                    tags.add(tag);
                }
            }
            annot.setTags(tags);

            tx.success();
        }
        return annot;
    }

    /**
     * Returns an Actor object from a node.
     * 
     * @param actorNode
     * @return
     */
    protected Actor createActorFromNode(Node actorNode) {
        if (actorNode == null)
            return null;
        try (Transaction tx = graphDb.beginTx()) {
            String id = (String) actorNode.getProperty("id", null);
            String uri = (String) actorNode.getProperty("uri", null);
            String name = (String) actorNode.getProperty("name", null);
            String type = (String) actorNode.getProperty("TYPE", null);
            tx.success();
            if (type != null && type.equals("PERSON")) {
                return new Person(id, uri, name);
            } else if (type != null && type.equals("GROUP")) {
                return new Group(id, uri, name);
            }
        }
        return null;
    }

    public Tag createTagFromNode(Node tagNode) {
        if (tagNode == null)
            return null;
        String id;
        String uri;
        String name;
        try (Transaction tx = graphDb.beginTx()) {
            name = (String) tagNode.getProperty("name", null);
            uri = (String) tagNode.getProperty("uri", null);
            id = (String) tagNode.getProperty("id", null);
            tx.success();
        }
        return new Tag(id, uri, name);

    }

    /**
     * @param resourceNode
     * @return
     */
    public Resource createResourceFromNode(Node resourceNode) {
        return (Resource) createUriFromNode(resourceNode);
    }

    /**
     * @param targetNode
     * @return
     */
    public Target createTargetFromNode(Node targetNode) {
        return (Target) createUriFromNode(targetNode);
    }

    protected Uri createUriFromNode(Node uriNode) {
        if (uriNode == null)
            return null;
        try (Transaction tx = graphDb.beginTx()) {
            String uri = (String) uriNode.getProperty("uri", null);
            String type = (String) uriNode.getProperty("TYPE", null);
            tx.success();
            if (type != null && type.equals("TARGET")) {
                return new Target(uri);
            } else if (type != null && type.equals("RESOURCE")) {
                return new Resource(uri);
            }
        }
        return null;
    }

    /**
     * Store a new annotation in the store or update an existing one. Returns
     * the stored annotation.
     * 
     * @param annot
     * @return
     */
    public Annotation storeAnnotation(Annotation annot) {
        Node annotNode = null;
        try (Transaction tx = graphDb.beginTx()) {
            /*
             * create or get the annotation
             */
            String id = annot.getUri();
            if (id == null) {
                id = createRessourceURI("annot:");
            }
            annotNode = getOrCreateAnnotationNode(id);

            /*
             * the annotation body
             */
            String bodyText = annot.getBodyText();
            if (bodyText != null) {
                annotNode.setProperty("bodyText", bodyText);
            }
            String bodyUri = annot.getBodyUri();
            if (bodyUri != null) {
                annotNode.setProperty("bodyUri", bodyUri);
            }
            
            /*
             * the annotation quote
             */
            String quote = annot.getQuote();
            if (quote != null) {
                annotNode.setProperty("quote", quote);
            }
                
            /*
             * the annotation target
             */
            Target target = annot.getTarget();
            Node targetNode = null;
            if (target != null) {
                targetNode = getOrCreateUriNode(target.getUri(), NodeTypes.TARGET);
                getOrCreateRelation(annotNode, RelationTypes.ANNOTATES, targetNode);
            }

            /*
             * The fragment part of the annotation target.
             */
            String targetFragment = annot.getTargetFragment();
            FragmentTypes fragmentType = annot.getFragmentType();
            if (targetFragment != null) {
                annotNode.setProperty("targetFragment", targetFragment);
                annotNode.setProperty("fragmentType", fragmentType.name());
            }

            /*
             * the annotation resource
             */
            Resource resource = annot.getResource();
            if (resource != null) {
                Node resourceNode = getOrCreateUriNode(resource.getUri(), NodeTypes.RESOURCE);
                getOrCreateRelation(annotNode, RelationTypes.ANNOTATES, resourceNode);
                getOrCreateRelation(targetNode, RelationTypes.PART_OF, resourceNode);
            }

            /*
             * The creator of this annotation.
             */
            Actor creator = annot.getCreator();
            if (creator != null) {
                Node creatorNode = getOrCreateActorNode(creator);
                getOrCreateRelation(creatorNode, RelationTypes.CREATED, annotNode);
            }

            /*
             * The creation date of this annotation.
             */
            String created = annot.getCreated();
            if (created != null) {
                annotNode.setProperty("created", created);
            }

            /*
             * Permissions for this annotation.
             */
            setPermissionRelation(annotNode, RelationTypes.PERMITS_ADMIN, annot.getAdminPermission());
            setPermissionRelation(annotNode, RelationTypes.PERMITS_DELETE, annot.getDeletePermission());
            setPermissionRelation(annotNode, RelationTypes.PERMITS_UPDATE, annot.getUpdatePermission());
            setPermissionRelation(annotNode, RelationTypes.PERMITS_READ, annot.getReadPermission());

            /*
             * Tags on this annotation.
             */
            Set<String> newTags = annot.getTags();
            // we ignore existing tags if tags == null
            if (newTags != null) {
                List<Relationship> oldHasTags = new ArrayList<Relationship>();
                for (Relationship rel : annotNode.getRelationships(RelationTypes.HAS_TAG)) {
                    oldHasTags.add(rel);
                }
                // adjust to new tags
                if (newTags.isEmpty()) {
                    // remove old tags
                    if (!oldHasTags.isEmpty()) {
                        for (Relationship rel : oldHasTags) {
                            rel.delete();
                            // TODO: should we delete orphan nodes too?
                        }
                    }
                } else {
                    if (!oldHasTags.isEmpty()) {
                        // adjust old tags
                        for (Relationship rel : oldHasTags) {
                            String oldTag = (String) rel.getEndNode().getProperty("name", null);
                            if (newTags.contains(oldTag)) {
                                // tag exists
                                newTags.remove(oldTag);
                            } else {
                                // tag exists no longer
                                rel.delete();
                                // TODO: should we delete orphan nodes too?
                            }
                        }
                    }
                    if (!newTags.isEmpty()) {
                        // still tags to add
                        for (String tag : newTags) {
                            // create new tag
                            Node tagNode = getOrCreateTagNode(new Tag(null, null, tag));
                            getOrCreateRelation(annotNode, RelationTypes.HAS_TAG, tagNode);
                        }
                    }

                }
            }
            tx.success();
        }

        // re-read and return annotation
        Annotation storedAnnot = createAnnotationFromNode(annotNode);
        return storedAnnot;
    }

    /**
     * Deletes the annotation with the given id.
     * 
     * @param id
     */
    public void deleteAnnotationById(String id) {
        try (Transaction tx = graphDb.beginTx()) {
            Node annotNode = getNodeIndex(NodeTypes.ANNOTATION).get("id", id).getSingle();
            if (annotNode != null) {
                // delete related objects
                for (Relationship rel : annotNode.getRelationships()) {
                    // delete relation and the related node if it has no other
                    // relations and is not permanent
                    Node other = rel.getOtherNode(annotNode);
                    rel.delete();
                    if (!(other.hasRelationship() || permanentNodeTypes.contains(other.getProperty("TYPE", null)))) {
                        deleteNode(other);
                    }
                }
                if (!annotNode.hasRelationship()) {
                    deleteNode(annotNode);
                } else {
                    logger.severe("deleteById: unable to delete: Node still has relations.");
                }
            }
            tx.success();
        }
    }

    /**
     * Returns all annotations with the given uri and/or user.
     * 
     * @param uri
     * @param userUri
     * @return
     */
    public List<Annotation> searchAnnotationByUriUser(String targetUri, String userUri) {
        List<Annotation> annotations = new ArrayList<Annotation>();
        if (targetUri != null) {
            // there should be only one
            Node target = getNodeFromIndex("uri", targetUri, NodeTypes.TARGET);
            if (target != null) {
                try (Transaction tx = graphDb.beginTx()) {
                    Iterable<Relationship> relations = target.getRelationships(RelationTypes.ANNOTATES);
                    for (Relationship relation : relations) {
                        Node ann = relation.getStartNode();
                        if (ann.getProperty("TYPE", "").equals("ANNOTATION")) {
                            Annotation annot = createAnnotationFromNode(ann);
                            annotations.add(annot);
                        } else {
                            logger.severe("ANNOTATES relation does not start with ANNOTATION: " + ann);
                        }
                    }
                    tx.success();
                }
            }
        }
        if (userUri != null) {
            // there should be only one
            Node person = getPersonNodeByUri(userUri);
            if (person != null) {
                try (Transaction tx = graphDb.beginTx()) {
                    Iterable<Relationship> relations = person.getRelationships(RelationTypes.CREATED);
                    for (Relationship relation : relations) {
                        Node ann = relation.getEndNode();
                        if (ann.getProperty("TYPE", "").equals("ANNOTATION")) {
                            Annotation annot = createAnnotationFromNode(ann);
                            annotations.add(annot);
                        } else {
                            logger.severe("CREATED relation does not end with ANNOTATION: " + ann);
                        }
                    }
                    tx.success();
                }
            }
        }
        // TODO: if both uri and user are given we should intersect

        return annotations;
    }

    /**
     * Returns Relationship of type from Node start to Node end. Creates one if
     * it doesn't exist.
     * 
     * @param start
     * @param type
     * @param end
     * @return
     */
    protected Relationship getOrCreateRelation(Node start, RelationshipType type, Node end) {
        if (start == null || end == null)
            return null;
        if (start.hasRelationship()) {
            // there are relations
            try (Transaction tx = graphDb.beginTx()) {
                Iterable<Relationship> rels = start.getRelationships(type, Direction.OUTGOING);
                for (Relationship rel : rels) {
                    if (rel.getEndNode().equals(end)) {
                        // relation exists
                        tx.success();
                        return rel;
                    }
                }
                tx.success();
            }
        }
        // create new one
        Relationship rel;
        try (Transaction tx = graphDb.beginTx()) {
            rel = start.createRelationshipTo(end, type);
            tx.success();
        }
        return rel;
    }

    protected Node getOrCreateAnnotationNode(String id) {
        Index<Node> idx = getNodeIndex(NodeTypes.ANNOTATION);
        Node annotation;
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> annotations = idx.get("id", id);
            annotation = annotations.getSingle();
            if (annotation == null) {
                // does not exist yet
                annotation = graphDb.createNode();
                annotation.setProperty("TYPE", NodeTypes.ANNOTATION.name());
                annotation.setProperty("id", id);
                idx.add(annotation, "id", id);
            }
            tx.success();
        }
        return annotation;
    }

    protected Node getOrCreateUriNode(String uri, NodeTypes type) {
        Index<Node> idx = getNodeIndex(type);
        Node target;
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> targets = idx.get("uri", uri);
            target = targets.getSingle();
            if (target == null) {
                // does not exist yet
                target = graphDb.createNode();
                target.setProperty("TYPE", type.name());
                target.setProperty("uri", uri);
                idx.add(target, "uri", uri);
            }
            tx.success();
        }
        return target;
    }

    protected Node getActorNode(Actor actor) {
        // Person/Group is identified by URI or id
        String uri = actor.getUriString();
        Index<Node> idx;
        Node person;
        if (actor.isGroup()) {
            idx = getNodeIndex(NodeTypes.GROUP);
        } else {
            idx = getNodeIndex(NodeTypes.PERSON);
        }
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> persons = idx.get("uri", uri);
            person = persons.getSingle();
            tx.success();
        }
        return person;
    }

    protected Node getOrCreateActorNode(Actor actor) {
        // Person/Group is identified by URI or id
        String uri = actor.getUriString();
        String name = actor.getName();
        String id = actor.getId();
        Node person;
        try (Transaction tx = graphDb.beginTx()) {
            Index<Node> idx;
            if (actor.isGroup()) {
                idx = getNodeIndex(NodeTypes.GROUP);
            } else {
                idx = getNodeIndex(NodeTypes.PERSON);
            }
            IndexHits<Node> persons = idx.get("uri", uri);
            person = persons.getSingle();
            if (person == null) {
                // does not exist yet
                person = graphDb.createNode();
                if (actor.isGroup()) {
                    person.setProperty("TYPE", NodeTypes.GROUP.name());
                } else {
                    person.setProperty("TYPE", NodeTypes.PERSON.name());
                }
                person.setProperty("uri", uri);
                idx.add(person, "uri", uri);
                if (name != null) {
                    person.setProperty("name", name);
                }
                if (id != null) {
                    person.setProperty("id", id);
                }
            }
            tx.success();
        }
        return person;
    }

    protected Node getOrCreateTagNode(Tag inTag) {
        Index<Node> idx = getNodeIndex(NodeTypes.TAG);
        String tagname = inTag.getName();
        Node tag;
        try (Transaction tx = graphDb.beginTx()) {
            IndexHits<Node> tags = idx.get("name", tagname);
            tag = tags.getSingle();
            if (tag == null) {
                // does not exist yet
                tag = graphDb.createNode();
                tag.setProperty("TYPE", NodeTypes.TAG.name());
                tag.setProperty("name", tagname);
                idx.add(tag, "name", tagname);

                tag.setProperty("id", inTag.getId());
                tag.setProperty("uri", inTag.getUri());
                idx.add(tag, "uri", inTag.getUri());
            }
            tx.success();
        }
        return tag;
    }

    /**
     * Create or update permissions relations for an annotation.
     * 
     * @param annotNode
     * @param type
     * @param annot
     */
    protected void setPermissionRelation(Node annotNode, RelationTypes type, Actor actor) {
        Node newActorNode = null;
        if (actor != null) {
            newActorNode = getOrCreateActorNode(actor);
        }
        Relationship rel = getRelation(annotNode, type, null);
        if (rel != null) {
            // relation exists
            try (Transaction tx = graphDb.beginTx()) {
                Node oldActorNode = rel.getEndNode();
                if (!oldActorNode.equals(newActorNode)) {
                    // new admin is different
                    rel.delete();
                    tx.success();
                }
                if (newActorNode != null) {
                    rel = getOrCreateRelation(annotNode, type, newActorNode);
                }
                tx.success();
            }
        } else {
            // no relation yet
            if (newActorNode != null) {
                rel = getOrCreateRelation(annotNode, type, newActorNode);
            }
        }
    }

    /**
     * Unindexes and deletes given Node if it has no relations.
     * 
     * @param node
     */
    protected void deleteNode(Node node) {
        try (Transaction tx = graphDb.beginTx()) {
            if (node.hasRelationship()) {
                logger.severe("deleteNode: unable to delete: Node still has relations.");
            } else {
                String ts = (String) node.getProperty("TYPE", null);
                try {
                    NodeTypes type = NodeTypes.valueOf(ts);
                    getNodeIndex(type).remove(node);
                } catch (Exception e) {
                    logger.severe("deleteNode: unable to get TYPE of node: " + node);
                }
                node.delete();
            }
            tx.success();
        }
    }

    /**
     * returns the (first) Relationship of RelationTypes type from Node start.
     * 
     * @param start
     * @param type
     * @param direction
     * @return
     */
    protected Relationship getRelation(Node start, RelationTypes type, Direction direction) {
        Iterable<Relationship> rels;
        try (Transaction tx = graphDb.beginTx()) {
            if (direction == null) {
                // ignore direction
                rels = start.getRelationships(type);
            } else {
                rels = start.getRelationships(type, direction);
            }
            tx.success();
            for (Relationship rel : rels) {
                // just the first one
                return rel;
            }
        }
        return null;
    }

    /**
     * Erzeuge eine urn aus der aktuellen Zeit in millis
     * 
     * @return
     */
    private String createRessourceURI(String prefix) {
        Calendar cal = Calendar.getInstance();
        long time = cal.getTimeInMillis();
        return String.format("%s%s%s", ANNOTATION_URI_PREFIX, prefix, time);
    }

    public List<Annotation> getAnnotationsByTag(String tagUri) {
        ArrayList<Annotation> ret = new ArrayList<Annotation>();
        Node tag = getTagNodeByUri(tagUri);
        if (tag != null) {
            try (Transaction tx = graphDb.beginTx()) {
                Iterable<Relationship> rels = tag.getRelationships(Direction.INCOMING, RelationTypes.HAS_TAG);
                for (Relationship rel : rels) {
                    Node node = rel.getStartNode();
                    ret.add(createAnnotationFromNode(node));
                }
                tx.success();
            }
        }
        return ret;
    }

    public List<Annotation> getAnnotationsByResource(String resourceUri) {
        ArrayList<Annotation> ret = new ArrayList<Annotation>();
        Node res = getNodeFromIndex("uri", resourceUri, NodeTypes.RESOURCE);
        if (res != null) {
            try (Transaction tx = graphDb.beginTx()) {
                Iterable<Relationship> rels = res.getRelationships(Direction.INCOMING, RelationTypes.ANNOTATES);
                for (Relationship rel : rels) {
                    Node an = rel.getStartNode();
                    Node rn = rel.getEndNode();
                    if (rn.getProperty("TYPE", "").equals("RESOURCE")) {
                        logger.severe("getAnnotationsByResource got ANNOTATES != RESOURCE");
                    }
                    ret.add(createAnnotationFromNode(an));
                }
                tx.success();
            }
        }
        return ret;
    }

}