package org.javers.core.metamodel.type;
import java.util.Optional;
import org.javers.common.exception.JaversException;
import org.javers.common.exception.JaversExceptionCode;
import org.javers.common.reflection.ReflectionUtil;
import org.javers.core.metamodel.clazz.ClientsClassDefinition;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import static org.javers.common.reflection.ReflectionUtil.extractClass;
import static org.javers.common.validation.Validate.argumentIsNotNull;
import static org.javers.common.validation.Validate.argumentsAreNotNull;
/**
* thread-safe, thin wrapper for map
*
* @author bartosz.walacik
*/
class TypeMapperState {
private final Map<String, JaversType> mappedTypes = new ConcurrentHashMap<>();
private final Map<DuckType, Class> mappedTypeNames = new ConcurrentHashMap<>();
private final TypeFactory typeFactory;
private final ValueType OBJECT_TYPE = new ValueType(Object.class);
TypeMapperState(TypeFactory typeFactory) {
this.typeFactory = typeFactory;
}
/**
* @throws JaversException TYPE_NAME_NOT_FOUND if given typeName is not registered
* @since 1.4
*/
Class getClassByTypeName(String typeName) {
return getClassByDuckType(new DuckType(typeName));
}
/**
* @throws JaversException TYPE_NAME_NOT_FOUND if given typeName is not registered
* @since 1.4
*/
Class getClassByDuckType(DuckType duckType) {
argumentsAreNotNull(duckType);
Class javaType = mappedTypeNames.get(duckType);
if (javaType != null){
return javaType;
}
synchronized (duckType.getTypeName()) {
Optional<? extends Class> classForName = parseClass(duckType.getTypeName());
if (classForName.isPresent()) {
mappedTypeNames.put(duckType, classForName.get());
return classForName.get();
}
}
//try to fallback to bare typeName when properties doesn't match
if (!duckType.isBare()){
return getClassByDuckType(duckType.bareCopy());
}
throw new JaversException(JaversExceptionCode.TYPE_NAME_NOT_FOUND, duckType.getTypeName());
}
boolean contains(Type javaType){
return mappedTypes.containsKey(javaType.toString());
}
JaversType getJaversType(Type javaType) {
argumentIsNotNull(javaType);
if (javaType == Object.class) {
return OBJECT_TYPE;
}
JaversType jType = getFromMap(javaType);
if (jType != null) {
return jType;
}
return computeIfAbsent(javaType, type -> infer(type));
}
void putIfAbsent(Type javaType, final JaversType jType) {
computeIfAbsent(javaType, ignored -> jType);
}
void computeIfAbsent(final ClientsClassDefinition def) {
computeIfAbsent(def.getBaseJavaClass(), ignored -> typeFactory.create(def));
}
//synchronizes on map Key (javaType) only for map writes
private JaversType computeIfAbsent(Type javaType, Function<Type, JaversType> computeFunction) {
synchronized (javaType) {
//map.contains double check
JaversType mappedType = getFromMap(javaType);
if (mappedType != null) {
return mappedType;
}
JaversType newType = computeFunction.apply(javaType);
addFullMapping(javaType, newType);
inferIdPropertyTypeForEntityIfNeed(newType);
return newType;
}
}
/**
* if type of given id-property is not already mapped, maps it as ValueType
* <p/>
* must be called within synchronized block
*/
private void inferIdPropertyTypeForEntityIfNeed(JaversType jType) {
argumentIsNotNull(jType);
if (jType instanceof EntityType) {
EntityType entityType = (EntityType) jType;
Type idType = entityType.getIdPropertyGenericType();
addFullMapping(idType, typeFactory.inferIdPropertyTypeAsValue(idType));
}
}
/**
* must be called within synchronized block
*/
private void addFullMapping(Type javaType, JaversType newType){
putToMap(javaType, newType);
if (newType instanceof ManagedType){
ManagedType managedType = (ManagedType)newType;
mappedTypeNames.put(new DuckType(managedType.getName()), ReflectionUtil.extractClass(javaType));
mappedTypeNames.put(new DuckType(managedType), ReflectionUtil.extractClass(javaType));
}
}
/**
* must be called within synchronized block
*/
private JaversType infer(Type javaType) {
argumentIsNotNull(javaType);
JaversType prototype = findNearestAncestor(javaType);
JaversType newType = typeFactory.infer(javaType, Optional.ofNullable(prototype));
return newType;
}
private JaversType findNearestAncestor(Type javaType) {
Class javaClass = extractClass(javaType);
List<DistancePair> distances = new ArrayList<>();
for (JaversType javersType : mappedTypes.values()) {
DistancePair distancePair = new DistancePair(javaClass, javersType);
//this is due too spoiled Java Array reflection API
if (javaClass.isArray()) {
return getJaversType(Object[].class);
}
//just to better speed
if (distancePair.getDistance() == 0) {
return distancePair.getJaversType();
}
distances.add(distancePair);
}
Collections.sort(distances);
if (distances.get(0).isMax()) {
return null;
}
return distances.get(0).getJaversType();
}
private Optional<? extends Class> parseClass(String qualifiedName){
try {
return Optional.of( TypeMapperState.class.forName(qualifiedName) );
}
catch (ClassNotFoundException e){
return Optional.empty();
}
}
private JaversType getFromMap(Type javaType) {
return mappedTypes.get(javaType.toString());
}
private void putToMap(Type javaType, JaversType javersType) {
mappedTypes.put(javaType.toString(), javersType);
}
}