org.structr.core.entity.AbstractNode.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.core.entity.AbstractNode.java

Source

/**
 * Copyright (C) 2010-2016 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr 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 Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.core.entity;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.chemistry.opencmis.commons.data.Ace;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.PropertyType;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Result;
import org.neo4j.graphdb.index.Index;
import org.neo4j.helpers.collection.LruMap;
import org.structr.cmis.CMISInfo;
import org.structr.cmis.common.CMISExtensionsData;
import org.structr.cmis.common.StructrItemActions;
import org.structr.cmis.info.CMISDocumentInfo;
import org.structr.cmis.info.CMISFolderInfo;
import org.structr.cmis.info.CMISItemInfo;
import org.structr.cmis.info.CMISPolicyInfo;
import org.structr.cmis.info.CMISRelationshipInfo;
import org.structr.cmis.info.CMISSecondaryInfo;
import org.structr.common.AccessControllable;
import org.structr.common.AccessPathCache;
import org.structr.common.GraphObjectComparator;
import org.structr.common.IdSorter;
import org.structr.common.Permission;
import org.structr.common.PermissionPropagation;
import org.structr.common.PermissionResolutionMask;
import org.structr.common.PropertyView;
import org.structr.common.SecurityContext;
import org.structr.common.ValidationHelper;
import org.structr.common.View;
import org.structr.common.error.ErrorBuffer;
import org.structr.common.error.FrameworkException;
import org.structr.common.error.NullArgumentToken;
import org.structr.common.error.ReadOnlyPropertyToken;
import org.structr.core.GraphObject;
import org.structr.core.IterableAdapter;
import org.structr.core.entity.relationship.Ownership;
import org.structr.core.Services;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.converter.PropertyConverter;
import org.structr.core.entity.relationship.PrincipalOwnsNode;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.NodeRelationshipStatisticsCommand;
import org.structr.core.graph.NodeService;
import org.structr.core.graph.RelationshipFactory;
import org.structr.core.graph.RelationshipInterface;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.PropertyMap;
import org.structr.core.script.Scripting;
import org.structr.schema.action.ActionContext;
import org.structr.schema.action.Function;

//~--- classes ----------------------------------------------------------------
/**
 * Abstract base class for all node entities in structr.
 *
 *
 *
 */
public abstract class AbstractNode implements NodeInterface, AccessControllable, CMISInfo, CMISItemInfo {

    private static final Map<String, Object> relationshipTemplateInstanceCache = new LruMap<>(1000);
    private static final Logger logger = Logger.getLogger(AbstractNode.class.getName());

    public static final View defaultView = new View(AbstractNode.class, PropertyView.Public, id, type);

    public static final View uiView = new View(AbstractNode.class, PropertyView.Ui, id, name, owner, type,
            createdBy, deleted, hidden, createdDate, lastModifiedDate, visibleToPublicUsers,
            visibleToAuthenticatedUsers, visibilityStartDate, visibilityEndDate);

    private PermissionResolutionMask permissionResolutionMask = null;
    private Relationship rawPathSegment = null;
    private boolean readOnlyPropertiesUnlocked = false;
    private boolean isCreation = false;

    protected SecurityContext securityContext = null;
    protected Principal cachedOwnerNode = null;
    protected String cachedUuid = null;
    protected Class entityType = null;
    protected Node dbNode = null;

    //~--- constructors ---------------------------------------------------
    public AbstractNode() {
    }

    public AbstractNode(SecurityContext securityContext, final Node dbNode, final Class entityType) {
        init(securityContext, dbNode, entityType, false);
    }

    //~--- methods --------------------------------------------------------
    @Override
    public void onNodeCreation() {
    }

    @Override
    public void onNodeInstantiation() {
    }

    @Override
    public void onNodeDeletion() {
    }

    @Override
    public final void init(final SecurityContext securityContext, final Node dbNode, final Class entityType,
            final boolean isCreation) {

        this.isCreation = isCreation;
        this.dbNode = dbNode;
        this.entityType = entityType;
        this.securityContext = securityContext;
    }

    @Override
    public void setSecurityContext(SecurityContext securityContext) {
        this.securityContext = securityContext;
    }

    @Override
    public SecurityContext getSecurityContext() {
        return securityContext;
    }

    @Override
    public boolean equals(final Object o) {

        if (o == null) {

            return false;
        }

        if (!(o instanceof AbstractNode)) {

            return false;
        }

        return (Integer.valueOf(this.hashCode()).equals(o.hashCode()));

    }

    @Override
    public int hashCode() {

        if (this.dbNode == null) {

            return (super.hashCode());
        }

        return Long.valueOf(dbNode.getId()).hashCode();

    }

    @Override
    public int compareTo(final Object other) {

        if (other instanceof AbstractNode) {

            final AbstractNode node = (AbstractNode) other;
            if (node == null) {
                return -1;
            }

            String name = getName();

            if (name == null) {
                return -1;
            }

            String nodeName = node.getName();

            if (nodeName == null) {
                return -1;
            }

            return name.compareTo(nodeName);
        }

        if (other instanceof String) {

            return getUuid().compareTo((String) other);

        }

        if (other == null) {
            throw new NullPointerException();
        }

        throw new IllegalStateException("Cannot compare " + this + " to " + other);
    }

    /**
     * Implement standard toString() method
     */
    @Override
    public String toString() {
        return getUuid();

    }

    /**
     * Can be used to permit the setting of a read-only property once. The
     * lock will be restored automatically after the next setProperty
     * operation. This method exists to prevent automatic set methods from
     * setting a read-only property while allowing a manual set method to
     * override this default behaviour.
     */
    @Override
    public void unlockReadOnlyPropertiesOnce() {

        this.readOnlyPropertiesUnlocked = true;

    }

    @Override
    public void removeProperty(final PropertyKey key) throws FrameworkException {

        if (!isGranted(Permission.write, securityContext)) {
            throw new FrameworkException(403, "Modification not permitted.");
        }

        if (this.dbNode != null) {

            if (key == null) {

                logger.log(Level.SEVERE, "Tried to set property with null key (action was denied)");

                return;

            }

            // check for read-only properties
            if (key.isReadOnly()) {

                // allow super user to set read-only properties
                if (readOnlyPropertiesUnlocked || securityContext.isSuperUser()) {

                    // permit write operation once and
                    // lock read-only properties again
                    readOnlyPropertiesUnlocked = false;
                } else {

                    throw new FrameworkException(404, new ReadOnlyPropertyToken(getType(), key));
                }

            }

            dbNode.removeProperty(key.dbName());

            // remove from index
            removeFromIndex(key);
        }

    }

    //~--- get methods ----------------------------------------------------
    @Override
    public PropertyKey getDefaultSortKey() {
        return AbstractNode.name;
    }

    @Override
    public String getDefaultSortOrder() {
        return GraphObjectComparator.ASCENDING;
    }

    @Override
    public String getType() {
        return getProperty(AbstractNode.type);
    }

    @Override
    public PropertyContainer getPropertyContainer() {
        return dbNode;
    }

    /**
     * Get name from underlying db node
     *
     * If name is null, return node id as fallback
     */
    @Override
    public String getName() {

        String name = getProperty(AbstractNode.name);
        if (name == null) {
            name = getNodeId().toString();
        }

        return name;
    }

    /**
     * Get id from underlying db
     */
    @Override
    public long getId() {

        if (dbNode == null) {

            return -1;
        }

        return dbNode.getId();

    }

    @Override
    public String getUuid() {

        if (cachedUuid == null) {
            cachedUuid = getProperty(GraphObject.id);
        }

        return cachedUuid;

    }

    public Long getNodeId() {
        return getId();

    }

    public String getIdString() {
        return Long.toString(getId());
    }

    /**
     * Indicates whether this node is visible to public users.
     *
     * @return whether this node is visible to public users
     */
    public boolean getVisibleToPublicUsers() {
        return getProperty(visibleToPublicUsers);
    }

    /**
     * Indicates whether this node is visible to authenticated users.
     *
     * @return whether this node is visible to authenticated users
     */
    public boolean getVisibleToAuthenticatedUsers() {
        return getProperty(visibleToPublicUsers);
    }

    /**
     * Indicates whether this node is hidden.
     *
     * @return whether this node is hidden
     */
    public boolean getHidden() {
        return getProperty(hidden);
    }

    /**
     * Indicates whether this node is deleted.
     *
     * @return whether this node is deleted
     */
    public boolean getDeleted() {
        return getProperty(deleted);
    }

    /**
     * Returns the property set for the given view as an Iterable.
     *
     * @param propertyView
     * @return the property set for the given view
     */
    @Override
    public Iterable<PropertyKey> getPropertyKeys(final String propertyView) {

        // check for custom view in content-type field
        if (securityContext != null && securityContext.hasCustomView()) {

            final Set<PropertyKey> keys = new LinkedHashSet<>(
                    StructrApp.getConfiguration().getPropertySet(entityType, propertyView));
            final Set<String> customView = securityContext.getCustomView();

            for (Iterator<PropertyKey> it = keys.iterator(); it.hasNext();) {
                if (!customView.contains(it.next().jsonName())) {

                    it.remove();
                }
            }

            return keys;
        }

        // this is the default if no application/json; properties=[...] content-type header is present on the request
        return StructrApp.getConfiguration().getPropertySet(entityType, propertyView);
    }

    /**
     * Return property value which is used for indexing.
     *
     * This is useful f.e. to filter markup from HTML to index only text, or
     * to get dates as long values.
     *
     * @param key
     * @return property value for indexing
     */
    @Override
    public Object getPropertyForIndexing(final PropertyKey key) {

        Object value = getProperty(key, false, null);
        if (value != null) {
            return value;
        }

        return getProperty(key);
    }

    /**
     * Returns the (converted, validated, transformed, etc.) property for
     * the given property key.
     *
     * @param <T>
     * @param key the property key to retrieve the value for
     * @return the converted, validated, transformed property value
     */
    @Override
    public <T> T getProperty(final PropertyKey<T> key) {
        return getProperty(key, null);
    }

    @Override
    public <T> T getProperty(final PropertyKey<T> key, final org.neo4j.helpers.Predicate<GraphObject> predicate) {
        return getProperty(key, true, predicate);
    }

    private <T> T getProperty(final PropertyKey<T> key, boolean applyConverter,
            final org.neo4j.helpers.Predicate<GraphObject> predicate) {

        // early null check, this should not happen...
        if (key == null || key.dbName() == null) {
            return null;
        }

        /**
         * check read access:
         * - permission resolution MUST already be done here because otherwise we won't be able to access the node
         * - securityContext should contain the masked permissions
         * - check property name against masked permissions
         */
        if (permissionResolutionMask != null) {

            if (!permissionResolutionMask.allowsPermission(Permission.read)) {
                return null;
            }

            if (!permissionResolutionMask.allowsProperty(key)) {
                return null;
            }
        }

        return key.getProperty(securityContext, this, applyConverter, predicate);
    }

    public String getPropertyMD5(final PropertyKey key) {

        Object value = getProperty(key);

        if (value instanceof String) {

            return DigestUtils.md5Hex((String) value);
        } else if (value instanceof byte[]) {

            return DigestUtils.md5Hex((byte[]) value);
        }

        logger.log(Level.WARNING, "Could not create MD5 hex out of value {0}", value);

        return null;

    }

    /**
     * Returns the property value for the given key as a Comparable
     *
     * @param key the property key to retrieve the value for
     * @return the property value for the given key as a Comparable
     */
    @Override
    public <T> Comparable getComparableProperty(final PropertyKey<T> key) {

        if (key != null) {

            final T propertyValue = getProperty(key);

            // check property converter
            PropertyConverter<T, ?> converter = key.databaseConverter(securityContext, this);
            if (converter != null) {

                try {
                    return converter.convertForSorting(propertyValue);

                } catch (Throwable t) {

                    t.printStackTrace();

                    logger.log(Level.WARNING, "Unable to convert property {0} of type {1}: {2}",
                            new Object[] { key.dbName(), getClass().getSimpleName(), t.getMessage() });
                }
            }

            // conversion failed, may the property value itself is comparable
            if (propertyValue instanceof Comparable) {
                return (Comparable) propertyValue;
            }

            // last try: convertFromInput to String to make comparable
            if (propertyValue != null) {
                return propertyValue.toString();
            }
        }

        return null;
    }

    /**
     * Returns the property value for the given key as a Iterable
     *
     * @param propertyKey the property key to retrieve the value for
     * @return the property value for the given key as a Iterable
     */
    public Iterable getIterableProperty(final PropertyKey<? extends Iterable> propertyKey) {
        return (Iterable) getProperty(propertyKey);
    }

    /**
     * Returns a list of related nodes for which a modification propagation
     * is configured via the relationship. Override this method to return a
     * set of nodes that should receive propagated modifications.
     *
     * @return a set of nodes to which modifications should be propagated
     */
    public Set<AbstractNode> getNodesForModificationPropagation() {
        return null;
    }

    /**
     * Returns database node.
     *
     * @return the database node
     */
    @Override
    public Node getNode() {

        return dbNode;

    }

    protected <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> Iterable<R> getIncomingRelationshipsAsSuperUser(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance());
        final R template = getRelationshipForType(type);

        return new IterableAdapter<>(
                template.getSource().getRawSource(SecurityContext.getSuperUserInstance(), dbNode, null), factory);
    }

    @Override
    public <R extends AbstractRelationship> Iterable<R> getRelationships() {
        return new IterableAdapter<>(dbNode.getRelationships(), new RelationshipFactory<R>(securityContext));
    }

    @Override
    public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> Iterable<R> getRelationships(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
        final R template = getRelationshipForType(type);
        final Direction direction = template.getDirectionForType(entityType);
        final RelationshipType relType = template;

        return new IterableAdapter<>(dbNode.getRelationships(relType, direction), factory);
    }

    @Override
    public <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, OneStartpoint<A>, T>> R getIncomingRelationship(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
        final R template = getRelationshipForType(type);
        final Relationship relationship = template.getSource().getRawSource(securityContext, dbNode, null);

        if (relationship != null) {
            return factory.adapt(relationship);
        }

        return null;
    }

    @Override
    public <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> Iterable<R> getIncomingRelationships(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
        final R template = getRelationshipForType(type);

        return new IterableAdapter<>(
                new IdSorter<>(template.getSource().getRawSource(securityContext, dbNode, null)), factory);
    }

    @Override
    public <A extends NodeInterface, B extends NodeInterface, S extends Source, R extends Relation<A, B, S, OneEndpoint<B>>> R getOutgoingRelationship(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
        final R template = getRelationshipForType(type);
        final Relationship relationship = template.getTarget().getRawSource(securityContext, dbNode, null);

        if (relationship != null) {
            return factory.adapt(relationship);
        }

        return null;
    }

    protected <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> R getOutgoingRelationshipAsSuperUser(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance());
        final R template = getRelationshipForType(type);
        final Relationship relationship = template.getSource().getRawTarget(SecurityContext.getSuperUserInstance(),
                dbNode, null);

        if (relationship != null) {
            return factory.adapt(relationship);
        }

        return null;
    }

    @Override
    public <A extends NodeInterface, B extends NodeInterface, S extends Source, R extends Relation<A, B, S, ManyEndpoint<B>>> Iterable<R> getOutgoingRelationships(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
        final R template = getRelationshipForType(type);

        return new IterableAdapter<>(
                new IdSorter<>(template.getTarget().getRawSource(securityContext, dbNode, null)), factory);
    }

    @Override
    public <R extends AbstractRelationship> Iterable<R> getIncomingRelationships() {
        return new IterableAdapter<>(new IdSorter<>(dbNode.getRelationships(Direction.INCOMING)),
                new RelationshipFactory<R>(securityContext));
    }

    @Override
    public <R extends AbstractRelationship> Iterable<R> getOutgoingRelationships() {
        return new IterableAdapter<>(new IdSorter<>(dbNode.getRelationships(Direction.OUTGOING)),
                new RelationshipFactory<R>(securityContext));
    }

    @Override
    public <R extends AbstractRelationship> Iterable<R> getRelationshipsAsSuperUser() {
        return new IterableAdapter<>(dbNode.getRelationships(),
                new RelationshipFactory<R>(SecurityContext.getSuperUserInstance()));
    }

    /**
     * Return statistical information on all relationships of this node
     *
     * @param dir
     * @return number of relationships
     */
    public Map<RelationshipType, Long> getRelationshipInfo(final Direction dir) throws FrameworkException {
        return StructrApp.getInstance(securityContext).command(NodeRelationshipStatisticsCommand.class)
                .execute(this, dir);
    }

    /**
     * Returns the owner node of this node, following an INCOMING OWNS
     * relationship.
     *
     * @return the owner node of this node
     */
    @Override
    public Principal getOwnerNode() {

        if (cachedOwnerNode == null) {

            if (this instanceof Principal && !(this instanceof Group)) {

                // a user is its own owner
                cachedOwnerNode = (Principal) this;

            } else {

                final Ownership ownership = getIncomingRelationshipAsSuperUser(PrincipalOwnsNode.class);
                if (ownership != null) {

                    Principal principal = ownership.getSourceNode();
                    cachedOwnerNode = (Principal) principal;
                }
            }
        }

        return cachedOwnerNode;
    }

    /**
     * Returns the database ID of the owner node of this node.
     *
     * @return the database ID of the owner node of this node
     */
    public Long getOwnerId() {

        return getOwnerNode().getId();

    }

    protected <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, OneStartpoint<A>, T>> R getIncomingRelationshipAsSuperUser(
            final Class<R> type) {

        final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance());
        final R template = getRelationshipForType(type);
        final Relationship relationship = template.getSource().getRawSource(SecurityContext.getSuperUserInstance(),
                dbNode, null);

        if (relationship != null) {
            return factory.adapt(relationship);
        }

        return null;
    }

    /**
     * Return true if this node has a relationship of given type and
     * direction.
     *
     * @param <A>
     * @param <B>
     * @param <S>
     * @param <T>
     * @param type
     * @return relationships
     */
    public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target> boolean hasRelationship(
            final Class<? extends Relation<A, B, S, T>> type) {
        return this.getRelationships(type).iterator().hasNext();
    }

    public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> boolean hasIncomingRelationships(
            final Class<R> type) {
        return getRelationshipForType(type).getSource().hasElements(securityContext, dbNode, null);
    }

    public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> boolean hasOutgoingRelationships(
            final Class<R> type) {
        return getRelationshipForType(type).getTarget().hasElements(securityContext, dbNode, null);
    }

    // ----- interface AccessControllable -----
    @Override
    public boolean isGranted(final Permission permission, final SecurityContext context) {

        // super user can do everything
        if (context != null && context.isSuperUser()) {
            return true;
        }

        Principal accessingUser = null;
        if (context != null) {

            accessingUser = context.getUser(false);
        }

        return isGranted(permission, accessingUser, 0, new HashSet<>());
    }

    private boolean isGranted(final Permission permission, final Principal accessingUser, final int level,
            final Set<Long> alreadyTraversed) {

        if (level > 100) {
            logger.log(Level.WARNING,
                    "Aborting recursive permission resolution because of recursion level > 100, this is quite likely an infinite loop.");
            return false;
        }

        // use quick checks for maximum performance
        if (isCreation && (accessingUser == null || accessingUser.equals(getOwnerNode()))) {
            return true;
        }

        // this includes SuperUser
        if (accessingUser != null && accessingUser.isAdmin()) {
            return true;
        }

        // allow accessingUser to access itself, but not parents etc.
        if (this.equals(accessingUser) && (level == 0 || (permission.equals(Permission.read) && level > 0))) {
            return true;
        }

        // check owner
        final Principal _owner = getOwnerNode();
        final boolean hasOwner = (_owner != null);

        // allow full access for nodes without owner
        // (covered by ResourceAccess objects)
        if (!hasOwner && Services.getPermissionsForOwnerlessNodes().contains(permission)) {

            if (accessingUser != null && isVisibleToAuthenticatedUsers()) {
                return true;
            }

            if (accessingUser == null && isVisibleToPublicUsers()) {
                return true;
            }
        }

        // node has an owner, deny anonymous access
        if (hasOwner && accessingUser == null) {
            return false;
        }

        if (accessingUser != null) {

            // owner is always allowed to do anything with its nodes
            if (hasOwner && accessingUser.equals(_owner)) {
                return true;
            }

            final Security security = getSecurityRelationship(accessingUser);
            if (security != null && security.isAllowed(permission)) {
                return true;
            }

            // Check permissions from domain relationships
            if (hasEffectivePermissions(accessingUser, permission)) {
                return true;
            }

            // Last: recursively check possible parent principals
            for (Principal parent : accessingUser.getParents()) {

                if (isGranted(permission, parent, level + 1, alreadyTraversed)) {
                    return true;
                }
            }
        }

        return false;
    }

    private boolean hasEffectivePermissions(final Principal principal, final Permission permission) {

        final boolean doLog = securityContext.hasParameter("debugLoggingEnabled");

        // don't check relationship propagation if there are no propagating relationships
        if (SchemaRelationshipNode.getPropagatingRelationshipTypes().isEmpty()) {
            return false;
        }

        if (doLog) {
            System.out.println("\n#######################################################\nResolving "
                    + permission.name() + " for user " + principal.getName() + " to " + this.getType() + " ("
                    + this.getUuid() + ")");
        }

        final SecurityContext superUserContext = SecurityContext.getSuperUserInstance();
        final RelationshipFactory relFactory = new RelationshipFactory(superUserContext);
        PermissionResolutionMask mask = AccessPathCache.get(principal, this);

        // current path segment has precedence over path based permission resolution mask
        if (rawPathSegment != null) {

            final boolean result = checkPathSegment(principal, permission, relFactory);

            if (doLog) {

                if (result) {

                    System.out.println("        " + permission.name() + " ALLOWED by path segment "
                            + rawPathSegment.getType().name());

                } else {

                    System.out.println("        " + permission.name() + " DENIED by path segment "
                            + rawPathSegment.getType().name());
                }
            }

            if (result) {
                return true;
            }
        }

        // use cached result only when it was already checked for the given permission
        if (mask != null && mask.alreadyChecked(permission)) {

            final boolean result = mask.allowsPermission(permission);

            if (doLog) {
                if (result) {

                    System.out.println("        " + permission.name() + " ALLOWED by cached mask " + mask);

                } else {

                    System.out.println("        " + permission.name() + " DENIED by cached mask " + mask);
                }
            }

            return result;
        }

        try {

            if (mask == null) {

                // store only a single mask for every node
                mask = new PermissionResolutionMask();
                AccessPathCache.put(principal, this, mask);

                if (doLog) {
                    System.out.println("        Storing initial mask: " + mask);
                }
            }

            // store all check attempts in the cache
            mask.setChecked(permission);

            final GraphDatabaseService db = StructrApp.getInstance().getGraphDatabaseService();
            final String relTypes = getPermissionPropagationRelTypes();
            final Map<String, Object> params = new HashMap<>();
            final long principalId = principal.getId();

            params.put("id1", principalId);
            params.put("id2", this.getId());

            // FIXME: make fixed path length of 8 configurable
            for (int i = 1; i < 10; i++) {

                final String query = "MATCH n, m, p = allShortestPaths(n-[" + relTypes + "*.." + i
                        + "]-m) WHERE id(n) = {id1} AND id(m) = {id2} RETURN p";
                final Result result = db.execute(query, params);

                while (result.hasNext()) {

                    final Map<String, Object> row = result.next();
                    final Path path = (Path) row.get("p");
                    Node previousNode = null;
                    boolean arrived = true;

                    for (final PropertyContainer container : path) {

                        if (container instanceof Node) {

                            // store previous node to determine relationship direction
                            previousNode = (Node) container;
                            AccessPathCache.update(principal, this, previousNode);

                        } else {

                            final Relationship rel = (Relationship) container;
                            final RelationshipInterface r = relFactory.instantiate(rel);

                            if (r instanceof PermissionPropagation) {

                                // update cache with relationship type
                                AccessPathCache.update(principal, this, rel);

                                final PermissionPropagation propagation = (PermissionPropagation) r;
                                final long startNodeId = rel.getStartNode().getId();
                                final long thisId = previousNode.getId();
                                final SchemaRelationshipNode.Direction relDirection = thisId == startNodeId
                                        ? SchemaRelationshipNode.Direction.Out
                                        : SchemaRelationshipNode.Direction.In;
                                final SchemaRelationshipNode.Direction propagationDirection = propagation
                                        .getPropagationDirection();

                                // check propagation direction
                                if (!propagationDirection.equals(SchemaRelationshipNode.Direction.Both)) {

                                    if (propagationDirection.equals(SchemaRelationshipNode.Direction.None)) {

                                        mask.clear();
                                        arrived = false;
                                        break;
                                    }

                                    if (!relDirection.equals(propagationDirection)) {

                                        mask.clear();
                                        arrived = false;
                                        break;
                                    }
                                }

                                applyCurrentStep(propagation, mask);

                                // break early
                                if (!mask.allowsPermission(permission)) {

                                    if (doLog) {
                                        System.out.println("        " + permission.name() + " DENIED by " + path);
                                    }

                                    arrived = false;
                                    break;
                                }

                            } else {

                                if (doLog) {
                                    System.out.println("        " + permission.name() + " DENIED by " + path);
                                }

                                arrived = false;
                                break;
                            }
                        }
                    }

                    if (arrived && mask.allowsPermission(permission)) {

                        if (doLog) {
                            System.out.println("        " + permission.name() + " ALLOWED by " + path);
                            System.out.println("        Storing mask from path: " + mask);
                        }

                        AccessPathCache.put(principal, this, mask);

                        return true;
                    }
                }
            }

        } catch (Throwable t) {
            t.printStackTrace();
        }

        mask.setPermission(permission, false);
        AccessPathCache.put(principal, this, mask);

        if (doLog) {
            System.out.println("        Storing mask from unsuccessful path: " + mask);
        }

        return false;
    }

    private boolean checkPathSegment(final Principal principal, final Permission permission,
            final RelationshipFactory relFactory) {

        final boolean doLog = securityContext.hasParameter("debugLoggingEnabled");
        final RelationshipInterface r = relFactory.instantiate(rawPathSegment);
        if (r instanceof PermissionPropagation) {

            final PermissionPropagation propagation = (PermissionPropagation) r;
            final long startNodeId = rawPathSegment.getStartNode().getId();
            final long thisId = getId();
            final SchemaRelationshipNode.Direction relDirection = thisId == startNodeId
                    ? SchemaRelationshipNode.Direction.In
                    : SchemaRelationshipNode.Direction.Out;
            final SchemaRelationshipNode.Direction propagationDirection = propagation.getPropagationDirection();
            final PermissionResolutionMask mask = new PermissionResolutionMask();

            // check propagation direction
            if (!propagationDirection.equals(SchemaRelationshipNode.Direction.Both)) {

                if (propagationDirection.equals(SchemaRelationshipNode.Direction.None)) {
                    return false;
                }

                if (!relDirection.equals(propagationDirection)) {
                    return false;
                }
            }

            // we can safely assume here that we arrived at this node with
            // the read permission, because otherwise the node would not
            // have been visible.
            mask.setPermission(Permission.read, true);

            // apply current
            applyCurrentStep(propagation, mask);

            if (mask.allowsPermission(permission)) {

                mask.setChecked(permission);
                AccessPathCache.put(principal, this, mask);

                if (doLog) {
                    System.out.println("Storing mask from path segment: " + mask);
                }

                return true;
            }
        }

        return false;
    }

    private void applyCurrentStep(final PermissionPropagation rel, PermissionResolutionMask mask) {

        final boolean doLog = securityContext.hasParameter("debugLoggingEnabled");

        switch (rel.getReadPropagation()) {
        case Add:
            mask.addRead();
            if (doLog) {
                System.out.println("                add read");
            }
            break;

        case Remove:
            mask.removeRead();
            if (doLog) {
                System.out.println("                remove read");
            }
            break;

        default:
            break;
        }

        switch (rel.getWritePropagation()) {
        case Add:
            mask.addWrite();
            if (doLog) {
                System.out.println("                add write");
            }
            break;

        case Remove:
            mask.removeWrite();
            if (doLog) {
                System.out.println("                remove write");
            }
            break;

        default:
            break;
        }

        switch (rel.getDeletePropagation()) {
        case Add:
            mask.addDelete();
            if (doLog) {
                System.out.println("                add delete");
            }
            break;

        case Remove:
            mask.removeDelete();
            if (doLog) {
                System.out.println("                remove delete");
            }
            break;

        default:
            break;
        }

        switch (rel.getAccessControlPropagation()) {
        case Add:
            mask.addAccessControl();
            if (doLog) {
                System.out.println("                add accessControl");
            }
            break;

        case Remove:
            mask.removeAccessControl();
            if (doLog) {
                System.out.println("                remove accessControl");
            }
            break;

        default:
            break;
        }

        // handle delta properties
        mask.handleProperties(rel.getDeltaProperties());
    }

    /**
     * Return the (cached) incoming relationship between this node and the
     * given principal which holds the security information.
     *
     * @param p
     * @return incoming security relationship
     */
    @Override
    public Security getSecurityRelationship(final Principal p) {

        if (p == null) {

            return null;
        }

        for (final Security r : getIncomingRelationshipsAsSuperUser(Security.class)) {

            if (r != null) {

                if (p.equals(r.getSourceNode())) {

                    return r;

                }
            }
        }

        return null;

    }

    @Override
    public boolean onCreation(SecurityContext securityContext, ErrorBuffer errorBuffer) throws FrameworkException {
        return true;
    }

    @Override
    public boolean onModification(SecurityContext securityContext, ErrorBuffer errorBuffer)
            throws FrameworkException {
        return true;
    }

    @Override
    public boolean onDeletion(SecurityContext securityContext, ErrorBuffer errorBuffer, PropertyMap properties)
            throws FrameworkException {
        return true;
    }

    @Override
    public void afterCreation(SecurityContext securityContext) {
    }

    @Override
    public void afterModification(SecurityContext securityContext) {
    }

    @Override
    public void afterDeletion(SecurityContext securityContext, PropertyMap properties) {
    }

    @Override
    public void ownerModified(SecurityContext securityContext) {
    }

    @Override
    public void securityModified(SecurityContext securityContext) {
    }

    @Override
    public void locationModified(SecurityContext securityContext) {
    }

    @Override
    public void propagatedModification(SecurityContext securityContext) {
    }

    @Override
    public boolean isValid(ErrorBuffer errorBuffer) {

        boolean error = false;

        error |= ValidationHelper.checkStringNotBlank(this, id, errorBuffer);
        error |= ValidationHelper.checkStringNotBlank(this, type, errorBuffer);

        return !error;

    }

    @Override
    public boolean isVisibleToPublicUsers() {

        return getVisibleToPublicUsers();

    }

    @Override
    public boolean isVisibleToAuthenticatedUsers() {

        return getProperty(visibleToAuthenticatedUsers);

    }

    @Override
    public boolean isNotHidden() {

        return !getHidden();

    }

    @Override
    public boolean isHidden() {

        return getHidden();

    }

    @Override
    public Date getVisibilityStartDate() {
        return getProperty(visibilityStartDate);
    }

    @Override
    public Date getVisibilityEndDate() {
        return getProperty(visibilityEndDate);
    }

    @Override
    public Date getCreatedDate() {
        return getProperty(createdDate);
    }

    @Override
    public Date getLastModifiedDate() {
        return getProperty(lastModifiedDate);
    }

    // ----- end interface AccessControllable -----
    public boolean isNotDeleted() {

        return !getDeleted();

    }

    @Override
    public boolean isDeleted() {

        return getDeleted();

    }

    /**
     * Return true if node is the root node
     *
     * @return isRootNode
     */
    public boolean isRootNode() {

        return getId() == 0;

    }

    public boolean isVisible() {

        return securityContext.isVisible(this);

    }

    @Override
    public boolean canHaveOwner() {
        return true;
    }

    /**
     * Set a property in database backend. This method needs to be wrappend
     * into a StructrTransaction, otherwise Neo4j will throw a
     * NotInTransactionException! Set property only if value has changed.
     *
     * @param <T>
     * @param key
     * @throws org.structr.common.error.FrameworkException
     */
    @Override
    public <T> Object setProperty(final PropertyKey<T> key, final T value) throws FrameworkException {

        try {

            // allow setting of ID without permissions
            if (!key.equals(GraphObject.id)) {

                if (!isGranted(Permission.write, securityContext)) {
                    throw new FrameworkException(403, "Modification not permitted.");
                }
            }

            T oldValue = getProperty(key);

            // no old value exists  OR  old value exists and is NOT equal => set property
            if (((oldValue == null) && (value != null)) || ((oldValue != null) && !oldValue.equals(value))) {

                return setPropertyInternal(key, value);

            }

        } finally {

            // unconditionally lock read-only properties after every write (attempt) to avoid security problems
            // since we made "unlock_readonly_properties_once" available through scripting
            this.readOnlyPropertiesUnlocked = false;

        }

        return null;
    }

    private <T> Object setPropertyInternal(final PropertyKey<T> key, final T value) throws FrameworkException {

        if (key == null) {

            logger.log(Level.SEVERE, "Tried to set property with null key (action was denied)");

            throw new FrameworkException(422, new NullArgumentToken(getClass().getSimpleName(), base));

        }

        // check for read-only properties
        if (key.isReadOnly() || (key.isWriteOnce() && (dbNode != null) && dbNode.hasProperty(key.dbName()))) {

            if (!readOnlyPropertiesUnlocked && !securityContext.isSuperUser()) {

                throw new FrameworkException(422, new ReadOnlyPropertyToken(getClass().getSimpleName(), key));

            }

        }

        return key.setProperty(securityContext, this, value);
    }

    @Override
    public void addToIndex() {

        for (PropertyKey key : StructrApp.getConfiguration().getPropertySet(entityType, PropertyView.All)) {

            if (key.isIndexed()) {

                key.index(this, this.getPropertyForIndexing(key));
            }
        }
    }

    @Override
    public void updateInIndex() {

        removeFromIndex();
        addToIndex();
    }

    @Override
    public void removeFromIndex() {

        for (Index<Node> index : Services.getInstance().getService(NodeService.class).getNodeIndices()) {
            index.remove(dbNode);
        }
    }

    public void removeFromIndex(PropertyKey key) {

        for (Index<Node> index : Services.getInstance().getService(NodeService.class).getNodeIndices()) {
            index.remove(dbNode, key.dbName());
        }
    }

    @Override
    public void indexPassiveProperties() {

        for (PropertyKey key : StructrApp.getConfiguration().getPropertySet(entityType, PropertyView.All)) {

            if (key.isPassivelyIndexed()) {

                key.index(this, this.getPropertyForIndexing(key));
            }
        }
    }

    public static <A extends NodeInterface, B extends NodeInterface, R extends Relation<A, B, ?, ?>> R getRelationshipForType(
            final Class<R> type) {

        R instance = (R) relationshipTemplateInstanceCache.get(type.getName());
        if (instance == null) {

            try {

                instance = type.newInstance();
                relationshipTemplateInstanceCache.put(type.getName(), instance);

            } catch (Throwable t) {

                // TODO: throw meaningful exception here,
                // should be a RuntimeException that indicates
                // wrong use of Relationships etc.
                t.printStackTrace();
            }
        }

        return instance;
    }

    @Override
    public String getPropertyWithVariableReplacement(ActionContext renderContext, PropertyKey<String> key)
            throws FrameworkException {
        return Scripting.replaceVariables(renderContext, this, getProperty(key));
    }

    @Override
    public Object evaluate(final SecurityContext securityContext, final String key, final String defaultValue)
            throws FrameworkException {

        switch (key) {

        case "owner":
            return getOwnerNode();

        case "_path":
            if (rawPathSegment != null) {

                return new RelationshipFactory<>(securityContext).adapt(rawPathSegment);

            } else {

                return null;
            }

        default:

            // evaluate object value or return default
            Object value = getProperty(StructrApp.getConfiguration().getPropertyKeyForJSONName(entityType, key));

            if (value != null) {
                return value;
            }

            value = invokeMethod(key, Collections.EMPTY_MAP, false);
            if (value != null) {
                return value;
            }

            return Function.numberOrString(defaultValue);
        }
    }

    @Override
    public Object invokeMethod(final String methodName, final Map<String, Object> propertySet,
            final boolean throwExceptionForUnknownMethods) throws FrameworkException {

        final Method method = StructrApp.getConfiguration().getExportedMethodsForType(entityType).get(methodName);
        if (method != null) {

            try {

                // First, try if single parameter is a map, then directly invoke method
                if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(Map.class)) {
                    return method.invoke(this, propertySet);
                }

                // second try: extracted parameter list
                return method.invoke(this, extractParameters(propertySet, method.getParameterTypes()));

            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException t) {

                if (t instanceof FrameworkException) {

                    throw (FrameworkException) t;

                } else if (t.getCause() instanceof FrameworkException) {

                    throw (FrameworkException) t.getCause();

                } else {

                    t.printStackTrace();

                    logger.log(Level.FINE, "Unable to invoke method {0}: {1}",
                            new Object[] { methodName, t.getMessage() });
                }
            }
        }

        // in the case of REST access we want to know if the method exists or not
        if (throwExceptionForUnknownMethods) {
            throw new FrameworkException(400, "Method " + methodName + " not found in type " + getType());
        }

        return null;
    }

    private Object[] extractParameters(Map<String, Object> properties, Class[] parameterTypes) {

        final List<Object> values = new ArrayList<>(properties.values());
        final List<Object> parameters = new ArrayList<>();
        int index = 0;

        // only try to convert when both lists have equal size
        if (values.size() == parameterTypes.length) {

            for (final Class parameterType : parameterTypes) {

                final Object value = convert(values.get(index++), parameterType);
                if (value != null) {

                    parameters.add(value);
                }
            }
        }

        return parameters.toArray(new Object[0]);
    }

    /*
     * Tries to convert the given value into an object
     * of the given type, using an intermediate type
     * of String for the conversion.
     */
    private Object convert(Object value, Class type) {

        Object convertedObject = null;

        if (type.equals(String.class)) {

            // strings can be returned immediately
            return value.toString();

        } else if (value instanceof Number) {

            Number number = (Number) value;

            if (type.equals(Integer.class) || type.equals(Integer.TYPE)) {
                return number.intValue();

            } else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
                return number.longValue();

            } else if (type.equals(Double.class) || type.equals(Double.TYPE)) {
                return number.doubleValue();

            } else if (type.equals(Float.class) || type.equals(Float.TYPE)) {
                return number.floatValue();

            } else if (type.equals(Short.class) || type.equals(Integer.TYPE)) {
                return number.shortValue();

            } else if (type.equals(Byte.class) || type.equals(Byte.TYPE)) {
                return number.byteValue();

            }

        } else if (value instanceof List) {

            return value;

        } else if (value instanceof Map) {
            return value;
        }

        // fallback
        try {

            Method valueOf = type.getMethod("valueOf", String.class);
            if (valueOf != null) {

                convertedObject = valueOf.invoke(null, value.toString());

            } else {

                logger.log(Level.WARNING, "Unable to find static valueOf method for type {0}", type);
            }

        } catch (Throwable t) {

            logger.log(Level.WARNING,
                    "Unable to deserialize value {0} of type {1}, Class has no static valueOf method.",
                    new Object[] { value, type });
        }

        return convertedObject;
    }

    @Override
    public void grant(Permission permission, Principal principal) throws FrameworkException {

        if (!isGranted(Permission.accessControl, securityContext)) {
            throw new FrameworkException(403, "Access control not permitted");
        }

        Security secRel = getSecurityRelationship(principal);
        if (secRel == null) {

            try {

                secRel = StructrApp.getInstance().create(principal, (NodeInterface) this, Security.class);

            } catch (FrameworkException ex) {

                logger.log(Level.SEVERE, "Could not create security relationship!", ex);

            }

        }

        secRel.addPermission(permission);

    }

    @Override
    public void revoke(Permission permission, Principal principal) throws FrameworkException {

        if (!isGranted(Permission.accessControl, securityContext)) {
            throw new FrameworkException(403, "Access control not permitted");
        }

        Security secRel = getSecurityRelationship(principal);
        if (secRel == null) {

            logger.log(Level.SEVERE, "Could not create revoke permission, no security relationship exists!");

        } else {

            secRel.removePermission(permission);
        }
    }

    @Override
    public void setRawPathSegment(final Relationship rawPathSegment) {
        this.rawPathSegment = rawPathSegment;
    }

    @Override
    public Relationship getRawPathSegment() {
        return rawPathSegment;
    }

    public void revokeAll() throws FrameworkException {

        if (!isGranted(Permission.accessControl, securityContext)) {
            throw new FrameworkException(403, "Access control not permitted");
        }

        final App app = StructrApp.getInstance();

        for (final Security security : getIncomingRelationshipsAsSuperUser(Security.class)) {
            app.delete(security);
        }
    }

    @Override
    public PermissionResolutionMask getPermissionResolutionMask() {
        return permissionResolutionMask;
    }

    private String getPermissionPropagationRelTypes() {
        return ":" + StringUtils.join(SchemaRelationshipNode.getPropagatingRelationshipTypes(), "|");
    }

    // ----- Cloud synchronization and replication -----
    @Override
    public List<GraphObject> getSyncData() throws FrameworkException {
        return new ArrayList<>(); // provide a basis for super.getSyncData() calls
    }

    @Override
    public boolean isNode() {
        return true;
    }

    @Override
    public boolean isRelationship() {
        return false;
    }

    @Override
    public NodeInterface getSyncNode() {
        return this;
    }

    @Override
    public RelationshipInterface getSyncRelationship() {
        throw new ClassCastException(
                this.getClass() + " cannot be cast to org.structr.core.graph.RelationshipInterface");
    }

    @Override
    public void updateFromPropertyMap(final Map<String, Object> properties) throws FrameworkException {

        // update all properties that exist in the source map
        for (final Entry<String, Object> entry : properties.entrySet()) {

            final String key = entry.getKey();
            final Object val = entry.getValue();

            if (val != null) {

                getNode().setProperty(key, val);

            } else {

                getNode().removeProperty(key);
            }
        }
    }

    // ----- CMIS support methods -----
    @Override
    public CMISInfo getCMISInfo() {
        return this;
    }

    @Override
    public BaseTypeId getBaseTypeId() {
        return BaseTypeId.CMIS_ITEM;
    }

    @Override
    public CMISFolderInfo getFolderInfo() {
        return null;
    }

    @Override
    public CMISDocumentInfo getDocumentInfo() {
        return null;
    }

    @Override
    public CMISItemInfo geItemInfo() {
        return this;
    }

    @Override
    public CMISRelationshipInfo getRelationshipInfo() {
        return null;
    }

    @Override
    public CMISPolicyInfo getPolicyInfo() {
        return null;
    }

    @Override
    public CMISSecondaryInfo getSecondaryInfo() {
        return null;
    }

    @Override
    public String getCreatedBy() {
        return getProperty(AbstractNode.createdBy);
    }

    @Override
    public String getLastModifiedBy() {
        return getProperty(AbstractNode.lastModifiedBy);
    }

    @Override
    public GregorianCalendar getLastModificationDate() {

        final Date creationDate = getProperty(AbstractNode.lastModifiedDate);
        if (creationDate != null) {

            final GregorianCalendar calendar = new GregorianCalendar();
            calendar.setTime(creationDate);

            return calendar;
        }

        return null;
    }

    @Override
    public GregorianCalendar getCreationDate() {

        final Date creationDate = getProperty(AbstractNode.createdDate);
        if (creationDate != null) {

            final GregorianCalendar calendar = new GregorianCalendar();
            calendar.setTime(creationDate);

            return calendar;
        }

        return null;
    }

    @Override
    public PropertyMap getDynamicProperties() {

        final PropertyMap propertyMap = new PropertyMap();
        final Class type = getClass();

        for (final PropertyKey key : StructrApp.getConfiguration().getPropertySet(type, PropertyView.All)) {

            // include all dynamic keys in definition
            if (key.isDynamic() || key.isCMISProperty()) {

                // only include primitives here
                final PropertyType dataType = key.getDataType();
                if (dataType != null) {

                    propertyMap.put(key, getProperty(key));
                }
            }
        }

        return propertyMap;
    }

    @Override
    public AllowableActions getAllowableActions() {
        return new StructrItemActions();
    }

    @Override
    public List<Ace> getAccessControlEntries() {

        final List<Ace> entries = new LinkedList<>();

        for (final Security security : getIncomingRelationshipsAsSuperUser(Security.class)) {

            if (security != null) {

                entries.add(new AceEntry(security));
            }
        }

        return entries;
    }

    // ----- nested classes -----
    private static class AceEntry extends CMISExtensionsData
            implements Ace, org.apache.chemistry.opencmis.commons.data.Principal {

        private final List<String> permissions = new LinkedList<>();
        private String principalId = null;

        /**
         * Construct a new AceEntry from the given Security relationship. This
         * method assumes that is is called in a transaction.
         *
         * @param security
         */
        public AceEntry(final Security security) {

            final Principal principal = security.getSourceNode();
            if (principal != null) {

                this.principalId = principal.getProperty(Principal.name);
            }

            permissions.addAll(security.getPermissions());
        }

        @Override
        public org.apache.chemistry.opencmis.commons.data.Principal getPrincipal() {
            return this;
        }

        @Override
        public String getPrincipalId() {
            return principalId;
        }

        @Override
        public List<String> getPermissions() {
            return permissions;
        }

        @Override
        public boolean isDirect() {
            return true;
        }

        // ----- interface Principal -----
        @Override
        public String getId() {
            return getPrincipalId();
        }
    }

    // ----- public static methods -----
    public static void invalidateCacheFor(final String uuid) {

    }
}