package org.springframework.data.mapping;
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.ObjectUtils;
import org.springframework.util.StringUtils;
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 TypeInformation<?> typeInformation;
private final TypeInformation<?> actualTypeInformation;
private final boolean isCollection;
private @Nullable PropertyPath next;
PropertyPath(String name, Class<?> owningType) {
this(name, ClassTypeInformation.from(owningType), Collections.emptyList());
}
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();
}
public TypeInformation<?> getOwningType() {
return owningType;
}
public String getSegment() {
return name;
}
public PropertyPath getLeafProperty() {
PropertyPath result = this;
while (result.hasNext()) {
result = result.requiredNext();
}
return result;
}
public Class<?> getLeafType() {
return getLeafProperty().getType();
}
public Class<?> getType() {
return this.actualTypeInformation.getType();
}
public TypeInformation<?> getTypeInformation() {
return this.typeInformation;
}
@Nullable
public PropertyPath next() {
return next;
}
public boolean hasNext() {
return next != null;
}
public String toDotPath() {
if (hasNext()) {
return getSegment() + "." + requiredNext().toDotPath();
}
return getSegment();
}
public boolean isCollection() {
return isCollection;
}
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);
}
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();
}
};
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof PropertyPath)) {
return false;
}
PropertyPath that = (PropertyPath) o;
if (isCollection != that.isCollection) {
return false;
}
if (!ObjectUtils.nullSafeEquals(owningType, that.owningType)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(name, that.name)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(typeInformation, that.typeInformation)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(actualTypeInformation, that.actualTypeInformation)) {
return false;
}
return ObjectUtils.nullSafeEquals(next, that.next);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(owningType);
result = 31 * result + ObjectUtils.nullSafeHashCode(name);
result = 31 * result + ObjectUtils.nullSafeHashCode(typeInformation);
result = 31 * result + ObjectUtils.nullSafeHashCode(actualTypeInformation);
result = 31 * result + (isCollection ? 1 : 0);
result = 31 * result + ObjectUtils.nullSafeHashCode(next);
return result;
}
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;
}
public static PropertyPath from(String source, Class<?> type) {
return from(source, ClassTypeInformation.from(type));
}
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$");
}
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;
}
private static PropertyPath create(String source, TypeInformation<?> type, List<PropertyPath> base) {
return create(source, type, "", base);
}
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;
}
@Override
public String toString() {
return String.format("%s.%s", owningType.getType().getSimpleName(), toDotPath());
}
private static final class Key {
private final TypeInformation<?> type;
private final String path;
private Key(TypeInformation<?> type, String path) {
this.type = type;
this.path = path;
}
public static Key of(TypeInformation<?> type, String path) {
return new Key(type, path);
}
public TypeInformation<?> getType() {
return this.type;
}
public String getPath() {
return this.path;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Key)) {
return false;
}
Key key = (Key) o;
if (!ObjectUtils.nullSafeEquals(type, key.type)) {
return false;
}
return ObjectUtils.nullSafeEquals(path, key.path);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(type);
result = 31 * result + ObjectUtils.nullSafeHashCode(path);
return result;
}
@Override
public String toString() {
return "PropertyPath.Key(type=" + this.getType() + ", path=" + this.getPath() + ")";
}
}
}