/*
 * Copyright 2011-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.mapping;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Value;

import java.beans.Introspector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;

Abstraction of a PropertyPath of a domain class.
Author:Oliver Gierke, Christoph Strobl, Mark Paluch, Mariusz Mączkowski
/** * Abstraction of a {@link PropertyPath} of a domain class. * * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch * @author Mariusz Mączkowski */
@EqualsAndHashCode public class PropertyPath implements Streamable<PropertyPath> { private static final String PARSE_DEPTH_EXCEEDED = "Trying to parse a path with depth greater than 1000! This has been disabled for security reasons to prevent parsing overflows."; private static final String DELIMITERS = "_\\."; private static final String ALL_UPPERCASE = "[A-Z0-9._$]+"; private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS)); private static final Pattern SPLITTER_FOR_QUOTED = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", "\\.")); private static final Map<Key, PropertyPath> CACHE = new ConcurrentReferenceHashMap<>(); private final TypeInformation<?> owningType; private final String name; private final @Getter TypeInformation<?> typeInformation; private final TypeInformation<?> actualTypeInformation; private final boolean isCollection; private @Nullable PropertyPath next;
Creates a leaf PropertyPath (no nested ones) with the given name inside the given owning type.
Params:
  • name – must not be null or empty.
  • owningType – must not be null.
/** * Creates a leaf {@link PropertyPath} (no nested ones) with the given name inside the given owning type. * * @param name must not be {@literal null} or empty. * @param owningType must not be {@literal null}. */
PropertyPath(String name, Class<?> owningType) { this(name, ClassTypeInformation.from(owningType), Collections.emptyList()); }
Creates a leaf PropertyPath (no nested ones with the given name and owning type.
Params:
  • name – must not be null or empty.
  • owningType – must not be null.
  • base – the PropertyPath previously found.
/** * Creates a leaf {@link PropertyPath} (no nested ones with the given name and owning type. * * @param name must not be {@literal null} or empty. * @param owningType must not be {@literal null}. * @param base the {@link PropertyPath} previously found. */
PropertyPath(String name, TypeInformation<?> owningType, List<PropertyPath> base) { Assert.hasText(name, "Name must not be null or empty!"); Assert.notNull(owningType, "Owning type must not be null!"); Assert.notNull(base, "Perviously found properties must not be null!"); String propertyName = Introspector.decapitalize(name); TypeInformation<?> propertyType = owningType.getProperty(propertyName); if (propertyType == null) { throw new PropertyReferenceException(propertyName, owningType, base); } this.owningType = owningType; this.typeInformation = propertyType; this.isCollection = propertyType.isCollectionLike(); this.name = propertyName; this.actualTypeInformation = propertyType.getActualType() == null ? propertyType : propertyType.getRequiredActualType(); }
Returns the owning type of the PropertyPath.
Returns:the owningType will never be null.
/** * Returns the owning type of the {@link PropertyPath}. * * @return the owningType will never be {@literal null}. */
public TypeInformation<?> getOwningType() { return owningType; }
Returns the name of the PropertyPath.
Returns:the name will never be null.
/** * Returns the name of the {@link PropertyPath}. * * @return the name will never be {@literal null}. */
public String getSegment() { return name; }
Returns the leaf property of the PropertyPath.
Returns:will never be null.
/** * Returns the leaf property of the {@link PropertyPath}. * * @return will never be {@literal null}. */
public PropertyPath getLeafProperty() { PropertyPath result = this; while (result.hasNext()) { result = result.requiredNext(); } return result; }
Returns the type of the leaf property of the current PropertyPath.
Returns:will never be null.
/** * Returns the type of the leaf property of the current {@link PropertyPath}. * * @return will never be {@literal null}. */
public Class<?> getLeafType() { return getLeafProperty().getType(); }
Returns the type of the property will return the plain resolved type for simple properties, the component type for any Iterable or the value type of a Map if the property is one.
Returns:
/** * Returns the type of the property will return the plain resolved type for simple properties, the component type for * any {@link Iterable} or the value type of a {@link java.util.Map} if the property is one. * * @return */
public Class<?> getType() { return this.actualTypeInformation.getType(); }
Returns the next nested PropertyPath.
See Also:
Returns:the next nested PropertyPath or null if no nested PropertyPath available.
/** * Returns the next nested {@link PropertyPath}. * * @return the next nested {@link PropertyPath} or {@literal null} if no nested {@link PropertyPath} available. * @see #hasNext() */
@Nullable public PropertyPath next() { return next; }
Returns whether there is a nested PropertyPath. If this returns true you can expect next() to return a non- null value.
Returns:
/** * Returns whether there is a nested {@link PropertyPath}. If this returns {@literal true} you can expect * {@link #next()} to return a non- {@literal null} value. * * @return */
public boolean hasNext() { return next != null; }
Returns the PropertyPath in dot notation.
Returns:
/** * Returns the {@link PropertyPath} in dot notation. * * @return */
public String toDotPath() { if (hasNext()) { return getSegment() + "." + requiredNext().toDotPath(); } return getSegment(); }
Returns whether the PropertyPath is actually a collection.
Returns:
/** * Returns whether the {@link PropertyPath} is actually a collection. * * @return */
public boolean isCollection() { return isCollection; }
Returns the PropertyPath for the path nested under the current property.
Params:
  • path – must not be null or empty.
Returns:will never be null.
/** * Returns the {@link PropertyPath} for the path nested under the current property. * * @param path must not be {@literal null} or empty. * @return will never be {@literal null}. */
public PropertyPath nested(String path) { Assert.hasText(path, "Path must not be null or empty!"); String lookup = toDotPath().concat(".").concat(path); return PropertyPath.from(lookup, owningType); } /* * (non-Javadoc) * @see java.lang.Iterable#iterator() */ public Iterator<PropertyPath> iterator() { return new Iterator<PropertyPath>() { private @Nullable PropertyPath current = PropertyPath.this; public boolean hasNext() { return current != null; } @Nullable public PropertyPath next() { PropertyPath result = current; if (result == null) { return null; } this.current = result.next(); return result; } public void remove() { throw new UnsupportedOperationException(); } }; }
Returns the next PropertyPath.
Throws:
Returns:
/** * Returns the next {@link PropertyPath}. * * @return * @throws IllegalStateException it there's no next one. */
private PropertyPath requiredNext() { PropertyPath result = next; if (result == null) { throw new IllegalStateException( "No next path available! Clients should call hasNext() before invoking this method!"); } return result; }
Extracts the PropertyPath chain from the given source String and type.
Params:
  • source –
  • type –
Returns:
/** * Extracts the {@link PropertyPath} chain from the given source {@link String} and type. * * @param source * @param type * @return */
public static PropertyPath from(String source, Class<?> type) { return from(source, ClassTypeInformation.from(type)); }
Extracts the PropertyPath chain from the given source String and TypeInformation.
Uses SPLITTER by default and SPLITTER_FOR_QUOTED for quoted literals.
Params:
  • source – must not be null.
  • type –
Returns:
/** * Extracts the {@link PropertyPath} chain from the given source {@link String} and {@link TypeInformation}. <br /> * Uses {@link #SPLITTER} by default and {@link #SPLITTER_FOR_QUOTED} for {@link Pattern#quote(String) quoted} * literals. * * @param source must not be {@literal null}. * @param type * @return */
public static PropertyPath from(String source, TypeInformation<?> type) { Assert.hasText(source, "Source must not be null or empty!"); Assert.notNull(type, "TypeInformation must not be null or empty!"); return CACHE.computeIfAbsent(Key.of(type, source), it -> { List<String> iteratorSource = new ArrayList<>(); Matcher matcher = isQuoted(it.path) ? SPLITTER_FOR_QUOTED.matcher(it.path.replace("\\Q", "").replace("\\E", "")) : SPLITTER.matcher("_" + it.path); while (matcher.find()) { iteratorSource.add(matcher.group(1)); } Iterator<String> parts = iteratorSource.iterator(); PropertyPath result = null; Stack<PropertyPath> current = new Stack<>(); while (parts.hasNext()) { if (result == null) { result = create(parts.next(), it.type, current); current.push(result); } else { current.push(create(parts.next(), current)); } } if (result == null) { throw new IllegalStateException( String.format("Expected parsing to yield a PropertyPath from %s but got null!", source)); } return result; }); } private static boolean isQuoted(String source) { return source.matches("^\\\\Q.*\\\\E$"); }
Creates a new PropertyPath as subordinary of the given PropertyPath.
Params:
  • source –
  • base –
Returns:
/** * Creates a new {@link PropertyPath} as subordinary of the given {@link PropertyPath}. * * @param source * @param base * @return */
private static PropertyPath create(String source, Stack<PropertyPath> base) { PropertyPath previous = base.peek(); PropertyPath propertyPath = create(source, previous.typeInformation.getRequiredActualType(), base); previous.next = propertyPath; return propertyPath; }
Factory method to create a new PropertyPath for the given String and owning type. It will inspect the given source for camel-case parts and traverse the String along its parts starting with the entire one and chewing off parts from the right side then. Whenever a valid property for the given class is found, the tail will be traversed for subordinary properties of the just found one and so on.
Params:
  • source –
  • type –
Returns:
/** * Factory method to create a new {@link PropertyPath} for the given {@link String} and owning type. It will inspect * the given source for camel-case parts and traverse the {@link String} along its parts starting with the entire one * and chewing off parts from the right side then. Whenever a valid property for the given class is found, the tail * will be traversed for subordinary properties of the just found one and so on. * * @param source * @param type * @return */
private static PropertyPath create(String source, TypeInformation<?> type, List<PropertyPath> base) { return create(source, type, "", base); }
Tries to look up a chain of PropertyPaths by trying the given source first. If that fails it will split the source apart at camel case borders (starting from the right side) and try to look up a PropertyPath from the calculated head and recombined new tail and additional tail.
Params:
  • source –
  • type –
  • addTail –
Returns:
/** * Tries to look up a chain of {@link PropertyPath}s by trying the given source first. If that fails it will split the * source apart at camel case borders (starting from the right side) and try to look up a {@link PropertyPath} from * the calculated head and recombined new tail and additional tail. * * @param source * @param type * @param addTail * @return */
private static PropertyPath create(String source, TypeInformation<?> type, String addTail, List<PropertyPath> base) { if (base.size() > 1000) { throw new IllegalArgumentException(PARSE_DEPTH_EXCEEDED); } PropertyReferenceException exception = null; PropertyPath current = null; try { current = new PropertyPath(source, type, base); if (!base.isEmpty()) { base.get(base.size() - 1).next = current; } List<PropertyPath> newBase = new ArrayList<>(base); newBase.add(current); if (StringUtils.hasText(addTail)) { current.next = create(addTail, current.actualTypeInformation, newBase); } return current; } catch (PropertyReferenceException e) { if (current != null) { throw e; } exception = e; } Pattern pattern = Pattern.compile("\\p{Lu}\\p{Ll}*$"); Matcher matcher = pattern.matcher(source); if (matcher.find() && matcher.start() != 0) { int position = matcher.start(); String head = source.substring(0, position); String tail = source.substring(position); try { return create(head, type, tail + addTail, base); } catch (PropertyReferenceException e) { throw e.hasDeeperResolutionDepthThan(exception) ? e : exception; } } throw exception; } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s.%s", owningType.getType().getSimpleName(), toDotPath()); } @Value(staticConstructor = "of") private static class Key { TypeInformation<?> type; String path; } }