/* * Copyright 2017-2019 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.cloud.gcp.data.datastore.core.convert; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import com.google.cloud.datastore.BaseEntity; import com.google.cloud.datastore.Blob; import com.google.cloud.datastore.EntityValue; import com.google.cloud.datastore.FullEntity; import com.google.cloud.datastore.FullEntity.Builder; import com.google.cloud.datastore.IncompleteKey; import com.google.cloud.datastore.ListValue; import com.google.cloud.datastore.Value; import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastoreDataException; import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastoreMappingContext; import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastorePersistentProperty; import org.springframework.cloud.gcp.data.datastore.core.mapping.EmbeddedType; import org.springframework.cloud.gcp.data.datastore.core.util.ValueUtil; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.CustomConversions; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import static org.springframework.cloud.gcp.data.datastore.core.util.ValueUtil.boxIfNeeded; /** * In order to support {@link CustomConversions}, this class applies 2-step conversions. * The first step produces one of * {@link org.springframework.data.mapping.model.SimpleTypeHolder}'s simple types. The * second step converts simple types to Datastore-native types. The second step is skipped * if the first one produces a Datastore-native type. * * @author Dmitry Solomakha * @author Chengyuan Zhao * * @since 1.1 */ public class TwoStepsConversions implements ReadWriteConversions { private static final Converter<Blob, byte[]> BLOB_TO_BYTE_ARRAY_CONVERTER = new Converter<Blob, byte[]>() { @Override public byte[] convert(Blob source) { return source.toByteArray(); } }; private static final Converter<byte[], Blob> BYTE_ARRAY_TO_BLOB_CONVERTER = new Converter<byte[], Blob>() { @Override public Blob convert(byte[] source) { return Blob.copyFrom(source); } }; private final GenericConversionService conversionService; private final GenericConversionService internalConversionService; private final CustomConversions customConversions; private final ObjectToKeyFactory objectToKeyFactory; private final DatastoreMappingContext datastoreMappingContext; private DatastoreEntityConverter datastoreEntityConverter; private final Map<Class, Optional<Class<?>>> writeConverters = new ConcurrentHashMap<>(); public TwoStepsConversions(CustomConversions customConversions, ObjectToKeyFactory objectToKeyFactory, DatastoreMappingContext datastoreMappingContext) { this.objectToKeyFactory = objectToKeyFactory; this.datastoreMappingContext = datastoreMappingContext; this.conversionService = new DefaultConversionService(); this.internalConversionService = new DefaultConversionService(); this.customConversions = customConversions; this.customConversions.registerConvertersIn(this.conversionService); this.internalConversionService.addConverter(BYTE_ARRAY_TO_BLOB_CONVERTER); this.internalConversionService.addConverter(BLOB_TO_BYTE_ARRAY_CONVERTER); } @Override public <T> T convertOnRead(Object val, Class targetCollectionType, Class targetComponentType) { return (T) convertOnRead(val, EmbeddedType.NOT_EMBEDDED, targetCollectionType, ClassTypeInformation.from(targetComponentType)); } @Override public <T> T convertOnRead(Object val, EmbeddedType embeddedType, TypeInformation targetTypeInformation) { TypeInformation componentTypeInformation; Class collectionType = null; if (targetTypeInformation.isCollectionLike()) { componentTypeInformation = targetTypeInformation.getComponentType(); collectionType = targetTypeInformation.getType(); } else { componentTypeInformation = targetTypeInformation; } return convertOnRead(val, embeddedType, collectionType, componentTypeInformation); } private <T> T convertOnRead(Object val, EmbeddedType embeddedType, Class targetCollectionType, TypeInformation targetComponentType) { if (val == null) { return null; } BiFunction<Object, TypeInformation<?>, ?> readConverter; switch (embeddedType) { case EMBEDDED_MAP: readConverter = (x, typeInformation) -> convertOnReadSingleEmbeddedMap(x, typeInformation.getComponentType().getType(), typeInformation.getMapValueType(), targetComponentType); break; case EMBEDDED_ENTITY: readConverter = this::convertOnReadSingleEmbedded; break; case NOT_EMBEDDED: readConverter = this::convertOnReadSingle; break; default: throw new DatastoreDataException( "Unexpected property embedded type: " + embeddedType); } if (ValueUtil.isCollectionLike(val.getClass()) && targetCollectionType != null && targetComponentType != null) { try { List elements = val.getClass().isArray() ? (Arrays.asList(val)) : StreamSupport.stream(((Iterable<?>) val).spliterator(), false) .map((v) -> readConverter.apply( (v instanceof Value) ? ((Value) v).get() : v, targetComponentType)) .collect(Collectors.toList()); return (T) convertCollection(elements, targetCollectionType); } catch (ConversionException | DatastoreDataException ex) { throw new DatastoreDataException("Unable process elements of a collection", ex); } } return (T) readConverter.apply(val, targetComponentType); } private <T, R> Map<T, R> convertOnReadSingleEmbeddedMap(Object value, Class<T> keyType, TypeInformation<R> targetComponentType, TypeInformation componentType) { Assert.notNull(value, "Cannot convert a null value."); if (value instanceof BaseEntity) { return this.datastoreEntityConverter.readAsMap((BaseEntity) value, componentType); } throw new DatastoreDataException( "Embedded entity was expected, but " + value.getClass() + " found"); } @SuppressWarnings("unchecked") private <T> T convertOnReadSingleEmbedded(Object value, TypeInformation<?> targetTypeInformation) { Assert.notNull(value, "Cannot convert a null value."); if (value instanceof BaseEntity) { return (T) this.datastoreEntityConverter.read(targetTypeInformation.getType(), (BaseEntity) value); } throw new DatastoreDataException("Embedded entity was expected, but " + value.getClass() + " found"); } @SuppressWarnings("unchecked") private <T> T convertOnReadSingle(Object val, TypeInformation<?> targetTypeInformation) { if (val == null) { return null; } Class targetType = boxIfNeeded(targetTypeInformation.getType()); Class sourceType = val.getClass(); Object result = null; TypeTargets typeTargets = computeTypeTargets(targetType); if (typeTargets.getFirstStepTarget() == null && typeTargets.getSecondStepTarget() == null && ClassUtils.isAssignable(targetType, val.getClass())) { //neither first or second steps were applied, no conversion is necessary result = val; } else if (typeTargets.getFirstStepTarget() == null && typeTargets.getSecondStepTarget() != null) { //only second step was applied on write result = this.internalConversionService.convert(val, targetType); } else if (typeTargets.getFirstStepTarget() != null && typeTargets.getSecondStepTarget() == null) { //only first step was applied on write result = this.conversionService.convert(val, targetType); } else if (typeTargets.getFirstStepTarget() != null && typeTargets.getSecondStepTarget() != null) { //both steps were applied Object secondStepVal = this.internalConversionService.convert(val, typeTargets.getFirstStepTarget()); result = this.conversionService.convert(secondStepVal, targetType); } // if the value can be directly converted else if (DatastoreNativeTypes.isNativeType(sourceType) && this.conversionService.canConvert(sourceType, targetType)) { result = this.conversionService.convert(val, targetType); } else if (DatastoreNativeTypes.isNativeType(sourceType) && this.internalConversionService.canConvert(sourceType, targetType)) { result = this.internalConversionService.convert(val, targetType); } if (result != null) { return (T) result; } else { throw new DatastoreDataException("Unable to convert " + val.getClass() + " to " + targetType); } } @Override public Value convertOnWrite(Object proppertyVal, DatastorePersistentProperty persistentProperty) { return convertOnWrite(proppertyVal, persistentProperty.getEmbeddedType(), persistentProperty.getFieldName(), persistentProperty.getTypeInformation()); } private Value convertOnWrite(Object proppertyVal, EmbeddedType embeddedType, String fieldName, TypeInformation typeInformation) { Object val = proppertyVal; Function<Object, Value> writeConverter = this::convertOnWriteSingle; if (proppertyVal != null) { switch (embeddedType) { case EMBEDDED_MAP: writeConverter = (x) -> convertOnWriteSingleEmbeddedMap(x, fieldName, (TypeInformation) typeInformation.getMapValueType()); break; case EMBEDDED_ENTITY: writeConverter = (x) -> convertOnWriteSingleEmbedded(x, fieldName); break; case NOT_EMBEDDED: writeConverter = this::convertOnWriteSingle; break; default: throw new DatastoreDataException( "Unexpected property embedded type: " + embeddedType); } } val = ValueUtil.toListIfArray(val); if (val instanceof Iterable) { List<Value<?>> values = new ArrayList<>(); for (Object propEltValue : (Iterable) val) { values.add(writeConverter.apply(propEltValue)); } return ListValue.of(values); } return writeConverter.apply(val); } private EntityValue applyEntityValueBuilder(Object val, String kindName, Consumer<Builder> consumer, boolean createKey) { FullEntity.Builder<IncompleteKey> builder; if (!createKey) { builder = FullEntity.newBuilder(); } else { /* The following does 3 sequential null checks. We only want an ID value if the object isn't null, has an ID property, and the ID property isn't null. * */ Optional idProp = Optional.ofNullable(val) .map(v -> this.datastoreMappingContext.getPersistentEntity(v.getClass())) .map(datastorePersistentEntity -> datastorePersistentEntity.getIdProperty()) .map(id -> this.datastoreMappingContext.getPersistentEntity(val.getClass()) .getPropertyAccessor(val).getProperty(id)); IncompleteKey key = idProp.isPresent() ? this.objectToKeyFactory.getKeyFromId(idProp.get(), kindName) : this.objectToKeyFactory.getIncompleteKey(kindName); builder = FullEntity.newBuilder(key); } consumer.accept(builder); return EntityValue.of(builder.build()); } private EntityValue convertOnWriteSingleEmbeddedMap(Object val, String kindName, TypeInformation valueTypeInformation) { return applyEntityValueBuilder(null, kindName, (builder) -> { Map map = (Map) val; for (Object key : map.keySet()) { String field = convertOnReadSingle(convertOnWriteSingle(key).get(), ClassTypeInformation.from(String.class)); builder.set(field, convertOnWrite(map.get(key), EmbeddedType.of(valueTypeInformation), field, valueTypeInformation)); } }, false); } private EntityValue convertOnWriteSingleEmbedded(Object val, String kindName) { return applyEntityValueBuilder(val, kindName, (builder) -> this.datastoreEntityConverter.write(val, builder), true); } @Override @SuppressWarnings("unchecked") public Value convertOnWriteSingle(Object propertyVal) { Object result = propertyVal; if (result != null) { TypeTargets typeTargets = computeTypeTargets(result.getClass()); if (typeTargets.getFirstStepTarget() != null) { result = this.conversionService.convert(propertyVal, typeTargets.getFirstStepTarget()); } if (typeTargets.getSecondStepTarget() != null) { result = this.internalConversionService.convert(result, typeTargets.getSecondStepTarget()); } } return DatastoreNativeTypes.wrapValue(result); } private TypeTargets computeTypeTargets(Class<?> firstStepSource) { Class<?> firstStepTarget = null; Class<?> secondStepTarget = null; if (!DatastoreNativeTypes.isNativeType(firstStepSource)) { Optional<Class<?>> simpleType = this.customConversions.getCustomWriteTarget(firstStepSource); if (simpleType.isPresent()) { firstStepTarget = simpleType.get(); } Class<?> effectiveFirstStepTarget = (firstStepTarget != null) ? firstStepTarget : firstStepSource; Optional<Class<?>> datastoreBasicType = getCustomWriteTarget(effectiveFirstStepTarget); if (datastoreBasicType.isPresent()) { secondStepTarget = datastoreBasicType.get(); } } return new TypeTargets(firstStepTarget, secondStepTarget); } @SuppressWarnings("unchecked") public <T> T convertCollection(Object collection, Class<?> target) { if (collection == null || target == null || ClassUtils.isAssignableValue(target, collection)) { return (T) collection; } return (T) this.conversionService.convert(collection, target); } private Optional<Class<?>> getCustomWriteTarget(Class<?> sourceType) { if (DatastoreNativeTypes.isNativeType(sourceType)) { return Optional.empty(); } return this.writeConverters.computeIfAbsent(sourceType, this::getDatastoreCompatibleType); } @Override public Optional<Class<?>> getDatastoreCompatibleType(Class inputType) { if (DatastoreNativeTypes.DATASTORE_NATIVE_TYPES.contains(inputType)) { return Optional.of(inputType); } return DatastoreNativeTypes.DATASTORE_NATIVE_TYPES.stream() .filter((simpleType) -> this.internalConversionService.canConvert(inputType, simpleType) && this.internalConversionService.canConvert(simpleType, inputType)) .findAny(); } @Override public void registerEntityConverter(DatastoreEntityConverter datastoreEntityConverter) { this.datastoreEntityConverter = datastoreEntityConverter; } private static class TypeTargets { private Class<?> firstStepTarget; private Class<?> secondStepTarget; TypeTargets(Class<?> firstStepTarget, Class<?> secondStepTarget) { this.firstStepTarget = firstStepTarget; this.secondStepTarget = secondStepTarget; } Class<?> getFirstStepTarget() { return this.firstStepTarget; } Class<?> getSecondStepTarget() { return this.secondStepTarget; } } }