/* * Copyright 2017-2018 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.lang.reflect.Constructor; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import com.google.cloud.datastore.BaseEntity; import com.google.cloud.datastore.EntityValue; import com.google.cloud.datastore.FullEntity; import com.google.cloud.datastore.IncompleteKey; import com.google.cloud.datastore.ListValue; import com.google.cloud.datastore.StringValue; 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.DatastorePersistentEntity; import org.springframework.cloud.gcp.data.datastore.core.mapping.DatastorePersistentProperty; import org.springframework.cloud.gcp.data.datastore.core.mapping.EmbeddedType; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import static org.springframework.cloud.gcp.data.datastore.core.mapping.EmbeddedType.NOT_EMBEDDED; /** * A class for object to entity and entity to object conversions. * * @author Dmitry Solomakha * @author Chengyuan Zhao * * @since 1.1 */ public class DefaultDatastoreEntityConverter implements DatastoreEntityConverter { private DatastoreMappingContext mappingContext; private final EntityInstantiators instantiators = new EntityInstantiators(); private final ReadWriteConversions conversions; public DefaultDatastoreEntityConverter(DatastoreMappingContext mappingContext, ObjectToKeyFactory objectToKeyFactory) { this(mappingContext, new TwoStepsConversions(new DatastoreCustomConversions(), objectToKeyFactory, mappingContext)); } public DefaultDatastoreEntityConverter(DatastoreMappingContext mappingContext, ReadWriteConversions conversions) { this.mappingContext = mappingContext; this.conversions = conversions; conversions.registerEntityConverter(this); } @Override public ReadWriteConversions getConversions() { return this.conversions; } @Override public <T, R> Map<T, R> readAsMap(BaseEntity entity, TypeInformation mapTypeInformation) { Assert.notNull(mapTypeInformation, "mapTypeInformation can't be null"); if (entity == null) { return null; } Map<T, R> result; if (!mapTypeInformation.getType().isInterface()) { try { result = (Map<T, R>) ((Constructor<?>) mapTypeInformation.getType().getConstructor()).newInstance(); } catch (Exception e) { throw new DatastoreDataException("Unable to create an instance of a custom map type: " + mapTypeInformation.getType() + " (make sure the class is public and has a public no-args constructor)", e); } } else { result = new HashMap<>(); } EntityPropertyValueProvider propertyValueProvider = new EntityPropertyValueProvider( entity, this.conversions); Set<String> fieldNames = entity.getNames(); for (String field : fieldNames) { result.put(this.conversions.convertOnRead(field, NOT_EMBEDDED, mapTypeInformation.getComponentType()), propertyValueProvider.getPropertyValue(field, EmbeddedType.of(mapTypeInformation.getMapValueType()), mapTypeInformation.getMapValueType())); } return result; } @Override public <T, R> Map<T, R> readAsMap(Class<T> keyType, TypeInformation<R> componentType, BaseEntity entity) { if (entity == null) { return null; } Map<T, R> result = new HashMap<>(); return readAsMap(entity, ClassTypeInformation.from(result.getClass())); } @Override @SuppressWarnings("unchecked") public <R> R read(Class<R> aClass, BaseEntity entity) { if (entity == null) { return null; } DatastorePersistentEntity<R> ostensiblePersistentEntity = (DatastorePersistentEntity<R>) this.mappingContext .getPersistentEntity(aClass); if (ostensiblePersistentEntity == null) { throw new DatastoreDataException("Unable to convert Datastore Entity to " + aClass); } EntityPropertyValueProvider propertyValueProvider = new EntityPropertyValueProvider(entity, this.conversions); DatastorePersistentEntity<?> persistentEntity = getDiscriminationPersistentEntity(ostensiblePersistentEntity, propertyValueProvider); ParameterValueProvider<DatastorePersistentProperty> parameterValueProvider = new PersistentEntityParameterValueProvider<>(persistentEntity, propertyValueProvider, null); EntityInstantiator instantiator = this.instantiators.getInstantiatorFor(persistentEntity); Object instance; try { instance = instantiator.createInstance(persistentEntity, parameterValueProvider); PersistentPropertyAccessor accessor = persistentEntity.getPropertyAccessor(instance); persistentEntity.doWithColumnBackedProperties((datastorePersistentProperty) -> { // if a property is a constructor argument, it was already computed on instantiation if (!persistentEntity.isConstructorArgument(datastorePersistentProperty)) { Object value = propertyValueProvider .getPropertyValue(datastorePersistentProperty); accessor.setProperty(datastorePersistentProperty, value); } }); } catch (DatastoreDataException ex) { throw new DatastoreDataException("Unable to read " + persistentEntity.getName() + " entity", ex); } return (R) instance; } private DatastorePersistentEntity getDiscriminationPersistentEntity(DatastorePersistentEntity ostensibleEntity, EntityPropertyValueProvider propertyValueProvider) { if (ostensibleEntity.getDiscriminationFieldName() == null) { return ostensibleEntity; } Set<Class> members = DatastoreMappingContext.getDiscriminationFamily(ostensibleEntity.getType()); Optional<DatastorePersistentEntity> persistentEntity = members == null ? Optional.empty() : members.stream().map(x -> (DatastorePersistentEntity) this.mappingContext.getPersistentEntity(x)) .filter(x -> x != null && isDiscriminationFieldMatch(x, propertyValueProvider)).findFirst(); return persistentEntity.orElse(ostensibleEntity); } private boolean isDiscriminationFieldMatch(DatastorePersistentEntity entity, EntityPropertyValueProvider propertyValueProvider) { return ((String[]) propertyValueProvider.getPropertyValue(entity.getDiscriminationFieldName(), NOT_EMBEDDED, ClassTypeInformation.from(String[].class)))[0].equals(entity.getDiscriminatorValue()); } @Override @SuppressWarnings("unchecked") public void write(Object source, BaseEntity.Builder sink) { DatastorePersistentEntity<?> persistentEntity = this.mappingContext.getPersistentEntity(source.getClass()); String discriminationFieldName = persistentEntity.getDiscriminationFieldName(); List<String> discriminationValues = persistentEntity.getCompatibleDiscriminationValues(); if (!discriminationValues.isEmpty() || discriminationFieldName != null) { sink.set(discriminationFieldName, discriminationValues.stream().map(StringValue::of).collect(Collectors.toList())); } PersistentPropertyAccessor accessor = persistentEntity.getPropertyAccessor(source); persistentEntity.doWithColumnBackedProperties( (DatastorePersistentProperty persistentProperty) -> { // Datastore doesn't store its Key as a regular field. if (persistentProperty.isIdProperty()) { return; } try { Object val = accessor.getProperty(persistentProperty); Value convertedVal = this.conversions.convertOnWrite(val, persistentProperty); if (persistentProperty.isUnindexed()) { convertedVal = setExcludeFromIndexes(convertedVal); } sink.set(persistentProperty.getFieldName(), convertedVal); } catch (DatastoreDataException ex) { throw new DatastoreDataException( "Unable to write " + persistentEntity.kindName() + "." + persistentProperty.getFieldName(), ex); } }); } private Value setExcludeFromIndexes(Value convertedVal) { // ListValues must have its contents individually excluded instead. // the entire list must NOT be excluded or there will be an exception. // Same for maps and embedded entities which are stored as EntityValue. if (convertedVal.getClass().equals(EntityValue.class)) { FullEntity.Builder<IncompleteKey> builder = FullEntity.newBuilder(); ((EntityValue) convertedVal).get().getProperties() .forEach((key, value) -> builder.set(key, setExcludeFromIndexes(value))); return EntityValue.of(builder.build()); } else if (convertedVal.getClass().equals(ListValue.class)) { return ListValue.of((List) ((ListValue) convertedVal).get().stream() .map(this::setExcludeFromIndexes).collect(Collectors.toList())); } else { return convertedVal.toBuilder().setExcludeFromIndexes(true).build(); } } }