/*
* Copyright 2016 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.rest.webmvc.json;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.util.Optionals;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.introspect.ClassIntrospector;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.util.NameTransformer;
/**
* Simple value object to capture a mapping of Jackson mapped field names using
* {@link com.fasterxml.jackson.annotation.JsonUnwrapped} and {@link PersistentProperty} instances.
*
* @author Mark Paluch
* @since 2.6
*/
class WrappedProperties {
private static final ClassIntrospector INTROSPECTOR = new BasicClassIntrospector();
private static final AnnotationIntrospector ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
private final Map<String, List<PersistentProperty<?>>> fieldNameToProperties;
/**
* Creates a new {@link WrappedProperties} instance for the given {@code fieldNameToProperties}.
*
* @param fieldNameToProperties must not be {@literal null}.
*/
private WrappedProperties(Map<String, List<PersistentProperty<?>>> fieldNameToProperties) {
this.fieldNameToProperties = new HashMap<String, List<PersistentProperty<?>>>(fieldNameToProperties);
}
/**
* Creates {@link WrappedProperties} for the given {@link PersistentEntities} and {@link PersistentEntity}.
*
* @param persistentEntities must not be {@literal null}.
* @param entity must not be {@literal null}.
* @param mapper must not be {@literal null}.
* @return
*/
public static WrappedProperties fromJacksonProperties(PersistentEntities persistentEntities,
PersistentEntity<?, ?> entity, ObjectMapper mapper) {
Assert.notNull(entity, "PersistentEntity must not be null!");
JacksonUnwrappedPropertiesResolver resolver = new JacksonUnwrappedPropertiesResolver(persistentEntities, mapper);
return new WrappedProperties(resolver.findUnwrappedPropertyPaths(entity.getType()));
}
public static WrappedProperties none() {
return new WrappedProperties(Collections.emptyMap());
}
/**
* @param fieldName must not be empty or {@literal null}.
* @return {@literal true} if the field name resolves to a {@literal PersistentProperty}.
*/
public boolean hasPersistentPropertiesForField(String fieldName) {
Assert.hasText(fieldName, "Field name must not be null or empty!");
return fieldNameToProperties.containsKey(fieldName);
}
/**
* @param fieldName must not be empty or {@literal null}.
* @return
*/
public List<PersistentProperty<?>> getPersistentProperties(String fieldName) {
Assert.hasText(fieldName, "Field name must not be null or empty!");
return hasPersistentPropertiesForField(fieldName)
? Collections.unmodifiableList(fieldNameToProperties.get(fieldName))
: Collections.<PersistentProperty<?>> emptyList();
}
/**
* This class resolves {@code @JsonUnwrapped} field names to a list of involved {@link PersistentProperty properties}.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
static class JacksonUnwrappedPropertiesResolver {
private final @NonNull PersistentEntities persistentEntities;
private final @NonNull ObjectMapper mapper;
/**
* Resolve {@code @JsonUnwrapped} field names to a list of involved {@link PersistentProperty properties}.
*
* @param type must not be {@literal null}.
* @return
*/
public Map<String, List<PersistentProperty<?>>> findUnwrappedPropertyPaths(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return findUnwrappedPropertyPaths(type, NameTransformer.NOP, false);
}
private Map<String, List<PersistentProperty<?>>> findUnwrappedPropertyPaths(Class<?> type,
NameTransformer nameTransformer, boolean considerRegularProperties) {
return persistentEntities.getPersistentEntity(type).map(entity -> {
Map<String, List<PersistentProperty<?>>> mapping = new HashMap<String, List<PersistentProperty<?>>>();
for (BeanPropertyDefinition property : getMappedProperties(entity)) {
Optionals.ifAllPresent(entity.getPersistentProperty(property.getInternalName()), //
findAnnotatedMember(property), //
(prop, member) -> {
if (isJsonUnwrapped(member)) {
mapping.putAll(findUnwrappedPropertyPaths(nameTransformer, member, prop));
} else if (considerRegularProperties) {
mapping.put(nameTransformer.transform(property.getName()),
Collections.<PersistentProperty<?>> singletonList(prop));
}
});
}
return mapping;
}).orElse(Collections.emptyMap());
}
private Map<String, List<PersistentProperty<?>>> findUnwrappedPropertyPaths(NameTransformer nameTransformer,
AnnotatedMember annotatedMember, PersistentProperty<?> persistentProperty) {
Map<String, List<PersistentProperty<?>>> mapping = new HashMap<String, List<PersistentProperty<?>>>();
NameTransformer propertyNameTransformer = NameTransformer.chainedTransformer(nameTransformer,
ANNOTATION_INTROSPECTOR.findUnwrappingNameTransformer(annotatedMember));
Map<String, List<PersistentProperty<?>>> nestedProperties = findUnwrappedPropertyPaths(
annotatedMember.getRawType(), propertyNameTransformer, true);
for (Entry<String, List<PersistentProperty<?>>> entry : nestedProperties.entrySet()) {
List<PersistentProperty<?>> persistentProperties = new ArrayList<PersistentProperty<?>>();
persistentProperties.add(persistentProperty);
persistentProperties.addAll(entry.getValue());
mapping.put(entry.getKey(), persistentProperties);
}
return mapping;
}
private List<BeanPropertyDefinition> getMappedProperties(PersistentEntity<?, ?> entity) {
List<BeanPropertyDefinition> properties = getBeanDescription(entity.getType()).findProperties();
List<BeanPropertyDefinition> withInternalName = new ArrayList<BeanPropertyDefinition>(properties.size());
for (BeanPropertyDefinition property : properties) {
Optionals.ifAllPresent(findAnnotatedMember(property), //
entity.getPersistentProperty(property.getInternalName()), //
(member, prop) -> withInternalName.add(property));
}
return withInternalName;
}
private BeanDescription getBeanDescription(Class<?> type) {
return INTROSPECTOR.forDeserialization(mapper.getDeserializationConfig(), mapper.constructType(type),
mapper.getDeserializationConfig());
}
private static Optional<AnnotatedMember> findAnnotatedMember(BeanPropertyDefinition property) {
if (property.getPrimaryMember() != null) {
return Optional.of(property.getPrimaryMember());
}
if (property.getGetter() != null) {
return Optional.of(property.getGetter());
}
if (property.getSetter() != null) {
return Optional.of(property.getSetter());
}
return Optional.empty();
}
private static boolean isJsonUnwrapped(AnnotatedMember primaryMember) {
return primaryMember.hasAnnotation(JsonUnwrapped.class)
&& primaryMember.getAnnotation(JsonUnwrapped.class).enabled();
}
}
}