/**
* Copyright (C) 2010-2017 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.common;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.common.error.EmptyPropertyToken;
import org.structr.common.error.ErrorBuffer;
import org.structr.common.error.FrameworkException;
import org.structr.common.error.MatchToken;
import org.structr.common.error.RangeToken;
import org.structr.common.error.TooShortToken;
import org.structr.common.error.UniqueToken;
import org.structr.core.GraphObject;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.RelationshipInterface;
import org.structr.core.property.GenericProperty;
import org.structr.core.property.PropertyKey;
/**
* Defines helper methods for property validation.
*
*
*/
public class ValidationHelper {
private static final Logger logger = LoggerFactory.getLogger(ValidationHelper.class.getName());
private static final PropertyKey UnknownType = new GenericProperty("unknown type");
// ----- public static methods -----
/**
* Checks whether the value for the given property key of the given node
* has at least the given length.
*
* @param node the node
* @param key the property key whose value should be checked
* @param minLength the min length
* @param errorBuffer the error buffer
*
* @return true if the condition is valid
*/
public static boolean isValidStringMinLength(final GraphObject node, final PropertyKey<String> key, final int minLength, final ErrorBuffer errorBuffer) {
String value = node.getProperty(key);
String type = node.getType();
if (StringUtils.isNotBlank(value)) {
if (value.length() >= minLength) {
return true;
}
errorBuffer.add(new TooShortToken(type, key, minLength));
return false;
}
errorBuffer.add(new EmptyPropertyToken(type, key));
return false;
}
/**
* Checks whether the value for the given property key of the given node
* is a non-empty string.
*
* @param node the node
* @param key the property key
* @param errorBuffer the error buffer
*
* @return true if the condition is valid
*/
public static boolean isValidStringNotBlank(final GraphObject node, final PropertyKey<String> key, final ErrorBuffer errorBuffer) {
if (StringUtils.isNotBlank(node.getProperty(key))) {
return true;
}
errorBuffer.add(new EmptyPropertyToken(node.getType(), key));
return false;
}
/**
* Checks whether the value for the given property key of the given node
* is a non-empty string.
*
* @param node the node
* @param key the property key
* @param errorBuffer the error buffer
*
* @return true if the condition is valid
*/
public static boolean isValidPropertyNotNull(final GraphObject node, final PropertyKey key, final ErrorBuffer errorBuffer) {
final String type = node.getType();
if (key == null) {
errorBuffer.add(new EmptyPropertyToken(type, UnknownType));
return false;
}
final Object value = node.getProperty(key);
if (value != null) {
if (value instanceof Iterable) {
if (((Iterable) value).iterator().hasNext()) {
return true;
}
} else {
return true;
}
}
errorBuffer.add(new EmptyPropertyToken(type, key));
return false;
}
/**
* Checks whether the value of the given property key of the given node
* if not null and matches the given regular expression.
*
* @param node
* @param key
* @param expression
* @param errorBuffer
* @return true if string matches expression
*/
public static boolean isValidStringMatchingRegex(final GraphObject node, final PropertyKey<String> key, final String expression, final ErrorBuffer errorBuffer) {
final String value = node.getProperty(key);
if (value != null && value.matches(expression)) {
return true;
}
// no match
errorBuffer.add(new MatchToken(node.getType(), key, expression));
return false;
}
public static boolean isValidIntegerInRange(final GraphObject node, final PropertyKey<Integer> key, final String range, final ErrorBuffer errorBuffer) {
// we expect expression to have the following format:
// - "[" or "]" followed by a number (including negative values
// - a comma (must exist)
// - a number (including negative values followed by "[" or "]"
final int length = range.length();
final String leftBound = range.substring(0, 1);
final String rightBound = range.substring(length-1, length);
final String[] parts = range.substring(1, length-1).split(",+");
final String type = node.getType();
if (parts.length == 2) {
final String leftPart = parts[0].trim();
final String rightPart = parts[1].trim();
final int left = Integer.parseInt(leftPart);
final int right = Integer.parseInt(rightPart);
final Integer value = node.getProperty(key);
// do not check for non-null values, ignore (silently succeed)
if (value != null) {
// result
boolean inRange = true;
if ("[".equals(leftBound)) {
inRange &= (value >= left);
} else {
inRange &= (value > left);
}
if ("]".equals(rightBound)) {
inRange &= (value <= right);
} else {
inRange &= (value < right);
}
if (!inRange) {
errorBuffer.add(new RangeToken(type, key, range));
}
return inRange;
}
}
// no error
return true;
}
public static boolean isValidIntegerArrayInRange(final GraphObject node, final PropertyKey<Integer[]> key, final String range, final ErrorBuffer errorBuffer) {
// we expect expression to have the following format:
// - "[" or "]" followed by a number (including negative values
// - a comma (must exist)
// - a number (including negative values followed by "[" or "]"
final int length = range.length();
final String leftBound = range.substring(0, 1);
final String rightBound = range.substring(length-1, length);
final String[] parts = range.substring(1, length-1).split(",+");
final String type = node.getType();
if (parts.length == 2) {
final String leftPart = parts[0].trim();
final String rightPart = parts[1].trim();
final int left = Integer.parseInt(leftPart);
final int right = Integer.parseInt(rightPart);
final Integer[] values = node.getProperty(key);
// do not check for non-null values, ignore (silently succeed)
if (values != null) {
// result
boolean inRange = true;
for (final Integer value : values) {
if ("[".equals(leftBound)) {
inRange &= (value >= left);
} else {
inRange &= (value > left);
}
if ("]".equals(rightBound)) {
inRange &= (value <= right);
} else {
inRange &= (value < right);
}
}
if (!inRange) {
errorBuffer.add(new RangeToken(type, key, range));
}
return inRange;
}
}
// no error
return true;
}
public static boolean isValidLongInRange(final GraphObject node, final PropertyKey<Long> key, final String range, final ErrorBuffer errorBuffer) {
// we expect expression to have the following format:
// - "[" or "]" followed by a number (including negative values
// - a comma (must exist)
// - a number (including negative values followed by "[" or "]"
final int length = range.length();
final String leftBound = range.substring(0, 1);
final String rightBound = range.substring(length-1, length);
final String[] parts = range.substring(1, length-1).split(",+");
final String type = node.getType();
if (parts.length == 2) {
final String leftPart = parts[0].trim();
final String rightPart = parts[1].trim();
final long left = Long.parseLong(leftPart);
final long right = Long.parseLong(rightPart);
final Long value = node.getProperty(key);
// do not check for non-null values, ignore (silently succeed)
if (value != null) {
// result
boolean inRange = true;
if ("[".equals(leftBound)) {
inRange &= (value >= left);
} else {
inRange &= (value > left);
}
if ("]".equals(rightBound)) {
inRange &= (value <= right);
} else {
inRange &= (value < right);
}
if (!inRange) {
errorBuffer.add(new RangeToken(type, key, range));
}
return inRange;
}
}
// no error
return true;
}
public static boolean isValidLongArrayInRange(final GraphObject node, final PropertyKey<Long[]> key, final String range, final ErrorBuffer errorBuffer) {
// we expect expression to have the following format:
// - "[" or "]" followed by a number (including negative values
// - a comma (must exist)
// - a number (including negative values followed by "[" or "]"
final int length = range.length();
final String leftBound = range.substring(0, 1);
final String rightBound = range.substring(length-1, length);
final String[] parts = range.substring(1, length-1).split(",+");
final String type = node.getType();
if (parts.length == 2) {
final String leftPart = parts[0].trim();
final String rightPart = parts[1].trim();
final long left = Long.parseLong(leftPart);
final long right = Long.parseLong(rightPart);
final Long[] values = node.getProperty(key);
// do not check for non-null values, ignore (silently succeed)
if (values != null) {
// result
boolean inRange = true;
for (final Long value : values) {
if ("[".equals(leftBound)) {
inRange &= (value >= left);
} else {
inRange &= (value > left);
}
if ("]".equals(rightBound)) {
inRange &= (value <= right);
} else {
inRange &= (value < right);
}
}
if (!inRange) {
errorBuffer.add(new RangeToken(type, key, range));
}
return inRange;
}
}
// no error
return true;
}
public static boolean isValidDoubleInRange(final GraphObject node, final PropertyKey<Double> key, final String range, final ErrorBuffer errorBuffer) {
// we expect expression to have the following format:
// - "[" or "]" followed by a number (including negative values
// - a comma (must exist)
// - a number (including negative values followed by "[" or "]"
final int length = range.length();
final String leftBound = range.substring(0, 1);
final String rightBound = range.substring(length-1, length);
final String[] parts = range.substring(1, length-1).split(",+");
final String type = node.getType();
if (parts.length == 2) {
final String leftPart = parts[0].trim();
final String rightPart = parts[1].trim();
final double left = Double.parseDouble(leftPart);
final double right = Double.parseDouble(rightPart);
final Double value = node.getProperty(key);
// do not check for non-null values, ignore (silently succeed)
if (value != null) {
// result
boolean inRange = true;
if ("[".equals(leftBound)) {
inRange &= (value >= left);
} else {
inRange &= (value > left);
}
if ("]".equals(rightBound)) {
inRange &= (value <= right);
} else {
inRange &= (value < right);
}
if (!inRange) {
errorBuffer.add(new RangeToken(type, key, range));
}
return inRange;
}
}
// no error
return true;
}
public static boolean isValidDoubleArrayInRange(final GraphObject node, final PropertyKey<Double[]> key, final String range, final ErrorBuffer errorBuffer) {
// we expect expression to have the following format:
// - "[" or "]" followed by a number (including negative values
// - a comma (must exist)
// - a number (including negative values followed by "[" or "]"
final int length = range.length();
final String leftBound = range.substring(0, 1);
final String rightBound = range.substring(length-1, length);
final String[] parts = range.substring(1, length-1).split(",+");
final String type = node.getType();
if (parts.length == 2) {
final String leftPart = parts[0].trim();
final String rightPart = parts[1].trim();
final double left = Double.parseDouble(leftPart);
final double right = Double.parseDouble(rightPart);
final Double[] values = node.getProperty(key);
// do not check for non-null values, ignore (silently succeed)
if (values != null) {
// result
boolean inRange = true;
for (final Double value : values) {
if ("[".equals(leftBound)) {
inRange &= (value >= left);
} else {
inRange &= (value > left);
}
if ("]".equals(rightBound)) {
inRange &= (value <= right);
} else {
inRange &= (value < right);
}
}
if (!inRange) {
errorBuffer.add(new RangeToken(type, key, range));
}
return inRange;
}
}
// no error
return true;
}
public static synchronized boolean isValidUniqueProperty(final GraphObject object, final PropertyKey key, final ErrorBuffer errorBuffer) {
if (key != null) {
final Object value = object.getProperty(key);
if (value != null) {
// validation will only be executed for non-null values
List<GraphObject> result = null;
// use declaring class for inheritance-aware uniqueness
Class type = key.getDeclaringClass();
if (type == null || (AbstractNode.name.equals(key) && NodeInterface.class.equals(type))) {
// fallback: object type
type = object.getClass();
}
try {
if (object instanceof NodeInterface) {
result = StructrApp.getInstance()
.nodeQuery(type)
.and(key, value)
.sortDescending(GraphObject.createdDate)
.getAsList();
} else {
result = StructrApp.getInstance()
.relationshipQuery(type)
.and(key, value)
.sortDescending(GraphObject.createdDate)
.getAsList();
}
} catch (FrameworkException fex) {
logger.warn("", fex);
}
/* This validation code runs at the end of a transaction, so if there
* is a constraint violation, there are at least two different nodes
* with the same value for the unique key. At this point, we don't
* know which node we are currently examining, so we sort by creation
* date (ascending order) and look at the first node of the result
* list. We want the validation code to fail for all constraint
* violating nodes that are older than the first node.
*/
for (final GraphObject foundNode : result) {
if (foundNode.getId() != object.getId()) {
// validation is aborted when the first validation failure occurs, so
// we can assume that the object currently exmained is the first
// existing object, hence all others get the error message with the
// UUID of the first one.
errorBuffer.add(new UniqueToken(object.getType(), key, object.getUuid()));
// error!
return false;
}
}
}
}
// no error
return true;
}
public static synchronized boolean isValidGloballyUniqueProperty(final GraphObject object, final PropertyKey key, final ErrorBuffer errorBuffer) {
if (key != null) {
List<? extends GraphObject> result = null;
final Object value = object.getProperty(key);
try {
if (object instanceof NodeInterface) {
result = StructrApp.getInstance()
.nodeQuery()
.and(key, value)
.sortDescending(GraphObject.createdDate)
.getAsList();
} else if (object instanceof RelationshipInterface) {
result = StructrApp.getInstance()
.relationshipQuery()
.and(key, value)
.sortDescending(GraphObject.createdDate)
.getAsList();
} else {
logger.error("GraphObject is neither NodeInterface nor RelationshipInterface");
return false;
}
} catch (FrameworkException fex) {
logger.warn("Unable to fetch list of nodes for uniqueness check", fex);
// handle error
}
for (final GraphObject foundNode : result) {
if (foundNode.getId() != object.getId()) {
// validation is aborted when the first validation failure occurs, so
// we can assume that the object currently exmained is the first
// existing object, hence all others get the error message with the
// UUID of the first one.
errorBuffer.add(new UniqueToken(object.getType(), key, object.getUuid()));
// error!
return false;
}
}
}
// no error
return true;
}
}