org.structr.core.property.Property.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.core.property.Property.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.property;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.chemistry.opencmis.commons.enums.PropertyType;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.BooleanClause;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.index.Index;
import org.neo4j.helpers.Predicate;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.PropertyValidator;
import org.structr.core.Services;
import org.structr.core.app.Query;
import org.structr.core.converter.PropertyConverter;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.AbstractRelationship;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.NodeService;
import org.structr.core.graph.NodeService.NodeIndex;
import org.structr.core.graph.NodeService.RelationshipIndex;
import org.structr.core.graph.search.PropertySearchAttribute;
import org.structr.core.graph.search.SearchAttribute;

/**
 * Abstract base class for all property types.
 *
 *
 */
public abstract class Property<T> implements PropertyKey<T> {

    private static final Logger logger = Logger.getLogger(Property.class.getName());
    private static final Pattern rangeQueryPattern = Pattern.compile("\\[(.+) TO (.+)\\]");

    protected List<PropertyValidator<T>> validators = new LinkedList<>();
    protected Class<? extends GraphObject> declaringClass = null;
    protected T defaultValue = null;
    protected boolean readOnly = false;
    protected boolean writeOnce = false;
    protected boolean unvalidated = false;
    protected boolean indexed = false;
    protected boolean indexedPassively = false;
    protected boolean searchable = false;
    protected boolean indexedWhenEmpty = false;
    protected boolean unique = false;
    protected boolean notNull = false;
    protected boolean dynamic = false;
    protected boolean isCMISProperty = false;
    protected String dbName = null;
    protected String jsonName = null;
    protected String format = null;
    protected String readFunction = null;
    protected String writeFunction = null;

    private boolean requiresSynchronization = false;

    protected Set<RelationshipIndex> relationshipIndices = new LinkedHashSet<>();
    protected Set<NodeIndex> nodeIndices = new LinkedHashSet<>();

    protected Property(final String name) {
        this(name, name);
    }

    protected Property(final String jsonName, final String dbName) {
        this(jsonName, dbName, null);
    }

    protected Property(final String jsonName, final String dbName, final T defaultValue) {
        this.defaultValue = defaultValue;
        this.jsonName = jsonName;
        this.dbName = dbName;
    }

    @Override
    public abstract Object fixDatabaseProperty(final Object value);

    public abstract Object getValueForEmptyFields();

    /**
     * Use this method to mark a property as being unvalidated. This
     * method will cause no callbacks to be executed when only
     * unvalidated properties are modified.
     *
     * @return  the Property to satisfy the builder pattern
     */
    public Property<T> unvalidated() {
        this.unvalidated = true;
        return this;
    }

    /**
     * Use this method to mark a property as being read-only.
     *
     * @return the Property to satisfy the builder pattern
     */
    public Property<T> readOnly() {
        this.readOnly = true;
        return this;
    }

    /**
     * Use this method to mark a property as being write-once.
     *
     * @return the Property to satisfy the builder pattern
     */
    public Property<T> writeOnce() {
        this.writeOnce = true;
        return this;
    }

    /**
     * Use this method to mark a property as being unique. Please note that
     * using this method will not actually cause a uniqueness check, just
     * notify the system that this property should be treated as having a
     * unique value.
     *
     * @return the Property to satisfy the builder pattern
     */
    public Property<T> unique() {
        this.unique = true;
        this.requiresSynchronization = true;
        return this;
    }

    /**
     * Use this method to mark a property as being not-null. Please note that
     * using this method will not actually cause a not-null check, just
     * notify the system that this property should be treated as such.
     *
     * @return the Property to satisfy the builder pattern
     */
    public Property<T> notNull() {
        this.notNull = true;
        return this;
    }

    @Override
    public Property<T> indexed() {

        this.indexed = true;
        this.searchable = true;

        nodeIndices.add(NodeIndex.fulltext);
        nodeIndices.add(NodeIndex.keyword);

        relationshipIndices.add(RelationshipIndex.rel_fulltext);
        relationshipIndices.add(RelationshipIndex.rel_keyword);

        return this;
    }

    @Override
    public Property<T> indexed(NodeIndex nodeIndex) {

        this.indexed = true;
        this.searchable = true;

        nodeIndices.add(nodeIndex);

        return this;
    }

    @Override
    public Property<T> indexedCaseInsensitive() {

        indexed();

        nodeIndices.add(NodeIndex.caseInsensitive);

        return this;
    }

    @Override
    public Property<T> indexed(final RelationshipIndex relIndex) {

        this.indexed = true;
        this.searchable = true;

        relationshipIndices.add(relIndex);

        return this;
    }

    @Override
    public Property<T> passivelyIndexed() {

        this.indexedPassively = true;
        this.indexed = true;
        this.searchable = true;

        nodeIndices.add(NodeIndex.fulltext);
        nodeIndices.add(NodeIndex.keyword);

        relationshipIndices.add(RelationshipIndex.rel_fulltext);
        relationshipIndices.add(RelationshipIndex.rel_keyword);

        return this;
    }

    @Override
    public Property<T> passivelyIndexed(final NodeIndex nodeIndex) {

        this.indexedPassively = true;
        this.indexed = true;
        this.searchable = true;

        nodeIndices.add(nodeIndex);
        return this;
    }

    @Override
    public Property<T> passivelyIndexed(final RelationshipIndex relIndex) {

        this.indexedPassively = true;
        this.indexed = true;
        this.searchable = true;

        relationshipIndices.add(relIndex);
        return this;
    }

    @Override
    public Property<T> indexedWhenEmpty() {

        passivelyIndexed();
        this.indexedWhenEmpty = true;

        return this;
    }

    @Override
    public Property<T> cmis() {

        this.isCMISProperty = true;

        return this;
    }

    @Override
    public void addValidator(final PropertyValidator<T> validator) {

        validators.add(validator);

        // fetch synchronization requirement from validator
        if (validator.requiresSynchronization()) {
            this.requiresSynchronization = true;
        }
    }

    public Property<T> validator(final PropertyValidator<T> validator) {
        addValidator(validator);
        return this;
    }

    @Override
    public List<PropertyValidator<T>> getValidators() {
        return validators;
    }

    @Override
    public boolean requiresSynchronization() {
        return requiresSynchronization;
    }

    @Override
    public String getSynchronizationKey() {
        return dbName;
    }

    @Override
    public void setDeclaringClass(final Class declaringClass) {
        this.declaringClass = declaringClass;
    }

    @Override
    public void registrationCallback(final Class type) {
    }

    @Override
    public Class getDeclaringClass() {
        return declaringClass;
    }

    @Override
    public String toString() {
        return jsonName();
    }

    @Override
    public String dbName() {
        return dbName;
    }

    @Override
    public String jsonName() {
        return jsonName;
    }

    @Override
    public void dbName(final String dbName) {
        this.dbName = dbName;
    }

    @Override
    public void jsonName(final String jsonName) {
        this.jsonName = jsonName;
    }

    @Override
    public Property<T> defaultValue(final T defaultValue) {
        this.defaultValue = defaultValue;
        return this;
    }

    @Override
    public T defaultValue() {
        return defaultValue;
    }

    @Override
    public String format() {
        return format;
    }

    @Override
    public Property<T> format(final String format) {
        this.format = format;
        return this;
    }

    @Override
    public Property<T> unique(final boolean unique) {
        this.unique = unique;
        return this;
    }

    @Override
    public Property<T> notNull(final boolean notNull) {
        this.notNull = notNull;
        return this;
    }

    @Override
    public Property<T> dynamic() {
        this.dynamic = true;
        return this;
    }

    @Override
    public String readFunction() {
        return readFunction;
    }

    @Override
    public Property<T> readFunction(final String readFunction) {
        this.readFunction = readFunction;
        return this;
    }

    @Override
    public String writeFunction() {
        return writeFunction;
    }

    @Override
    public Property<T> writeFunction(final String writeFunction) {
        this.writeFunction = writeFunction;
        return this;
    }

    @Override
    public int hashCode() {

        // make hashCode funtion work for subtypes that override jsonName() etc. as well
        if (dbName() != null && jsonName() != null) {
            return (dbName().hashCode() * 31) + jsonName().hashCode();
        }

        if (dbName() != null) {
            return dbName().hashCode();
        }

        if (jsonName() != null) {
            return jsonName().hashCode();
        }

        // TODO: check if it's ok if null key is not unique
        return super.hashCode();
    }

    @Override
    public boolean equals(final Object o) {

        if (o instanceof PropertyKey) {

            return o.hashCode() == hashCode();
        }

        return false;
    }

    @Override
    public boolean isUnvalidated() {
        return unvalidated;
    }

    @Override
    public boolean isReadOnly() {
        return readOnly;
    }

    @Override
    public boolean isWriteOnce() {
        return writeOnce;
    }

    @Override
    public boolean isIndexed() {
        return indexed;
    }

    @Override
    public boolean isPassivelyIndexed() {
        return indexedPassively;
    }

    @Override
    public boolean isSearchable() {
        return searchable;
    }

    @Override
    public boolean isIndexedWhenEmpty() {
        return indexedWhenEmpty;
    }

    @Override
    public boolean isUnique() {
        return unique;
    }

    @Override
    public boolean isNotNull() {
        return notNull;
    }

    @Override
    public boolean isDynamic() {
        return dynamic;
    }

    @Override
    public void index(final GraphObject entity, Object value) {

        if (entity instanceof AbstractNode) {

            NodeService nodeService = Services.getInstance().getService(NodeService.class);
            AbstractNode node = (AbstractNode) entity;
            Node dbNode = node.getNode();

            for (NodeIndex indexName : nodeIndices()) {

                Index<Node> index = nodeService.getNodeIndex(indexName);
                if (index != null) {

                    try {

                        index.remove(dbNode, dbName);

                        if (value != null && !StringUtils.isBlank(value.toString())) {
                            index.add(dbNode, dbName, value);

                        } else if (isIndexedWhenEmpty()) {

                            value = getValueForEmptyFields();
                            if (value != null) {

                                index.add(dbNode, dbName, value);
                            }
                        }

                    } catch (Throwable t) {

                        logger.log(Level.INFO,
                                "Unable to index property with dbName {0} and value {1} of type {2} on {3}: {4}",
                                new Object[] { dbName, value, this.getClass().getSimpleName(), entity, t });
                        t.printStackTrace();
                    }
                }
            }

        } else {

            NodeService nodeService = Services.getInstance().getService(NodeService.class);
            AbstractRelationship rel = (AbstractRelationship) entity;
            Relationship dbRel = rel.getRelationship();

            for (RelationshipIndex indexName : relationshipIndices()) {

                Index<Relationship> index = nodeService.getRelationshipIndex(indexName);
                if (index != null) {

                    try {

                        index.remove(dbRel, dbName);

                        if (value != null && !StringUtils.isBlank(value.toString())) {

                            index.add(dbRel, dbName, value);

                        } else if (isIndexedWhenEmpty()) {

                            value = getValueForEmptyFields();
                            if (value != null) {

                                index.add(dbRel, dbName, value);
                            }
                        }

                    } catch (Throwable t) {

                        logger.log(Level.INFO,
                                "Unable to index property with dbName {0} and value {1} of type {2} on {3}: {4}",
                                new Object[] { dbName, value, this.getClass().getSimpleName(), entity, t });
                    }
                }
            }
        }
    }

    @Override
    public SearchAttribute getSearchAttribute(final SecurityContext securityContext,
            final BooleanClause.Occur occur, final T searchValue, final boolean exactMatch, final Query query) {
        return new PropertySearchAttribute(this, searchValue, occur, exactMatch);
    }

    @Override
    public void extractSearchableAttribute(final SecurityContext securityContext, final HttpServletRequest request,
            final Query query) throws FrameworkException {

        final String[] searchValues = request.getParameterValues(jsonName());
        if (searchValues != null) {

            for (String searchValue : searchValues) {

                if (!query.isExactSearch()) {

                    // no quotes allowed in loose search queries!
                    searchValue = removeQuotes(searchValue);

                    query.and(this, convertSearchValue(securityContext, searchValue), false);

                } else {

                    determineSearchType(securityContext, searchValue, query);
                }
            }
        }
    }

    @Override
    public T convertSearchValue(final SecurityContext securityContext, final String requestParameter)
            throws FrameworkException {

        PropertyConverter inputConverter = inputConverter(securityContext);
        Object convertedSearchValue = requestParameter;

        if (inputConverter != null) {

            convertedSearchValue = inputConverter.convert(convertedSearchValue);
        }

        return (T) convertedSearchValue;
    }

    @Override
    public int getProcessingOrderPosition() {
        return 0;
    }

    public Set<NodeIndex> nodeIndices() {
        return nodeIndices;
    }

    public Set<RelationshipIndex> relationshipIndices() {
        return relationshipIndices;
    }

    // ----- CMIS support -----
    @Override
    public PropertyType getDataType() {
        return null;
    }

    @Override
    public boolean isCMISProperty() {
        return isCMISProperty;
    }

    // ----- protected methods -----
    protected boolean multiValueSplitAllowed() {
        return true;
    }

    protected final String removeQuotes(final String searchValue) {
        String resultStr = searchValue;

        if (resultStr.contains("\"")) {
            resultStr = resultStr.replaceAll("[\"]+", "");
        }

        if (resultStr.contains("'")) {
            resultStr = resultStr.replaceAll("[']+", "");
        }

        return resultStr;
    }

    protected void determineSearchType(final SecurityContext securityContext, final String requestParameter,
            final Query query) throws FrameworkException {

        if (StringUtils.startsWith(requestParameter, "[") && StringUtils.endsWith(requestParameter, "]")) {

            // check for existance of range query string
            Matcher matcher = rangeQueryPattern.matcher(requestParameter);
            if (matcher.matches()) {

                if (matcher.groupCount() == 2) {

                    String rangeStart = matcher.group(1);
                    String rangeEnd = matcher.group(2);

                    PropertyConverter inputConverter = inputConverter(securityContext);
                    Object rangeStartConverted = rangeStart;
                    Object rangeEndConverted = rangeEnd;

                    if (inputConverter != null) {

                        rangeStartConverted = inputConverter.convert(rangeStartConverted);
                        rangeEndConverted = inputConverter.convert(rangeEndConverted);
                    }

                    query.andRange(this, rangeStartConverted, rangeEndConverted);

                    return;
                }

                logger.log(Level.WARNING, "Unable to determine range query bounds for {0}", requestParameter);

            } else {

                if ("[]".equals(requestParameter)) {

                    if (isIndexedWhenEmpty()) {

                        // requestParameter contains only [],
                        // which we use as a "not-blank" selector
                        query.notBlank(this);

                        return;

                    } else {

                        throw new FrameworkException(400, "PropertyKey " + jsonName()
                                + " must be indexedWhenEmpty() to be used in not-blank search query.");
                    }

                } else {

                    throw new FrameworkException(422, "Invalid range pattern.");
                }
            }
        }

        if (requestParameter.contains(",") && requestParameter.contains(";")) {
            throw new FrameworkException(422, "Mixing of AND and OR not allowed in request parameters");
        }

        if (requestParameter.contains(";")) {

            if (multiValueSplitAllowed()) {

                // descend into a new group
                query.and();

                for (final String part : requestParameter.split("[;]+")) {

                    query.or(this, convertSearchValue(securityContext, part));
                }

                // ascend to the last group
                query.parent();

            } else {

                query.or(this, convertSearchValue(securityContext, requestParameter));
            }

        } else {

            query.and(this, convertSearchValue(securityContext, requestParameter));
        }
    }

    protected <T extends NodeInterface> Set<T> getRelatedNodesReverse(final SecurityContext securityContext,
            final NodeInterface obj, final Class destinationType, final Predicate<GraphObject> predicate) {
        // this is the default implementation
        return Collections.emptySet();
    }
}