Java tutorial
/* * Copyright 2012-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.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.config.JsonSchemaFormat; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceDescription; import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.json.JsonSchema.AbstractJsonSchemaProperty; import org.springframework.data.rest.webmvc.json.JsonSchema.Definitions; import org.springframework.data.rest.webmvc.json.JsonSchema.EnumProperty; import org.springframework.data.rest.webmvc.json.JsonSchema.Item; import org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty; import org.springframework.data.rest.webmvc.mapping.Associations; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; /** * Converter to create {@link JsonSchema} instances for {@link PersistentEntity}s. * * @author Jon Brisbin * @author Oliver Gierke * @author Greg Turnquist */ public class PersistentEntityToJsonSchemaConverter implements ConditionalGenericConverter { private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); private static final TypeDescriptor SCHEMA_TYPE = TypeDescriptor.valueOf(JsonSchema.class); private static final TypeInformation<?> STRING_TYPE_INFORMATION = ClassTypeInformation.from(String.class); private final Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>(); private final Associations associations; private final PersistentEntities entities; private final MessageSourceAccessor accessor; private final ObjectMapper objectMapper; private final RepositoryRestConfiguration configuration; private final ValueTypeSchemaPropertyCustomizerFactory customizerFactory; /** * Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link PersistentEntities} and * {@link ResourceMappings}. * * @param entities must not be {@literal null}. * @param mappings must not be {@literal null}. * @param accessor must not be {@literal null}. * @param objectMapper must not be {@literal null}. * @param configuration must not be {@literal null}. */ public PersistentEntityToJsonSchemaConverter(PersistentEntities entities, Associations associations, MessageSourceAccessor accessor, ObjectMapper objectMapper, RepositoryRestConfiguration configuration, ValueTypeSchemaPropertyCustomizerFactory customizerFactory) { Assert.notNull(entities, "PersistentEntities must not be null!"); Assert.notNull(associations, "AssociationLinks must not be null!"); Assert.notNull(accessor, "MessageSourceAccessor must not be null!"); Assert.notNull(objectMapper, "ObjectMapper must not be null!"); Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!"); this.entities = entities; this.associations = associations; this.accessor = accessor; this.objectMapper = objectMapper; this.configuration = configuration; this.customizerFactory = customizerFactory; for (TypeInformation<?> domainType : entities.getManagedTypes()) { convertiblePairs.add(new ConvertiblePair(domainType.getType(), JsonSchema.class)); } } /* * (non-Javadoc) * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) */ @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return Class.class.isAssignableFrom(sourceType.getType()) && JsonSchema.class.isAssignableFrom(targetType.getType()); } /* * (non-Javadoc) * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() */ @Override public Set<ConvertiblePair> getConvertibleTypes() { return convertiblePairs; } /** * Converts the given type into a {@link JsonSchema} instance. * * @param domainType must not be {@literal null}. * @return */ public JsonSchema convert(Class<?> domainType) { return (JsonSchema) convert(domainType, STRING_TYPE, SCHEMA_TYPE); } /* * (non-Javadoc) * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) */ @Override public JsonSchema convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { final PersistentEntity<?, ?> persistentEntity = entities.getPersistentEntity((Class<?>) source); final ResourceMetadata metadata = associations.getMappings().getMetadataFor(persistentEntity.getType()); Definitions definitions = new Definitions(); List<AbstractJsonSchemaProperty<?>> propertiesFor = getPropertiesFor(persistentEntity.getType(), metadata, definitions); String title = resolveMessageWithDefault(new ResolvableType(persistentEntity.getType())); return new JsonSchema(title, resolveMessage(metadata.getItemResourceDescription()), propertiesFor, definitions); } private List<AbstractJsonSchemaProperty<?>> getPropertiesFor(Class<?> type, final ResourceMetadata metadata, final Definitions definitions) { final PersistentEntity<?, ?> entity = entities.getPersistentEntity(type); final JacksonMetadata jackson = new JacksonMetadata(objectMapper, type); if (entity == null) { return Collections.<AbstractJsonSchemaProperty<?>>emptyList(); } JsonSchemaPropertyRegistrar registrar = new JsonSchemaPropertyRegistrar(jackson); for (BeanPropertyDefinition definition : jackson) { PersistentProperty<?> persistentProperty = entity.getPersistentProperty(definition.getInternalName()); // First pass, early drops to avoid unnecessary calculation if (persistentProperty != null) { if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(type)) { continue; } if (persistentProperty.isVersionProperty()) { continue; } if (!definition.couldSerialize()) { continue; } } AnnotatedMember primaryMember = definition.getPrimaryMember(); if (primaryMember == null) { continue; } TypeInformation<?> propertyType = persistentProperty == null ? ClassTypeInformation.from(primaryMember.getRawType()) : persistentProperty.getTypeInformation(); TypeInformation<?> actualPropertyType = propertyType.getActualType(); Class<?> rawPropertyType = propertyType.getType(); JsonSchemaFormat format = configuration.getMetadataConfiguration().getSchemaFormatFor(rawPropertyType); ResourceDescription description = persistentProperty == null ? jackson.getFallbackDescription(metadata, definition) : getDescriptionFor(persistentProperty, metadata); JsonSchemaProperty property = getSchemaProperty(definition, propertyType, description); boolean isSyntheticProperty = persistentProperty == null; boolean isNotWritable = !isSyntheticProperty && !persistentProperty.isWritable(); boolean isJacksonReadOnly = !isSyntheticProperty && jackson.isReadOnly(persistentProperty); if (isSyntheticProperty || isNotWritable || isJacksonReadOnly) { property = property.withReadOnly(); } if (format != null) { // Types with explicitly registered format -> value object with format registrar.register(property.withFormat(format), actualPropertyType); continue; } Pattern pattern = configuration.getMetadataConfiguration().getPatternFor(rawPropertyType); if (pattern != null) { registrar.register(property.withPattern(pattern), actualPropertyType); continue; } if (jackson.isValueType()) { registrar.register(property.with(STRING_TYPE_INFORMATION), actualPropertyType); continue; } if (persistentProperty == null) { registrar.register(property, actualPropertyType); continue; } if (configuration.isLookupType(persistentProperty.getActualType())) { registrar.register(property.with(propertyType), actualPropertyType); } else if (associations.isLinkableAssociation(persistentProperty)) { registrar.register(property.asAssociation(), null); } else { if (persistentProperty.isEntity()) { if (!definitions.hasDefinitionFor(propertyType)) { definitions.addDefinition(propertyType, new Item(propertyType, getNestedPropertiesFor(persistentProperty, definitions))); } registrar.register(property.with(propertyType, Definitions.getReference(propertyType)), actualPropertyType); } else { registrar.register(property.with(propertyType), actualPropertyType); } } } return registrar.getProperties(); } private Collection<AbstractJsonSchemaProperty<?>> getNestedPropertiesFor(PersistentProperty<?> property, Definitions descriptors) { if (!property.isEntity()) { return Collections.emptyList(); } return getPropertiesFor(property.getActualType(), associations.getMappings().getMetadataFor(property.getActualType()), descriptors); } private JsonSchemaProperty getSchemaProperty(BeanPropertyDefinition definition, TypeInformation<?> type, ResourceDescription description) { String name = definition.getName(); String title = resolveMessageWithDefault(new ResolvableProperty(definition)); String resolvedDescription = resolveMessage(description); boolean required = definition.isRequired(); Class<?> rawType = type.getType(); if (!rawType.isEnum()) { return new JsonSchemaProperty(name, title, resolvedDescription, required).with(type); } String message = resolveMessage(new DefaultMessageSourceResolvable(description.getMessage())); return new EnumProperty(name, title, rawType, description.getDefaultMessage().equals(resolvedDescription) ? message : resolvedDescription, required); } private ResourceDescription getDescriptionFor(PersistentProperty<?> property, ResourceMetadata metadata) { ResourceMapping propertyMapping = metadata.getMappingFor(property); return propertyMapping.getDescription(); } private String resolveMessageWithDefault(MessageSourceResolvable resolvable) { return resolveMessage(new DefaultingMessageSourceResolvable(resolvable)); } private String resolveMessage(MessageSourceResolvable resolvable) { if (resolvable == null) { return null; } try { return accessor.getMessage(resolvable); } catch (NoSuchMessageException o_O) { if (configuration.getMetadataConfiguration().omitUnresolvableDescriptionKeys()) { return null; } else { throw o_O; } } } /** * Helper to register {@link JsonSchemaProperty} instances after post-processing them. * * @author Oliver Gierke * @since 2.4 */ private class JsonSchemaPropertyRegistrar { private final JacksonMetadata metadata; private final List<AbstractJsonSchemaProperty<?>> properties; /** * Creates a new {@link JsonSchemaPropertyRegistrar} using the given {@link JacksonMetadata}. * * @param metadata must not be {@literal null}. */ public JsonSchemaPropertyRegistrar(JacksonMetadata metadata) { Assert.notNull(metadata, "Metadata must not be null!"); this.metadata = metadata; this.properties = new ArrayList<AbstractJsonSchemaProperty<?>>(); } public void register(JsonSchemaProperty property, TypeInformation<?> type) { if (type == null) { properties.add(property); return; } JsonSerializer<?> serializer = metadata.getTypeSerializer(type.getType()); if ((serializer instanceof JsonSchemaPropertyCustomizer)) { properties.add(((JsonSchemaPropertyCustomizer) serializer).customize(property, type)); return; } if (configuration.isLookupType(type.getType())) { properties.add(customizerFactory.getCustomizerFor(type.getType()).customize(property, type)); return; } properties.add(property); } public List<AbstractJsonSchemaProperty<?>> getProperties() { return properties; } } @RequiredArgsConstructor public static class ValueTypeSchemaPropertyCustomizerFactory { private final @NonNull RepositoryInvokerFactory factory; public JsonSchemaPropertyCustomizer getCustomizerFor(final Class<?> type) { return new JsonSchemaPropertyCustomizer() { /* * (non-Javadoc) * @see org.springframework.data.rest.webmvc.json.JsonSchemaPropertyCustomizer#customize(org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty, org.springframework.data.util.TypeInformation) */ @Override public JsonSchemaProperty customize(JsonSchemaProperty property, TypeInformation<?> type) { List<String> result = new ArrayList<String>(); for (Object element : factory.getInvokerFor(type.getType()).invokeFindAll((Sort) null)) { result.add(element.toString()); } Collections.sort(result); return new EnumProperty(property.getName(), property.getTitle(), result, property.description, true); } }; } } /** * Message source resolvable that defaults the messages to the last segment of the dot-separated code in case the * configured delegate doesn't return a default message itself. * * @author Oliver Gierke * @since 2.4 */ private static class DefaultingMessageSourceResolvable implements MessageSourceResolvable { private static Pattern SPLIT_CAMEL_CASE = Pattern.compile("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])"); private final MessageSourceResolvable delegate; /** * Creates a new {@link DefaultingMessageSourceResolvable} for the given delegate {@link MessageSourceResolvable}. * * @param delegate must not be {@literal null}. */ public DefaultingMessageSourceResolvable(MessageSourceResolvable delegate) { this.delegate = delegate; } /* * (non-Javadoc) * @see org.springframework.context.MessageSourceResolvable#getArguments() */ @Override public Object[] getArguments() { return delegate.getArguments(); } /* * (non-Javadoc) * @see org.springframework.context.MessageSourceResolvable#getCodes() */ @Override public String[] getCodes() { return delegate.getCodes(); } /* * (non-Javadoc) * @see org.springframework.context.MessageSourceResolvable#getDefaultMessage() */ @Override public String getDefaultMessage() { String defaultMessage = delegate.getDefaultMessage(); if (defaultMessage != null) { return defaultMessage; } String[] split = getCodes()[0].split("\\."); String tail = split[split.length - 1]; tail = "_title".equals(tail) ? split[split.length - 2] : tail; return StringUtils.capitalize( StringUtils.collectionToDelimitedString(Arrays.asList(SPLIT_CAMEL_CASE.split(tail)), " ") .toLowerCase(Locale.US)); } } /** * A {@link BeanPropertyDefinition} that can be resolved via a {@link MessageSource}. * * @author Oliver Gierke * @since 2.4.1 */ private static class ResolvableProperty extends DefaultMessageSourceResolvable { private static final long serialVersionUID = -5603381674553244480L; /** * Creates a new {@link ResolvableProperty} for the given {@link BeanPropertyDefinition}. * * @param property must not be {@literal null}. */ public ResolvableProperty(BeanPropertyDefinition property) { super(getCodes(property)); } private static String[] getCodes(BeanPropertyDefinition property) { Assert.notNull(property, "BeanPropertyDefinition must not be null!"); Class<?> owner = property.getPrimaryMember().getDeclaringClass(); String propertyTitle = property.getInternalName().concat("._title"); String localName = owner.getSimpleName().concat(".").concat(propertyTitle); String fullName = owner.getName().concat(".").concat(propertyTitle); return new String[] { fullName, localName, propertyTitle }; } } /** * A type whose title can be resolved through a {@link MessageSource}. * * @author Oliver Gierke * @since 2.4.1 */ private static class ResolvableType extends DefaultMessageSourceResolvable { private static final long serialVersionUID = -7199875272753949857L; /** * Creates a new {@link ResolvableType} for the given type. * * @param type must not be {@literal null}. */ public ResolvableType(Class<?> type) { super(getTitleCodes(type)); } private static String[] getTitleCodes(Class<?> type) { Assert.notNull(type, "Type must not be null!"); return new String[] { type.getName().concat("._title"), type.getSimpleName().concat("._title") }; } } }