/*
* Copyright 2011-2017 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
*
* http://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 java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Value object to encapsulate the constructor to be used when mapping persistent data to objects.
*
* @author Oliver Gierke
* @author Jon Brisbin
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class PreferredConstructor<T, P extends PersistentProperty<P>> {
private final Constructor<T> constructor;
private final List<Parameter<Object, P>> parameters;
private final Map<PersistentProperty<?>, Boolean> isPropertyParameterCache = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();
/**
* Creates a new {@link PreferredConstructor} from the given {@link Constructor} and {@link Parameter}s.
*
* @param constructor must not be {@literal null}.
* @param parameters must not be {@literal null}.
*/
@SafeVarargs
public PreferredConstructor(Constructor<T> constructor, Parameter<Object, P>... parameters) {
Assert.notNull(constructor, "Constructor must not be null!");
Assert.notNull(parameters, "Parameters must not be null!");
ReflectionUtils.makeAccessible(constructor);
this.constructor = constructor;
this.parameters = Arrays.asList(parameters);
}
/**
* Returns the underlying {@link Constructor}.
*
* @return
*/
public Constructor<T> getConstructor() {
return constructor;
}
/**
* Returns the {@link Parameter}s of the constructor.
*
* @return
*/
public Streamable<Parameter<Object, P>> getParameters() {
return Streamable.of(parameters);
}
/**
* Returns whether the constructor has {@link Parameter}s.
*
* @see #isNoArgConstructor()
* @return
*/
public boolean hasParameters() {
return !parameters.isEmpty();
}
/**
* Returns whether the constructor does not have any arguments.
*
* @see #hasParameters()
* @return
*/
public boolean isNoArgConstructor() {
return parameters.isEmpty();
}
/**
* Returns whether the constructor was explicitly selected (by {@link PersistenceConstructor}).
*
* @return
*/
public boolean isExplicitlyAnnotated() {
return constructor.isAnnotationPresent(PersistenceConstructor.class);
}
/**
* Returns whether the given {@link PersistentProperty} is referenced in a constructor argument of the
* {@link PersistentEntity} backing this {@link PreferredConstructor}.
*
* @param property must not be {@literal null}.
* @return
*/
public boolean isConstructorParameter(PersistentProperty<?> property) {
Assert.notNull(property, "Property must not be null!");
try {
read.lock();
Boolean cached = isPropertyParameterCache.get(property);
if (cached != null) {
return cached;
}
} finally {
read.unlock();
}
try {
write.lock();
for (Parameter<?, P> parameter : parameters) {
if (parameter.maps(property)) {
isPropertyParameterCache.put(property, true);
return true;
}
}
isPropertyParameterCache.put(property, false);
return false;
} finally {
write.unlock();
}
}
/**
* Returns whether the given {@link Parameter} is one referring to an enclosing class. That is in case the class this
* {@link PreferredConstructor} belongs to is a member class actually. If that's the case the compiler creates a first
* constructor argument of the enclosing class type.
*
* @param parameter must not be {@literal null}.
* @return
*/
public boolean isEnclosingClassParameter(Parameter<?, P> parameter) {
Assert.notNull(parameter, "Parameter must not be null!");
if (parameters.isEmpty() || !parameter.isEnclosingClassParameter()) {
return false;
}
return parameters.get(0).equals(parameter);
}
/**
* Value object to represent constructor parameters.
*
* @param <T> the type of the parameter
* @author Oliver Gierke
*/
@EqualsAndHashCode(exclude = { "enclosingClassCache", "hasSpelExpression" })
public static class Parameter<T, P extends PersistentProperty<P>> {
private final Optional<String> name;
private final TypeInformation<T> type;
private final Optional<String> key;
private final Optional<PersistentEntity<T, P>> entity;
private final Lazy<Boolean> enclosingClassCache;
private final Lazy<Boolean> hasSpelExpression;
/**
* Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of
* {@link Annotation}s. Will insprect the annotations for an {@link Value} annotation to lookup a key or an SpEL
* expression to be evaluated.
*
* @param name the name of the parameter, can be {@literal null}
* @param type must not be {@literal null}
* @param annotations must not be {@literal null} but can be empty
* @param entity must not be {@literal null}.
*/
public Parameter(Optional<String> name, TypeInformation<T> type, Annotation[] annotations,
Optional<PersistentEntity<T, P>> entity) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(annotations, "Annotations must not be null!");
this.name = name;
this.type = type;
this.key = getValue(annotations);
this.entity = entity;
this.enclosingClassCache = Lazy.of(() -> {
Class<T> owningType = entity.orElseThrow(IllegalStateException::new).getType();
return owningType.isMemberClass() && type.getType().equals(owningType.getEnclosingClass());
});
this.hasSpelExpression = Lazy.of(() -> getSpelExpression().map(StringUtils::hasText).orElse(false));
}
private static Optional<String> getValue(Annotation[] annotations) {
return Arrays.stream(annotations)//
.filter(it -> it.annotationType() == Value.class)//
.findFirst().map(it -> ((Value) it).value())//
.filter(StringUtils::hasText);
}
/**
* Returns the name of the parameter.
*
* @return
*/
public Optional<String> getName() {
return name;
}
/**
* Returns the {@link TypeInformation} of the parameter.
*
* @return
*/
public TypeInformation<T> getType() {
return type;
}
/**
* Returns the raw resolved type of the parameter.
*
* @return
*/
public Class<T> getRawType() {
return type.getType();
}
/**
* Returns the key to be used when looking up a source data structure to populate the actual parameter value.
*
* @return
*/
public Optional<String> getSpelExpression() {
return key;
}
/**
* Returns whether the constructor parameter is equipped with a SpEL expression.
*
* @return
*/
public boolean hasSpelExpression() {
return hasSpelExpression.get();
}
/**
* Returns whether the {@link Parameter} maps the given {@link PersistentProperty}.
*
* @param property
* @return
*/
boolean maps(PersistentProperty<?> property) {
//
//
return name.map(s -> entity
.flatMap(it -> it.getPersistentProperty(s))
.map(property::equals).orElse(false)).orElse(false);
}
private boolean isEnclosingClassParameter() {
return enclosingClassCache.get();
}
}
}