/* * Copyright 2012 - 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.solr.core.convert; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrInputField; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.solr.core.mapping.SolrPersistentEntity; import org.springframework.data.solr.core.mapping.SolrPersistentProperty; import org.springframework.data.solr.core.query.Criteria; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** * Implementation of {@link SolrConverter} to read/write {@link org.apache.solr.common.SolrDocument}/ * {@link SolrInputDocument}. <br/> * * @author Christoph Strobl * @author Francisco Spaeth * @author Mark Paluch */ public class MappingSolrConverter extends SolrConverterBase implements SolrConverter, ApplicationContextAware, InitializingBean { private enum WildcardPosition { LEADING { @Override public boolean match(String fieldName, String candidate) { return StringUtils.endsWith(candidate, removeWildcard(fieldName)); } @Override public String extractName(String fieldName, String dynamicFieldName) { Assert.isTrue(match(fieldName, dynamicFieldName), "dynamicFieldName must be derivated from fieldName"); return StringUtils.removeEnd(dynamicFieldName, removeWildcard(fieldName)); } @Override public String createName(String fieldName, String name) { return name + removeWildcard(fieldName); } }, TRAILING { @Override public boolean match(String fieldName, String candidate) { return StringUtils.startsWith(candidate, removeWildcard(fieldName)); } @Override public String extractName(String fieldName, String dynamicFieldName) { Assert.isTrue(match(fieldName, dynamicFieldName), "dynamicFieldName must be derivated from fieldName"); return StringUtils.removeStart(dynamicFieldName, removeWildcard(fieldName)); } @Override public String createName(String fieldName, String name) { return removeWildcard(fieldName) + name; } }; public static WildcardPosition getAppropriate(String fieldName) { if (StringUtils.startsWith(fieldName, Criteria.WILDCARD)) { return WildcardPosition.LEADING; } else { return WildcardPosition.TRAILING; } } String removeWildcard(String fieldName) { return StringUtils.remove(fieldName, Criteria.WILDCARD); } public abstract boolean match(String fieldName, String candidate); public abstract String extractName(String fieldName, String dynamicFieldName); public abstract String createName(String fieldName, String name); } private final MappingContext<? extends SolrPersistentEntity<?>, SolrPersistentProperty> mappingContext; private final EntityInstantiators instantiators = new EntityInstantiators(); @SuppressWarnings("unused") // private ApplicationContext applicationContext; public MappingSolrConverter( MappingContext<? extends SolrPersistentEntity<?>, SolrPersistentProperty> mappingContext) { Assert.notNull(mappingContext, "MappingContext must not be null!"); this.mappingContext = mappingContext; } @Override public MappingContext<? extends SolrPersistentEntity<?>, SolrPersistentProperty> getMappingContext() { return mappingContext; } @Override public <S, R> List<R> read(SolrDocumentList source, Class<R> type) { if (source == null) { return Collections.emptyList(); } List<R> resultList = new ArrayList<>(source.size()); TypeInformation<R> typeInformation = ClassTypeInformation.from(type); for (Map<String, ?> item : source) { resultList.add(read(typeInformation, item)); } return resultList; } @Override public <R> R read(Class<R> type, Map<String, ?> source) { return read(ClassTypeInformation.from(type), source); } @SuppressWarnings("unchecked") protected <S> S read(TypeInformation<S> targetTypeInformation, Map<String, ?> source) { if (source == null) { return null; } Assert.notNull(targetTypeInformation, "TargetTypeInformation must not be null!"); Class<S> rawType = targetTypeInformation.getType(); // in case there's a custom conversion for the document if (hasCustomReadTarget(source.getClass(), rawType)) { return convert(source, rawType); } SolrPersistentEntity<S> entity = (SolrPersistentEntity<S>) mappingContext.getPersistentEntity(rawType).get(); return read(entity, source, null); } private <S> S read(final SolrPersistentEntity<S> entity, final Map<String, ?> source, Object parent) { ParameterValueProvider<SolrPersistentProperty> parameterValueProvider = getParameterValueProvider(entity, source, parent); EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); final S instance = instantiator.createInstance(entity, parameterValueProvider); final PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance), getConversionService()); entity.doWithProperties((PropertyHandler<SolrPersistentProperty>) persistentProperty -> { if (entity.isConstructorArgument(persistentProperty)) { return; } Optional<Object> o = getValue(persistentProperty, source, instance); if (o.isPresent()) { if (o.get() instanceof Collection && !persistentProperty.isCollectionLike()) { Collection<?> c = (Collection<?>) o.get(); if (!c.isEmpty()) { if (c.size() == 1) { accessor.setProperty(persistentProperty, Optional.ofNullable(c.iterator().next())); } else { throw new MappingException(String.format( "Cannot set multiple values %s read from '%s' to non collection property '%s'. Please check your mapping / schema defintion!", c, persistentProperty.getFieldName(), persistentProperty.getName())); } } } else { accessor.setProperty(persistentProperty, o); } } }); return instance; } protected Optional<Object> getValue(SolrPersistentProperty property, Object source, Object parent) { SolrPropertyValueProvider provider = new SolrPropertyValueProvider(source, parent); return provider.getPropertyValue(property); } private ParameterValueProvider<SolrPersistentProperty> getParameterValueProvider(SolrPersistentEntity<?> entity, Map<String, ?> source, Object parent) { SolrPropertyValueProvider provider = new SolrPropertyValueProvider(source, parent); return new PersistentEntityParameterValueProvider<>(entity, provider, Optional.ofNullable(parent)); } @SuppressWarnings("unchecked") @Override public void write(Object source, @SuppressWarnings("rawtypes") Map target) { if (source == null) { return; } Class<?> sourceClass = source.getClass(); if (hasCustomWriteTarget(sourceClass, SolrInputDocument.class) && canConvert(sourceClass, SolrInputDocument.class)) { SolrInputDocument convertedDocument = convert(source, SolrInputDocument.class); target.putAll(convertedDocument); } else { SolrPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(sourceClass); write(source, target, entity); } } @SuppressWarnings("rawtypes") protected void write(Object source, final Map target, SolrPersistentEntity<?> entity) { final PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(source), getConversionService()); entity.doWithProperties((PropertyHandler<SolrPersistentProperty>) persistentProperty -> { Optional<Object> value = accessor.getProperty(persistentProperty); if (!value.isPresent() || persistentProperty.isReadonly()) { return; } if (persistentProperty.containsWildcard() && !persistentProperty.isMap()) { throw new IllegalArgumentException("Field '" + persistentProperty.getFieldName() + "' must not contain wildcards. Consider excluding Field from beeing indexed."); } Collection<SolrInputField> fields; if (persistentProperty.isMap() && persistentProperty.containsWildcard()) { fields = writeWildcardMapPropertyToTarget(target, persistentProperty, (Map<?, ?>) value.get()); } else { fields = writeRegularPropertyToTarget(target, persistentProperty, value.get()); } if (persistentProperty.isBoosted()) { for (SolrInputField field : fields) { field.setBoost(persistentProperty.getBoost()); } } }); if (entity.isBoosted() && target instanceof SolrInputDocument) { ((SolrInputDocument) target).setDocumentBoost(entity.getBoost()); } } private Collection<SolrInputField> writeWildcardMapPropertyToTarget(Map<? super Object, ? super Object> target, SolrPersistentProperty persistentProperty, Map<?, ?> fieldValue) { TypeInformation<?> mapTypeInformation = persistentProperty.getTypeInformation().getMapValueType().get(); Class<?> rawMapType = mapTypeInformation.getType(); String fieldName = persistentProperty.getFieldName(); Collection<SolrInputField> fields = new ArrayList<>(); for (Map.Entry<?, ?> entry : fieldValue.entrySet()) { Object value = entry.getValue(); String key = entry.getKey().toString(); if (persistentProperty.isDynamicProperty()) { key = WildcardPosition.getAppropriate(fieldName).createName(fieldName, key); } SolrInputField field = new SolrInputField(key); if (value instanceof Iterable) { for (Object o : (Iterable<?>) value) { field.addValue(convertToSolrType(rawMapType, o), 1f); } } else { if (rawMapType.isArray()) { for (Object o : (Object[]) value) { field.addValue(convertToSolrType(rawMapType, o), 1f); } } else { field.addValue(convertToSolrType(rawMapType, value), 1f); } } target.put(key, field); fields.add(field); } return fields; } private Collection<SolrInputField> writeRegularPropertyToTarget(final Map<? super Object, ? super Object> target, SolrPersistentProperty persistentProperty, Object fieldValue) { SolrInputField field = new SolrInputField(persistentProperty.getFieldName()); if (persistentProperty.isCollectionLike()) { Collection<?> collection = asCollection(fieldValue); for (Object o : collection) { if (o != null) { field.addValue(convertToSolrType(persistentProperty.getType(), o), 1f); } } } else if (fieldValue instanceof Enum) { field.setValue(this.getConversionService().convert(fieldValue, String.class), 1f); } else { field.setValue(convertToSolrType(persistentProperty.getType(), fieldValue), 1f); } target.put(persistentProperty.getFieldName(), field); return Collections.singleton(field); } private Object convertToSolrType(Class<?> type, Object value) { if (type == null || value == null) { return value; } return getCustomWriteTargetType(value.getClass()) // .filter(targetType -> canConvert(value.getClass(), targetType)) // .map(targetType -> (Object) convert(value, targetType)) // .orElse(value); } private static Collection<?> asCollection(Object source) { if (source instanceof Collection) { return (Collection<?>) source; } return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } private class SolrPropertyValueProvider implements PropertyValueProvider<SolrPersistentProperty> { private final Object source; private final Object parent; public SolrPropertyValueProvider(Object source, Object parent) { this.source = source; this.parent = parent; } @SuppressWarnings("unchecked") @Override public <T> Optional<T> getPropertyValue(SolrPersistentProperty property) { if (source instanceof Map<?, ?>) { return Optional.ofNullable(readValue((Map<String, ?>) source, property, parent)); } return Optional.ofNullable(readValue(source, property.getTypeInformation(), this.parent)); } @SuppressWarnings("unchecked") private <T> T readValue(Map<String, ?> value, SolrPersistentProperty property, Object parent) { if (value == null) { return null; } if (property.containsWildcard()) { return (T) readWildcard(value, property, parent); } if (property.isScoreProperty()) { return (T) readScore(value, property, parent); } return readValue(value.get(property.getFieldName()), property.getTypeInformation(), parent); } @SuppressWarnings("unchecked") private <T> T readScore(Map<String, ?> value, SolrPersistentProperty property, Object parent) { return (T) value.get("score"); } @SuppressWarnings("unchecked") private <T> T readValue(Object value, TypeInformation<?> type, Object parent) { if (value == null) { return null; } Assert.notNull(type, "TypeInformation must not be null!"); Class<?> rawType = type.getType(); if (hasCustomReadTarget(value.getClass(), rawType)) { return (T) convert(value, rawType); } Object documentValue = null; if (value instanceof SolrInputField) { documentValue = ((SolrInputField) value).getValue(); } else { documentValue = value; } if (documentValue instanceof Collection) { return (T) readCollection((Collection<?>) documentValue, type, parent); } else if (canConvert(documentValue.getClass(), rawType)) { return (T) convert(documentValue, rawType); } return (T) documentValue; } private Object readWildcard(Map<String, ?> source, SolrPersistentProperty property, Object parent) { WildcardPosition wildcardPosition = WildcardPosition.getAppropriate(property.getFieldName()); if (property.isMap()) { return readWildcardMap(source, property, parent, wildcardPosition); } else if (property.isCollectionLike()) { return readWildcardCollectionLike(source, property, parent, wildcardPosition); } else { for (Map.Entry<String, ?> potentialMatch : source.entrySet()) { if (wildcardPosition.match(property.getFieldName(), potentialMatch.getKey())) { return getValue(property, potentialMatch.getValue(), parent).orElse(null); } } } return null; } private Object readWildcardCollectionLike(Map<String, ?> source, SolrPersistentProperty property, Object parent, WildcardPosition wildcardPosition) { Class<?> genericTargetType = property.getComponentType().orElse(Object.class); List<Object> values = new ArrayList<>(); for (Map.Entry<String, ?> potentialMatch : source.entrySet()) { if (!wildcardPosition.match(property.getFieldName(), potentialMatch.getKey())) { continue; } Object value = potentialMatch.getValue(); if (value instanceof Iterable) { for (Object o : (Iterable<?>) value) { values.add(readValue(property, o, parent, genericTargetType)); } } else { Object o = readValue(property, potentialMatch.getValue(), parent, genericTargetType); if (o instanceof Collection) { values.addAll((Collection<?>) o); } else { values.add(o); } } } return values.isEmpty() ? null : (property.isArray() ? values.toArray() : values); } private Object readWildcardMap(Map<String, ?> source, SolrPersistentProperty property, Object parent, WildcardPosition wildcardPosition) { Optional<TypeInformation<?>> mapTypeInformation = property.getTypeInformation().getMapValueType(); Class<?> rawMapType = mapTypeInformation.get().getType(); Class<?> genericTargetType; if (mapTypeInformation.get().getTypeArguments() != null && !mapTypeInformation.get().getTypeArguments().isEmpty()) { genericTargetType = mapTypeInformation.get().getTypeArguments().get(0).getType(); } else { genericTargetType = Object.class; } Map<String, Object> values; if (LinkedHashMap.class.isAssignableFrom(property.getActualType())) { values = new LinkedHashMap<>(); } else { values = new HashMap<>(); } for (Map.Entry<String, ?> potentialMatch : source.entrySet()) { String key = potentialMatch.getKey(); if (!wildcardPosition.match(property.getFieldName(), key)) { continue; } if (property.isDynamicProperty()) { key = wildcardPosition.extractName(property.getFieldName(), key); } Object value = potentialMatch.getValue(); if (value instanceof Iterable) { if (rawMapType.isArray() || ClassUtils.isAssignable(rawMapType, value.getClass())) { List<Object> nestedValues = new ArrayList<>(); for (Object o : (Iterable<?>) value) { nestedValues.add(readValue(property, o, parent, genericTargetType)); } values.put(key, (rawMapType.isArray() ? nestedValues.toArray() : nestedValues)); } else { throw new IllegalArgumentException("Incompartible types found. Expected " + rawMapType + " for " + property.getName() + " with name " + property.getFieldName() + ", but found " + value.getClass()); } } else { if (rawMapType.isArray() || ClassUtils.isAssignable(rawMapType, List.class)) { ArrayList<Object> singletonArrayList = new ArrayList<>(1); Object read = readValue(property, value, parent, genericTargetType); singletonArrayList.add(read); values.put(key, (rawMapType.isArray() ? singletonArrayList.toArray() : singletonArrayList)); } else { Optional<Object> o = getValue(property, value, parent); values.put(key, o.isPresent() ? o.get() : null); } } } return values.isEmpty() ? null : values; } private Object readValue(SolrPersistentProperty property, Object o, Object parent, Class<?> target) { Optional<Object> value = getValue(property, o, parent); if (!value.isPresent()) { return null; } if (target == null || target.equals(Object.class)) { return value.get(); } if (canConvert(value.get().getClass(), target)) { return convert(value.get(), target); } return value.get(); } private Object readCollection(Collection<?> source, TypeInformation<?> type, Object parent) { Assert.notNull(type, "Type must not be null!"); Class<?> collectionType = type.getType(); if (CollectionUtils.isEmpty(source)) { return source; } collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class; Collection<Object> items; if (type.getType().isArray()) { items = new ArrayList<>(); } else { items = CollectionFactory.createCollection(collectionType, source.size()); } TypeInformation<?> componentType = type.isCollectionLike() ? type.getComponentType().get() : type; for (Object aSource : source) { items.add(readValue(aSource, componentType, parent)); } return type.getType().isArray() ? convertItemsToArrayOfType(type, items) : items; } private Object convertItemsToArrayOfType(TypeInformation<?> type, Collection<Object> items) { Object[] newArray = (Object[]) java.lang.reflect.Array.newInstance(type.getActualType().getType(), items.size()); Object[] itemsArray = items.toArray(); System.arraycopy(itemsArray, 0, newArray, 0, itemsArray.length); return newArray; } } }