1 /*
2  * Hibernate Validator, declare and validate application constraints
3  *
4  * License: Apache License, Version 2.0
5  * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
6  */

7 package org.hibernate.validator.internal.engine.path;
8
9 import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;
10
11 import java.io.Serializable;
12 import java.lang.invoke.MethodHandles;
13 import java.util.ArrayList;
14 import java.util.Collections;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.regex.Matcher;
18 import java.util.regex.Pattern;
19
20 import javax.validation.ElementKind;
21 import javax.validation.Path;
22
23 import org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData;
24 import org.hibernate.validator.internal.util.Contracts;
25 import org.hibernate.validator.internal.util.logging.Log;
26 import org.hibernate.validator.internal.util.logging.LoggerFactory;
27
28 /**
29  * Default implementation of {@code javax.validation.Path}.
30  *
31  * @author Hardy Ferentschik
32  * @author Gunnar Morling
33  * @author Kevin Pollet &lt;kevin.pollet@serli.com&gt; (C) 2011 SERLI
34  */

35 public final class PathImpl implements Path, Serializable {
36     private static final long serialVersionUID = 7564511574909882392L;
37     private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
38
39     private static final String PROPERTY_PATH_SEPARATOR = ".";
40
41     /**
42      * Regular expression used to split a string path into its elements.
43      *
44      * @see <a href="http://www.regexplanet.com/simple/index.jsp">Regular expression tester</a>
45      */

46     private static final String LEADING_PROPERTY_GROUP = "[^\\[\\.]+";  // everything up to a [ or .
47     private static final String OPTIONAL_INDEX_GROUP = "\\[(\\w*)\\]";
48     private static final String REMAINING_PROPERTY_STRING = "\\.(.*)";  // processed recursively
49
50     private static final Pattern PATH_PATTERN = Pattern.compile( "(" + LEADING_PROPERTY_GROUP + ")(" + OPTIONAL_INDEX_GROUP + ")?(" + REMAINING_PROPERTY_STRING + ")*" );
51     private static final int PROPERTY_NAME_GROUP = 1;
52     private static final int INDEXED_GROUP = 2;
53     private static final int INDEX_GROUP = 3;
54     private static final int REMAINING_STRING_GROUP = 5;
55
56     private List<Node> nodeList;
57     private boolean nodeListRequiresCopy;
58     private NodeImpl currentLeafNode;
59     private int hashCode;
60
61     /**
62      * Returns a {@code Path} instance representing the path described by the
63      * given string. To create a root node the empty string should be passed.
64      *
65      * @param propertyPath the path as string representation.
66      *
67      * @return a {@code Path} instance representing the path described by the
68      *         given string.
69      *
70      * @throws IllegalArgumentException in case {@code property == null} or
71      * {@code property} cannot be parsed.
72      */

73     public static PathImpl createPathFromString(String propertyPath) {
74         Contracts.assertNotNull( propertyPath, MESSAGES.propertyPathCannotBeNull() );
75
76         if ( propertyPath.length() == 0 ) {
77             return createRootPath();
78         }
79
80         return parseProperty( propertyPath );
81     }
82
83     public static PathImpl createPathForExecutable(ExecutableMetaData executable) {
84         Contracts.assertNotNull( executable, "A method is required to create a method return value path." );
85
86         PathImpl path = createRootPath();
87
88         if ( executable.getKind() == ElementKind.CONSTRUCTOR ) {
89             path.addConstructorNode( executable.getName(), executable.getParameterTypes() );
90         }
91         else {
92             path.addMethodNode( executable.getName(), executable.getParameterTypes() );
93         }
94
95         return path;
96     }
97
98     public static PathImpl createRootPath() {
99         PathImpl path = new PathImpl();
100         path.addBeanNode();
101         return path;
102     }
103
104     public static PathImpl createCopy(PathImpl path) {
105         return new PathImpl( path );
106     }
107
108     public static PathImpl createCopyWithoutLeafNode(PathImpl path) {
109         return new PathImpl( path.nodeList.subList( 0, path.nodeList.size() - 1 ) );
110     }
111
112
113     public boolean isRootPath() {
114         return nodeList.size() == 1 && nodeList.get( 0 ).getName() == null;
115     }
116
117     public NodeImpl addPropertyNode(String nodeName) {
118         requiresWriteableNodeList();
119
120         NodeImpl parent = currentLeafNode;
121         currentLeafNode = NodeImpl.createPropertyNode( nodeName, parent );
122         nodeList.add( currentLeafNode );
123         resetHashCode();
124         return currentLeafNode;
125     }
126
127     public NodeImpl addContainerElementNode(String nodeName) {
128         requiresWriteableNodeList();
129
130         NodeImpl parent = currentLeafNode;
131         currentLeafNode = NodeImpl.createContainerElementNode( nodeName, parent );
132         nodeList.add( currentLeafNode );
133         resetHashCode();
134         return currentLeafNode;
135     }
136
137     public NodeImpl addParameterNode(String nodeName, int index) {
138         requiresWriteableNodeList();
139
140         NodeImpl parent = currentLeafNode;
141         currentLeafNode = NodeImpl.createParameterNode( nodeName, parent, index );
142         nodeList.add( currentLeafNode );
143         resetHashCode();
144         return currentLeafNode;
145     }
146
147     public NodeImpl addCrossParameterNode() {
148         requiresWriteableNodeList();
149
150         NodeImpl parent = currentLeafNode;
151         currentLeafNode = NodeImpl.createCrossParameterNode( parent );
152         nodeList.add( currentLeafNode );
153         resetHashCode();
154         return currentLeafNode;
155     }
156
157     public NodeImpl addBeanNode() {
158         requiresWriteableNodeList();
159
160         NodeImpl parent = currentLeafNode;
161         currentLeafNode = NodeImpl.createBeanNode( parent );
162         nodeList.add( currentLeafNode );
163         resetHashCode();
164         return currentLeafNode;
165     }
166
167     public NodeImpl addReturnValueNode() {
168         requiresWriteableNodeList();
169
170         NodeImpl parent = currentLeafNode;
171         currentLeafNode = NodeImpl.createReturnValue( parent );
172         nodeList.add( currentLeafNode );
173         resetHashCode();
174         return currentLeafNode;
175     }
176
177     private NodeImpl addConstructorNode(String name, Class<?>[] parameterTypes) {
178         requiresWriteableNodeList();
179
180         NodeImpl parent = currentLeafNode;
181         currentLeafNode = NodeImpl.createConstructorNode( name, parent, parameterTypes );
182         nodeList.add( currentLeafNode );
183         resetHashCode();
184         return currentLeafNode;
185     }
186
187     private NodeImpl addMethodNode(String name, Class<?>[] parameterTypes) {
188         requiresWriteableNodeList();
189
190         NodeImpl parent = currentLeafNode;
191         currentLeafNode = NodeImpl.createMethodNode( name, parent, parameterTypes );
192         nodeList.add( currentLeafNode );
193         resetHashCode();
194         return currentLeafNode;
195     }
196
197     public NodeImpl makeLeafNodeIterable() {
198         requiresWriteableNodeList();
199
200         currentLeafNode = NodeImpl.makeIterable( currentLeafNode );
201
202         nodeList.set( nodeList.size() - 1, currentLeafNode );
203         resetHashCode();
204         return currentLeafNode;
205     }
206
207     public NodeImpl makeLeafNodeIterableAndSetIndex(Integer index) {
208         requiresWriteableNodeList();
209
210         currentLeafNode = NodeImpl.makeIterableAndSetIndex( currentLeafNode, index );
211
212         nodeList.set( nodeList.size() - 1, currentLeafNode );
213         resetHashCode();
214         return currentLeafNode;
215     }
216
217     public NodeImpl makeLeafNodeIterableAndSetMapKey(Object key) {
218         requiresWriteableNodeList();
219
220         currentLeafNode = NodeImpl.makeIterableAndSetMapKey( currentLeafNode, key );
221
222         nodeList.set( nodeList.size() - 1, currentLeafNode );
223         resetHashCode();
224         return currentLeafNode;
225     }
226
227     public NodeImpl setLeafNodeValueIfRequired(Object value) {
228         // The value is only exposed for property and container element nodes
229         if ( currentLeafNode.getKind() == ElementKind.PROPERTY || currentLeafNode.getKind() == ElementKind.CONTAINER_ELEMENT ) {
230             requiresWriteableNodeList();
231
232             currentLeafNode = NodeImpl.setPropertyValue( currentLeafNode, value );
233
234             nodeList.set( nodeList.size() - 1, currentLeafNode );
235
236             // the property value is not part of the NodeImpl hashCode so we don't need to reset the PathImpl hashCode
237         }
238         return currentLeafNode;
239     }
240
241     public NodeImpl setLeafNodeTypeParameter(Class<?> containerClass, Integer typeArgumentIndex) {
242         requiresWriteableNodeList();
243
244         currentLeafNode = NodeImpl.setTypeParameter( currentLeafNode, containerClass, typeArgumentIndex );
245
246         nodeList.set( nodeList.size() - 1, currentLeafNode );
247         resetHashCode();
248         return currentLeafNode;
249     }
250
251     public void removeLeafNode() {
252         if ( !nodeList.isEmpty() ) {
253             requiresWriteableNodeList();
254
255             nodeList.remove( nodeList.size() - 1 );
256             currentLeafNode = nodeList.isEmpty() ? null : (NodeImpl) nodeList.get( nodeList.size() - 1 );
257             resetHashCode();
258         }
259     }
260
261     public NodeImpl getLeafNode() {
262         return currentLeafNode;
263     }
264
265     @Override
266     public Iterator<Path.Node> iterator() {
267         if ( nodeList.size() == 0 ) {
268             return Collections.<Path.Node>emptyList().iterator();
269         }
270         if ( nodeList.size() == 1 ) {
271             return nodeList.iterator();
272         }
273         return nodeList.subList( 1, nodeList.size() ).iterator();
274     }
275
276     public String asString() {
277         StringBuilder builder = new StringBuilder();
278         boolean first = true;
279         for ( int i = 1; i < nodeList.size(); i++ ) {
280             NodeImpl nodeImpl = (NodeImpl) nodeList.get( i );
281             String name = nodeImpl.asString();
282             if ( name.isEmpty() ) {
283                 // skip the node if it does not contribute to the string representation of the path, eg class level constraints
284                 continue;
285             }
286
287             if ( !first ) {
288                 builder.append( PROPERTY_PATH_SEPARATOR );
289             }
290
291             builder.append( nodeImpl.asString() );
292
293             first = false;
294         }
295         return builder.toString();
296     }
297
298     private void requiresWriteableNodeList() {
299         if ( !nodeListRequiresCopy ) {
300             return;
301         }
302
303         // Usually, the write operation is about adding one more node, so let's make the list one element larger.
304         List<Node> newNodeList = new ArrayList<>( nodeList.size() + 1 );
305         newNodeList.addAll( nodeList );
306         nodeList = newNodeList;
307         nodeListRequiresCopy = false;
308     }
309
310     @Override
311     public String toString() {
312         return asString();
313     }
314
315     @Override
316     public boolean equals(Object obj) {
317         if ( this == obj ) {
318             return true;
319         }
320         if ( obj == null ) {
321             return false;
322         }
323         if ( getClass() != obj.getClass() ) {
324             return false;
325         }
326         PathImpl other = (PathImpl) obj;
327         if ( nodeList == null ) {
328             if ( other.nodeList != null ) {
329                 return false;
330             }
331         }
332         else if ( !nodeList.equals( other.nodeList ) ) {
333             return false;
334         }
335         return true;
336     }
337
338     @Override
339     // deferred hash code building
340     public int hashCode() {
341         if ( hashCode == -1 ) {
342             hashCode = buildHashCode();
343         }
344
345         return hashCode;
346     }
347
348     private int buildHashCode() {
349         final int prime = 31;
350         int result = 1;
351         result = prime * result
352                 + ( ( nodeList == null ) ? 0 : nodeList.hashCode() );
353         return result;
354     }
355
356     /**
357      * Copy constructor.
358      *
359      * @param path the path to make a copy of.
360      */

361     private PathImpl(PathImpl path) {
362         nodeList = path.nodeList;
363         currentLeafNode = path.currentLeafNode;
364         hashCode = path.hashCode;
365         nodeListRequiresCopy = true;
366     }
367
368     private PathImpl() {
369         nodeList = new ArrayList<>( 1 );
370         hashCode = -1;
371         nodeListRequiresCopy = false;
372     }
373
374     private PathImpl(List<Node> nodeList) {
375         this.nodeList = nodeList;
376         currentLeafNode = (NodeImpl) nodeList.get( nodeList.size() - 1 );
377         hashCode = -1;
378         nodeListRequiresCopy = true;
379     }
380
381     private void resetHashCode() {
382         hashCode = -1;
383     }
384
385     private static PathImpl parseProperty(String propertyName) {
386         PathImpl path = createRootPath();
387         String tmp = propertyName;
388         do {
389             Matcher matcher = PATH_PATTERN.matcher( tmp );
390             if ( matcher.matches() ) {
391
392                 String value = matcher.group( PROPERTY_NAME_GROUP );
393                 if ( !isValidJavaIdentifier( value ) ) {
394                     throw LOG.getInvalidJavaIdentifierException( value );
395                 }
396
397                 // create the node
398                 path.addPropertyNode( value );
399
400                 // is the node indexable
401                 if ( matcher.group( INDEXED_GROUP ) != null ) {
402                     path.makeLeafNodeIterable();
403                 }
404
405                 // take care of the index/key if one exists
406                 String indexOrKey = matcher.group( INDEX_GROUP );
407                 if ( indexOrKey != null && indexOrKey.length() > 0 ) {
408                     try {
409                         Integer i = Integer.parseInt( indexOrKey );
410                         path.makeLeafNodeIterableAndSetIndex( i );
411                     }
412                     catch (NumberFormatException e) {
413                         path.makeLeafNodeIterableAndSetMapKey( indexOrKey );
414                     }
415                 }
416
417                 // match the remaining string
418                 tmp = matcher.group( REMAINING_STRING_GROUP );
419             }
420             else {
421                 throw LOG.getUnableToParsePropertyPathException( propertyName );
422             }
423         } while ( tmp != null );
424
425         if ( path.getLeafNode().isIterable() ) {
426             path.addBeanNode();
427         }
428
429         return path;
430     }
431
432     /**
433      * Validate that the given identifier is a valid Java identifier according to the Java Language Specification,
434      * <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8">chapter 3.8</a>
435      *
436      * @param identifier string identifier to validate
437      *
438      * @return true if the given identifier is a valid Java Identifier
439      *
440      * @throws IllegalArgumentException if the given identifier is {@code null}
441      */

442     private static boolean isValidJavaIdentifier(String identifier) {
443         Contracts.assertNotNull( identifier, "identifier param cannot be null" );
444
445         if ( identifier.length() == 0 || !Character.isJavaIdentifierStart( (int) identifier.charAt( 0 ) ) ) {
446             return false;
447         }
448
449         for ( int i = 1; i < identifier.length(); i++ ) {
450             if ( !Character.isJavaIdentifierPart( (int) identifier.charAt( i ) ) ) {
451                 return false;
452             }
453         }
454         return true;
455     }
456 }
457