/*
* 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.convert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
/**
* Default implementation of {@link TypeMapper}.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class DefaultTypeMapper<S> implements TypeMapper<S> {
private final TypeAliasAccessor<S> accessor;
private final List<? extends TypeInformationMapper> mappers;
private final Map<Alias, Optional<TypeInformation<?>>> typeCache;
/**
* Creates a new {@link DefaultTypeMapper} using the given {@link TypeAliasAccessor}. It will use a
* {@link SimpleTypeInformationMapper} to calculate type aliases.
*
* @param accessor must not be {@literal null}.
*/
public DefaultTypeMapper(TypeAliasAccessor<S> accessor) {
this(accessor, Collections.singletonList(new SimpleTypeInformationMapper()));
}
/**
* Creates a new {@link DefaultTypeMapper} using the given {@link TypeAliasAccessor} and {@link TypeInformationMapper}
* s.
*
* @param accessor must not be {@literal null}.
* @param mappers must not be {@literal null}.
*/
public DefaultTypeMapper(TypeAliasAccessor<S> accessor, List<? extends TypeInformationMapper> mappers) {
this(accessor, null, mappers);
}
/**
* Creates a new {@link DefaultTypeMapper} using the given {@link TypeAliasAccessor}, {@link MappingContext} and
* additional {@link TypeInformationMapper}s. Will register a {@link MappingContextTypeInformationMapper} before the
* given additional mappers.
*
* @param accessor must not be {@literal null}.
* @param mappingContext
* @param additionalMappers must not be {@literal null}.
*/
public DefaultTypeMapper(TypeAliasAccessor<S> accessor,
MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext,
List<? extends TypeInformationMapper> additionalMappers) {
Assert.notNull(accessor, "Accessor must not be null!");
Assert.notNull(additionalMappers, "AdditionalMappers must not be null!");
List<TypeInformationMapper> mappers = new ArrayList<>(additionalMappers.size() + 1);
if (mappingContext != null) {
mappers.add(new MappingContextTypeInformationMapper(mappingContext));
}
mappers.addAll(additionalMappers);
this.mappers = Collections.unmodifiableList(mappers);
this.accessor = accessor;
this.typeCache = new ConcurrentHashMap<>();
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeMapper#readType(java.lang.Object)
*/
public Optional<TypeInformation<?>> readType(S source) {
Assert.notNull(source, "Source object must not be null!");
return getFromCacheOrCreate(accessor.readAliasFrom(source));
}
/**
* Tries to lookup a {@link TypeInformation} for the given alias from the cache and return it if found. If none is
* found it'll consult the {@link TypeInformationMapper}s and cache the value found.
*
* @param alias
* @return
*/
private Optional<TypeInformation<?>> getFromCacheOrCreate(Alias alias) {
return typeCache.computeIfAbsent(alias, key -> Optionals.firstNonEmpty(mappers, it -> it.resolveTypeFrom(alias)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeMapper#readType(java.lang.Object, org.springframework.data.util.TypeInformation)
*/
public <T> TypeInformation<? extends T> readType(S source, TypeInformation<T> basicType) {
Assert.notNull(source, "Source must not be null!");
Assert.notNull(basicType, "Basic type must not be null!");
Optional<TypeInformation<? extends T>> calculated = getDefaultedTypeToBeUsed(source)//
.map(it -> specializeOrDefault(it, basicType));
return calculated.orElse(basicType);
}
private static <T> TypeInformation<? extends T> specializeOrDefault(Class<?> it, TypeInformation<T> type) {
ClassTypeInformation<?> targetType = ClassTypeInformation.from(it);
Class<T> rawType = type.getType();
return rawType.isAssignableFrom(it) && !rawType.equals(it) ? type.specialize(targetType) : type;
}
/**
* Returns the type discovered through {@link #readType(Object)} but defaulted to the one returned by
* {@link #getFallbackTypeFor(Object)}.
*
* @param source
* @return
*/
private Optional<Class<?>> getDefaultedTypeToBeUsed(S source) {
return readType(source)//
.map(it -> readType(source))//
.orElseGet(() -> getFallbackTypeFor(source))//
.map(TypeInformation::getType);
}
/**
* Returns the type fallback {@link TypeInformation} in case none could be extracted from the given source.
*
* @param source will never be {@literal null}.
* @return
*/
protected Optional<TypeInformation<?>> getFallbackTypeFor(S source) {
return Optional.empty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeMapper#writeType(java.lang.Class, java.lang.Object)
*/
public void writeType(Class<?> type, S dbObject) {
writeType(ClassTypeInformation.from(type), dbObject);
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeMapper#writeType(org.springframework.data.util.TypeInformation, java.lang.Object)
*/
public void writeType(TypeInformation<?> info, S sink) {
Assert.notNull(info, "TypeInformation must not be null!");
getAliasFor(info).getValue().ifPresent(it -> accessor.writeTypeTo(sink, it));
}
/**
* Returns the alias to be used for the given {@link TypeInformation}.
*
* @param info must not be {@literal null}
* @return the alias for the given {@link TypeInformation} or {@literal null} of none was found or all mappers
* returned {@literal null}.
*/
protected final Alias getAliasFor(TypeInformation<?> info) {
Assert.notNull(info, "TypeInformation must not be null!");
return Optionals.firstNonEmpty(mappers, it -> it.createAliasFor(info), Alias.NONE);
}
}