org.hibernate.validator.internal.engine.path.PathImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bean-validator Show documentation
Show all versions of bean-validator Show documentation
JSR 380's RI, Hibernate Validator version ${hibernate-validator.version} and its dependencies repackaged as OSGi bundle
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or .
*/
package org.hibernate.validator.internal.engine.path;
import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ElementKind;
import javax.validation.Path;
import org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData;
import org.hibernate.validator.internal.util.Contracts;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
/**
* Default implementation of {@code javax.validation.Path}.
*
* @author Hardy Ferentschik
* @author Gunnar Morling
* @author Kevin Pollet <[email protected]> (C) 2011 SERLI
*/
public final class PathImpl implements Path, Serializable {
private static final long serialVersionUID = 7564511574909882392L;
private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
private static final String PROPERTY_PATH_SEPARATOR = ".";
/**
* Regular expression used to split a string path into its elements.
*
* @see Regular expression tester
*/
private static final String LEADING_PROPERTY_GROUP = "[^\\[\\.]+"; // everything up to a [ or .
private static final String OPTIONAL_INDEX_GROUP = "\\[(\\w*)\\]";
private static final String REMAINING_PROPERTY_STRING = "\\.(.*)"; // processed recursively
private static final Pattern PATH_PATTERN = Pattern.compile( "(" + LEADING_PROPERTY_GROUP + ")(" + OPTIONAL_INDEX_GROUP + ")?(" + REMAINING_PROPERTY_STRING + ")*" );
private static final int PROPERTY_NAME_GROUP = 1;
private static final int INDEXED_GROUP = 2;
private static final int INDEX_GROUP = 3;
private static final int REMAINING_STRING_GROUP = 5;
private List nodeList;
private boolean nodeListRequiresCopy;
private NodeImpl currentLeafNode;
private int hashCode;
/**
* Returns a {@code Path} instance representing the path described by the
* given string. To create a root node the empty string should be passed.
*
* @param propertyPath the path as string representation.
*
* @return a {@code Path} instance representing the path described by the
* given string.
*
* @throws IllegalArgumentException in case {@code property == null} or
* {@code property} cannot be parsed.
*/
public static PathImpl createPathFromString(String propertyPath) {
Contracts.assertNotNull( propertyPath, MESSAGES.propertyPathCannotBeNull() );
if ( propertyPath.length() == 0 ) {
return createRootPath();
}
return parseProperty( propertyPath );
}
public static PathImpl createPathForExecutable(ExecutableMetaData executable) {
Contracts.assertNotNull( executable, "A method is required to create a method return value path." );
PathImpl path = createRootPath();
if ( executable.getKind() == ElementKind.CONSTRUCTOR ) {
path.addConstructorNode( executable.getName(), executable.getParameterTypes() );
}
else {
path.addMethodNode( executable.getName(), executable.getParameterTypes() );
}
return path;
}
public static PathImpl createRootPath() {
PathImpl path = new PathImpl();
path.addBeanNode();
return path;
}
public static PathImpl createCopy(PathImpl path) {
return new PathImpl( path );
}
public boolean isRootPath() {
return nodeList.size() == 1 && nodeList.get( 0 ).getName() == null;
}
public PathImpl getPathWithoutLeafNode() {
return new PathImpl( nodeList.subList( 0, nodeList.size() - 1 ) );
}
public NodeImpl addPropertyNode(String nodeName) {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createPropertyNode( nodeName, parent );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl addContainerElementNode(String nodeName) {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createContainerElementNode( nodeName, parent );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl addParameterNode(String nodeName, int index) {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createParameterNode( nodeName, parent, index );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl addCrossParameterNode() {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createCrossParameterNode( parent );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl addBeanNode() {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createBeanNode( parent );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl addReturnValueNode() {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createReturnValue( parent );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
private NodeImpl addConstructorNode(String name, Class[] parameterTypes) {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createConstructorNode( name, parent, parameterTypes );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
private NodeImpl addMethodNode(String name, Class[] parameterTypes) {
requiresWriteableNodeList();
NodeImpl parent = currentLeafNode;
currentLeafNode = NodeImpl.createMethodNode( name, parent, parameterTypes );
nodeList.add( currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl makeLeafNodeIterable() {
requiresWriteableNodeList();
currentLeafNode = NodeImpl.makeIterable( currentLeafNode );
nodeList.set( nodeList.size() - 1, currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl makeLeafNodeIterableAndSetIndex(Integer index) {
requiresWriteableNodeList();
currentLeafNode = NodeImpl.makeIterableAndSetIndex( currentLeafNode, index );
nodeList.set( nodeList.size() - 1, currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl makeLeafNodeIterableAndSetMapKey(Object key) {
requiresWriteableNodeList();
currentLeafNode = NodeImpl.makeIterableAndSetMapKey( currentLeafNode, key );
nodeList.set( nodeList.size() - 1, currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public NodeImpl setLeafNodeValueIfRequired(Object value) {
// The value is only exposed for property and container element nodes
if ( currentLeafNode.getKind() == ElementKind.PROPERTY || currentLeafNode.getKind() == ElementKind.CONTAINER_ELEMENT ) {
requiresWriteableNodeList();
currentLeafNode = NodeImpl.setPropertyValue( currentLeafNode, value );
nodeList.set( nodeList.size() - 1, currentLeafNode );
// the property value is not part of the NodeImpl hashCode so we don't need to reset the PathImpl hashCode
}
return currentLeafNode;
}
public NodeImpl setLeafNodeTypeParameter(Class containerClass, Integer typeArgumentIndex) {
requiresWriteableNodeList();
currentLeafNode = NodeImpl.setTypeParameter( currentLeafNode, containerClass, typeArgumentIndex );
nodeList.set( nodeList.size() - 1, currentLeafNode );
resetHashCode();
return currentLeafNode;
}
public void removeLeafNode() {
if ( !nodeList.isEmpty() ) {
requiresWriteableNodeList();
nodeList.remove( nodeList.size() - 1 );
currentLeafNode = nodeList.isEmpty() ? null : (NodeImpl) nodeList.get( nodeList.size() - 1 );
resetHashCode();
}
}
public NodeImpl getLeafNode() {
return currentLeafNode;
}
@Override
public Iterator iterator() {
if ( nodeList.size() == 0 ) {
return Collections.emptyList().iterator();
}
if ( nodeList.size() == 1 ) {
return nodeList.iterator();
}
return nodeList.subList( 1, nodeList.size() ).iterator();
}
public String asString() {
StringBuilder builder = new StringBuilder();
boolean first = true;
for ( int i = 1; i < nodeList.size(); i++ ) {
NodeImpl nodeImpl = (NodeImpl) nodeList.get( i );
String name = nodeImpl.asString();
if ( name.isEmpty() ) {
// skip the node if it does not contribute to the string representation of the path, eg class level constraints
continue;
}
if ( !first ) {
builder.append( PROPERTY_PATH_SEPARATOR );
}
builder.append( nodeImpl.asString() );
first = false;
}
return builder.toString();
}
private void requiresWriteableNodeList() {
if ( !nodeListRequiresCopy ) {
return;
}
// Usually, the write operation is about adding one more node, so let's make the list one element larger.
List newNodeList = new ArrayList<>( nodeList.size() + 1 );
newNodeList.addAll( nodeList );
nodeList = newNodeList;
nodeListRequiresCopy = false;
}
@Override
public String toString() {
return asString();
}
@Override
public boolean equals(Object obj) {
if ( this == obj ) {
return true;
}
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
PathImpl other = (PathImpl) obj;
if ( nodeList == null ) {
if ( other.nodeList != null ) {
return false;
}
}
else if ( !nodeList.equals( other.nodeList ) ) {
return false;
}
return true;
}
@Override
// deferred hash code building
public int hashCode() {
if ( hashCode == -1 ) {
hashCode = buildHashCode();
}
return hashCode;
}
private int buildHashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ( ( nodeList == null ) ? 0 : nodeList.hashCode() );
return result;
}
/**
* Copy constructor.
*
* @param path the path to make a copy of.
*/
private PathImpl(PathImpl path) {
nodeList = path.nodeList;
currentLeafNode = path.currentLeafNode;
hashCode = path.hashCode;
nodeListRequiresCopy = true;
}
private PathImpl() {
nodeList = new ArrayList<>( 1 );
hashCode = -1;
nodeListRequiresCopy = false;
}
private PathImpl(List nodeList) {
this.nodeList = nodeList;
currentLeafNode = (NodeImpl) nodeList.get( nodeList.size() - 1 );
hashCode = -1;
nodeListRequiresCopy = true;
}
private void resetHashCode() {
hashCode = -1;
}
private static PathImpl parseProperty(String propertyName) {
PathImpl path = createRootPath();
String tmp = propertyName;
do {
Matcher matcher = PATH_PATTERN.matcher( tmp );
if ( matcher.matches() ) {
String value = matcher.group( PROPERTY_NAME_GROUP );
if ( !isValidJavaIdentifier( value ) ) {
throw LOG.getInvalidJavaIdentifierException( value );
}
// create the node
path.addPropertyNode( value );
// is the node indexable
if ( matcher.group( INDEXED_GROUP ) != null ) {
path.makeLeafNodeIterable();
}
// take care of the index/key if one exists
String indexOrKey = matcher.group( INDEX_GROUP );
if ( indexOrKey != null && indexOrKey.length() > 0 ) {
try {
Integer i = Integer.parseInt( indexOrKey );
path.makeLeafNodeIterableAndSetIndex( i );
}
catch (NumberFormatException e) {
path.makeLeafNodeIterableAndSetMapKey( indexOrKey );
}
}
// match the remaining string
tmp = matcher.group( REMAINING_STRING_GROUP );
}
else {
throw LOG.getUnableToParsePropertyPathException( propertyName );
}
} while ( tmp != null );
if ( path.getLeafNode().isIterable() ) {
path.addBeanNode();
}
return path;
}
/**
* Validate that the given identifier is a valid Java identifier according to the Java Language Specification,
* chapter 3.8
*
* @param identifier string identifier to validate
*
* @return true if the given identifier is a valid Java Identifier
*
* @throws IllegalArgumentException if the given identifier is {@code null}
*/
private static boolean isValidJavaIdentifier(String identifier) {
Contracts.assertNotNull( identifier, "identifier param cannot be null" );
if ( identifier.length() == 0 || !Character.isJavaIdentifierStart( (int) identifier.charAt( 0 ) ) ) {
return false;
}
for ( int i = 1; i < identifier.length(); i++ ) {
if ( !Character.isJavaIdentifierPart( (int) identifier.charAt( i ) ) ) {
return false;
}
}
return true;
}
}