DATAREST-644 - Improvements to JSON Schema output.
Fixed JSON Schema output use "definitions" keyword instead of the previously used wrong "descriptors". We now also include the title attribute for properties, using rest.description.$type.$property._title as i18n key. Titles are now also defaulted to the camel-case property name split up, lowercased and capitalized, e.g. "orderDate" will become "Order date". JacksonMetadata now allows obtaining the Jackson serializer being used for a given type and exposes whether a property is considered read-only for Jackson. Introduced JsonSchemaPropertyCustomizer to potentially tweak the JSON schema property definition. This is helpful in case custom JsonSerializers are implemented to also reflect the change in representation in the schema.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
* Copyright 2012-2015 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.
|
||||
@@ -22,7 +22,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation to descibe semantics of a resource.
|
||||
* Annotation to describe semantics of a resource.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public class SimpleResourceDescription extends ResolvableResourceDescriptionSupport {
|
||||
|
||||
protected static final String DEFAULT_KEY_PREFIX = "rest.description";
|
||||
public static final String DEFAULT_KEY_PREFIX = "rest.description";
|
||||
protected static final MediaType DEFAULT_MEDIA_TYPE = MediaType.TEXT_PLAIN;
|
||||
|
||||
private final String message;
|
||||
|
||||
@@ -47,9 +47,13 @@ public class TypedResourceDescription extends SimpleResourceDescription {
|
||||
}
|
||||
|
||||
public static ResourceDescription defaultFor(String rel, PersistentProperty<?> property) {
|
||||
return defaultFor(rel, property.getName(), property.getType());
|
||||
}
|
||||
|
||||
String message = String.format("%s.%s.%s", DEFAULT_KEY_PREFIX, rel, property.getName());
|
||||
return new TypedResourceDescription(message, DEFAULT_MEDIA_TYPE, property.getType());
|
||||
public static ResourceDescription defaultFor(String rel, String name, Class<?> type) {
|
||||
|
||||
String message = String.format("%s.%s.%s", DEFAULT_KEY_PREFIX, rel, name);
|
||||
return new TypedResourceDescription(message, DEFAULT_MEDIA_TYPE, type);
|
||||
}
|
||||
|
||||
public static ResourceDescription defaultFor(String rel, Class<?> type) {
|
||||
|
||||
@@ -22,15 +22,21 @@ import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.rest.core.annotation.Description;
|
||||
import org.springframework.data.rest.core.mapping.AnnotationBasedResourceDescription;
|
||||
import org.springframework.data.rest.core.mapping.ResourceDescription;
|
||||
import org.springframework.data.rest.core.mapping.SimpleResourceDescription;
|
||||
import org.springframework.data.rest.core.mapping.ResourceMetadata;
|
||||
import org.springframework.data.rest.core.mapping.TypedResourceDescription;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.databind.BeanDescription;
|
||||
import com.fasterxml.jackson.databind.DeserializationConfig;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationConfig;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
|
||||
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
|
||||
|
||||
/**
|
||||
* Value object to abstract Jackson based bean metadata of a given type.
|
||||
@@ -40,7 +46,9 @@ import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
*/
|
||||
public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final List<BeanPropertyDefinition> definitions;
|
||||
private final List<BeanPropertyDefinition> deserializationDefinitions;
|
||||
private final boolean isValue;
|
||||
|
||||
/**
|
||||
@@ -54,12 +62,19 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
Assert.notNull(mapper, "ObjectMapper must not be null!");
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
this.mapper = mapper;
|
||||
|
||||
SerializationConfig serializationConfig = mapper.getSerializationConfig();
|
||||
JavaType javaType = serializationConfig.constructType(type);
|
||||
BeanDescription description = serializationConfig.introspect(javaType);
|
||||
|
||||
this.definitions = description.findProperties();
|
||||
this.isValue = description.findJsonValueMethod() != null;
|
||||
|
||||
DeserializationConfig deserializationConfig = mapper.getDeserializationConfig();
|
||||
JavaType deserializationType = deserializationConfig.constructType(type);
|
||||
|
||||
this.deserializationDefinitions = deserializationConfig.introspect(deserializationType).findProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,30 +88,27 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
|
||||
Assert.notNull(property, "PersistentProperty must not be null!");
|
||||
|
||||
for (BeanPropertyDefinition definition : definitions) {
|
||||
if (definition.getInternalName().equals(property.getName())) {
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return getDefinitionFor(property, definitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fallback {@link ResourceDescription} to be used for the given {@link BeanPropertyDefinition}.
|
||||
*
|
||||
* @param ownerMetadata must not be {@literal null}.
|
||||
* @param definition must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public ResourceDescription getFallbackDescription(BeanPropertyDefinition definition) {
|
||||
public ResourceDescription getFallbackDescription(ResourceMetadata ownerMetadata, BeanPropertyDefinition definition) {
|
||||
|
||||
Assert.notNull(ownerMetadata, "Owner's resource metadata must not be null!");
|
||||
Assert.notNull(definition, "BeanPropertyDefinition must not be null!");
|
||||
|
||||
AnnotatedMember member = definition.getPrimaryMember();
|
||||
Description description = member.getAnnotation(Description.class);
|
||||
ResourceDescription fallback = SimpleResourceDescription.defaultFor(definition.getName());
|
||||
ResourceDescription fallback = TypedResourceDescription.defaultFor(ownerMetadata.getItemResourceRel(),
|
||||
definition.getInternalName(), definition.getPrimaryMember().getRawType());
|
||||
|
||||
return description == null ? null : new AnnotationBasedResourceDescription(description, fallback);
|
||||
return description == null ? fallback : new AnnotationBasedResourceDescription(description, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +118,9 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
* @return
|
||||
*/
|
||||
public boolean isExported(PersistentProperty<?> property) {
|
||||
|
||||
Assert.notNull(property, "PersistentProperty must not be null!");
|
||||
|
||||
return getDefinitionFor(property) != null;
|
||||
}
|
||||
|
||||
@@ -118,6 +133,46 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
return isValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link PersistentProperty} is considered read-only by Jackson.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean isReadOnly(PersistentProperty<?> property) {
|
||||
|
||||
BeanPropertyDefinition definition = getDefinitionFor(property, deserializationDefinitions);
|
||||
return definition == null ? false : !definition.couldDeserialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JsonSerializer} for the given type, or {@literal null} if none available.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSerializer<?> getTypeSerializer(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
try {
|
||||
|
||||
SerializerProvider provider = mapper.getSerializerProvider();
|
||||
|
||||
if (!(provider instanceof DefaultSerializerProvider)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
provider = ((DefaultSerializerProvider) provider).createInstance(mapper.getSerializationConfig(),
|
||||
mapper.getSerializerFactory());
|
||||
|
||||
return provider.findValueSerializer(type);
|
||||
|
||||
} catch (JsonMappingException o_O) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
@@ -126,4 +181,23 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
public Iterator<BeanPropertyDefinition> iterator() {
|
||||
return definitions.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the {@link BeanPropertyDefinition} for the given {@link PersistentProperty} within the given definitions.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @param definitions must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static BeanPropertyDefinition getDefinitionFor(PersistentProperty<?> property,
|
||||
Iterable<BeanPropertyDefinition> definitions) {
|
||||
|
||||
for (BeanPropertyDefinition definition : definitions) {
|
||||
if (definition.getInternalName().equals(property.getName())) {
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,28 +54,28 @@ public class JsonSchema {
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final PropertiesContainer container;
|
||||
private final Descriptors descriptors;
|
||||
private final Definitions definitions;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JsonSchema} instance for the given title, description, {@link JsonSchemaProperty}s and
|
||||
* {@link Descriptors}.
|
||||
* Creates a new {@link JsonSchema} instance for the given title, description, {@link AbstractJsonSchemaProperty}s and
|
||||
* {@link Definitions}.
|
||||
*
|
||||
* @param title must not be {@literal null} or empty.
|
||||
* @param description can be {@literal null}.
|
||||
* @param properties must not be {@literal null}.
|
||||
* @param descriptors must not be {@literal null}.
|
||||
* @param definitions must not be {@literal null}.
|
||||
*/
|
||||
public JsonSchema(String title, String description, Collection<JsonSchemaProperty<?>> properties,
|
||||
Descriptors descriptors) {
|
||||
public JsonSchema(String title, String description, Collection<AbstractJsonSchemaProperty<?>> properties,
|
||||
Definitions definitions) {
|
||||
|
||||
Assert.hasText(title, "Title must not be null or empty!");
|
||||
Assert.notNull(properties, "JsonSchemaProperties must not be null!");
|
||||
Assert.notNull(descriptors, "Desciptors must not be null!");
|
||||
Assert.notNull(definitions, "Definitions must not be null!");
|
||||
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.container = new PropertiesContainer(properties);
|
||||
this.descriptors = descriptors;
|
||||
this.definitions = definitions;
|
||||
}
|
||||
|
||||
@JsonProperty("$schema")
|
||||
@@ -100,12 +100,9 @@ public class JsonSchema {
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the descriptors
|
||||
*/
|
||||
@JsonUnwrapped
|
||||
public Descriptors getDescriptors() {
|
||||
return descriptors;
|
||||
public Definitions getDefinitions() {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,9 +172,9 @@ public class JsonSchema {
|
||||
* @param type must not be {@literal null}.
|
||||
* @param properties must not be {@literal null}.
|
||||
*/
|
||||
public Item(TypeInformation<?> type, Collection<JsonSchemaProperty<?>> properties) {
|
||||
public Item(TypeInformation<?> type, Collection<AbstractJsonSchemaProperty<?>> properties) {
|
||||
|
||||
this.type = toJsonSchemaType(type);
|
||||
this.type = toJsonSchemaType(type.getActualType());
|
||||
this.properties = new PropertiesContainer(properties);
|
||||
}
|
||||
|
||||
@@ -200,22 +197,22 @@ public class JsonSchema {
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
static class PropertiesContainer {
|
||||
|
||||
public final Map<String, JsonSchemaProperty<?>> properties;
|
||||
public final Map<String, AbstractJsonSchemaProperty<?>> properties;
|
||||
public final Collection<String> requiredProperties;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PropertiesContainer} for the given {@link JsonSchemaProperty}s.
|
||||
* Creates a new {@link PropertiesContainer} for the given {@link AbstractJsonSchemaProperty}s.
|
||||
*
|
||||
* @param properties must not be {@literal null}.
|
||||
*/
|
||||
public PropertiesContainer(Collection<JsonSchemaProperty<?>> properties) {
|
||||
public PropertiesContainer(Collection<AbstractJsonSchemaProperty<?>> properties) {
|
||||
|
||||
Assert.notNull(properties, "JsonSchemaPropertys must not be null!");
|
||||
|
||||
this.properties = new HashMap<String, JsonSchema.JsonSchemaProperty<?>>();
|
||||
this.properties = new HashMap<String, JsonSchema.AbstractJsonSchemaProperty<?>>();
|
||||
this.requiredProperties = new ArrayList<String>();
|
||||
|
||||
for (JsonSchemaProperty<?> property : properties) {
|
||||
for (AbstractJsonSchemaProperty<?> property : properties) {
|
||||
this.properties.put(property.getName(), property);
|
||||
|
||||
if (property.isRequired()) {
|
||||
@@ -226,39 +223,39 @@ public class JsonSchema {
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object to abstract a {@link Map} of JSON Schema descriptors.
|
||||
* Value object to abstract a {@link Map} of JSON Schema definitions.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class Descriptors {
|
||||
public static class Definitions {
|
||||
|
||||
private final Map<String, Item> descriptors;
|
||||
private final Map<String, Item> definitions;
|
||||
|
||||
public Descriptors() {
|
||||
this.descriptors = new HashMap<String, Item>();
|
||||
public Definitions() {
|
||||
this.definitions = new HashMap<String, Item>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the descriptors
|
||||
*/
|
||||
public Map<String, Item> getDescriptors() {
|
||||
return descriptors;
|
||||
public Map<String, Item> getDefinitions() {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
boolean hasDescriptorFor(TypeInformation<?> type) {
|
||||
return this.descriptors.containsKey(typeKey(type));
|
||||
boolean hasDefinitionFor(TypeInformation<?> type) {
|
||||
return this.definitions.containsKey(typeKey(type));
|
||||
}
|
||||
|
||||
String addDescriptor(TypeInformation<?> type, Item item) {
|
||||
String addDefinition(TypeInformation<?> type, Item item) {
|
||||
|
||||
String reference = typeKey(type);
|
||||
this.descriptors.put(reference, item);
|
||||
this.definitions.put(reference, item);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
static String getReference(TypeInformation<?> type) {
|
||||
return String.format("#/descriptors/%s", typeKey(type));
|
||||
return String.format("#/definitions/%s", typeKey(type));
|
||||
}
|
||||
|
||||
static String typeKey(TypeInformation<?> type) {
|
||||
@@ -273,16 +270,22 @@ public class JsonSchema {
|
||||
* @since 2.3
|
||||
*/
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
abstract static class JsonSchemaProperty<T extends JsonSchemaProperty<T>> {
|
||||
abstract static class AbstractJsonSchemaProperty<T extends AbstractJsonSchemaProperty<T>> {
|
||||
|
||||
private final String name;
|
||||
private final String title;
|
||||
private final boolean required;
|
||||
|
||||
private boolean readOnly;
|
||||
|
||||
protected JsonSchemaProperty(String name, boolean required) {
|
||||
protected AbstractJsonSchemaProperty(String name, boolean required) {
|
||||
this(name, null, required);
|
||||
}
|
||||
|
||||
protected AbstractJsonSchemaProperty(String name, String title, boolean required) {
|
||||
|
||||
this.name = name;
|
||||
this.title = title;
|
||||
this.required = required;
|
||||
this.readOnly = false;
|
||||
}
|
||||
@@ -292,6 +295,10 @@ public class JsonSchema {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
private boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
@@ -313,7 +320,7 @@ public class JsonSchema {
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
*/
|
||||
static class Property extends JsonSchemaProperty<Property> {
|
||||
public static class JsonSchemaProperty extends AbstractJsonSchemaProperty<JsonSchemaProperty> {
|
||||
|
||||
private static final TypeInformation<?> STRING_TYPE_INFORMATION = ClassTypeInformation.from(String.class);
|
||||
|
||||
@@ -325,19 +332,38 @@ public class JsonSchema {
|
||||
public @JsonProperty("$ref") String reference;
|
||||
public Map<String, String> items;
|
||||
|
||||
public Property(String name, String description, boolean required) {
|
||||
JsonSchemaProperty(String name, String title, String description, boolean required) {
|
||||
|
||||
super(name, required);
|
||||
super(name, title, required);
|
||||
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
Property with(TypeInformation<?> type) {
|
||||
/**
|
||||
* Configures the {@link JsonSchemaProperty} to reflect the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSchemaProperty withType(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
return with(ClassTypeInformation.from(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link JsonSchemaProperty} to reflect the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSchemaProperty with(TypeInformation<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
this.type = toJsonSchemaType(type);
|
||||
|
||||
if (isDate(type)) {
|
||||
return with(JsonSchemaFormat.DATE_TIME);
|
||||
return withFormat(JsonSchemaFormat.DATE_TIME);
|
||||
}
|
||||
|
||||
if (type.isCollectionLike()) {
|
||||
@@ -352,17 +378,47 @@ public class JsonSchema {
|
||||
return this;
|
||||
}
|
||||
|
||||
Property with(JsonSchemaFormat format) {
|
||||
/**
|
||||
* Configures the given {@link JsonSchemaFormat} to be exposed on the current {@link JsonSchemaProperty}.
|
||||
*
|
||||
* @param format must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSchemaProperty withFormat(JsonSchemaFormat format) {
|
||||
|
||||
Assert.notNull(format, "Format must not be null!");
|
||||
|
||||
this.format = format;
|
||||
return with(STRING_TYPE_INFORMATION);
|
||||
}
|
||||
|
||||
Property with(Pattern pattern) {
|
||||
/**
|
||||
* Configures the {@link JsonSchemaProperty} to require the given regular expression as pattern.
|
||||
*
|
||||
* @param regex must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSchemaProperty withRegex(String regex) {
|
||||
|
||||
Assert.hasText(regex, "Regular expression must not be null or empty!");
|
||||
return withPattern(Pattern.compile(regex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link JsonSchemaProperty} to require the given {@link Pattern}.
|
||||
*
|
||||
* @param pattern must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSchemaProperty withPattern(Pattern pattern) {
|
||||
|
||||
Assert.notNull(pattern, "Pattern must not be null!");
|
||||
|
||||
this.pattern = pattern.toString();
|
||||
return with(STRING_TYPE_INFORMATION);
|
||||
}
|
||||
|
||||
Property with(TypeInformation<?> type, String reference) {
|
||||
JsonSchemaProperty with(TypeInformation<?> type, String reference) {
|
||||
|
||||
if (type.isCollectionLike()) {
|
||||
|
||||
@@ -383,19 +439,19 @@ public class JsonSchema {
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Property} representing enumerations. Will cause all valid values to be rendered in a nested
|
||||
* A {@link JsonSchemaProperty} representing enumerations. Will cause all valid values to be rendered in a nested
|
||||
* {@literal enum} property.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
*/
|
||||
static class EnumProperty extends Property {
|
||||
static class EnumProperty extends JsonSchemaProperty {
|
||||
|
||||
private final List<String> values;
|
||||
|
||||
public EnumProperty(String name, Class<?> type, String description, boolean required) {
|
||||
public EnumProperty(String name, String title, Class<?> type, String description, boolean required) {
|
||||
|
||||
super(name, description, required);
|
||||
super(name, title, description, required);
|
||||
|
||||
this.values = new ArrayList<String>();
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2015 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 org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
/**
|
||||
* Callback interface to customize the {@link JsonSchemaProperty} created by default for a given type.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.4
|
||||
* @soundtrack Superflight - Four Sided Cube (Bellyjam)
|
||||
*/
|
||||
public interface JsonSchemaPropertyCustomizer {
|
||||
|
||||
/**
|
||||
* Returns the customized {@link JsonSchemaProperty} based on the given one and the given type.
|
||||
*
|
||||
* @param property will never be {@literal null}.
|
||||
* @param type will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
JsonSchemaProperty customize(JsonSchemaProperty property, TypeInformation<?> type);
|
||||
}
|
||||
@@ -16,15 +16,18 @@
|
||||
package org.springframework.data.rest.webmvc.json;
|
||||
|
||||
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.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;
|
||||
@@ -37,16 +40,19 @@ 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.Descriptors;
|
||||
import org.springframework.data.rest.core.mapping.SimpleResourceDescription;
|
||||
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.json.JsonSchema.Property;
|
||||
import org.springframework.data.rest.webmvc.mapping.AssociationLinks;
|
||||
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.BeanPropertyDefinition;
|
||||
|
||||
@@ -138,95 +144,115 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
final PersistentEntity<?, ?> persistentEntity = entities.getPersistentEntity((Class<?>) source);
|
||||
final ResourceMetadata metadata = mappings.getMetadataFor(persistentEntity.getType());
|
||||
|
||||
Descriptors descriptors = new Descriptors();
|
||||
List<JsonSchemaProperty<?>> propertiesFor = getPropertiesFor(persistentEntity.getType(), metadata, descriptors);
|
||||
Definitions descriptors = new Definitions();
|
||||
List<AbstractJsonSchemaProperty<?>> propertiesFor = getPropertiesFor(persistentEntity.getType(), metadata,
|
||||
descriptors);
|
||||
|
||||
return new JsonSchema(persistentEntity.getName(), resolveMessage(metadata.getItemResourceDescription()),
|
||||
propertiesFor, descriptors);
|
||||
String title = resolveMessageWithDefault(new DefaultMessageSourceResolvable(
|
||||
SimpleResourceDescription.DEFAULT_KEY_PREFIX.concat(".").concat(persistentEntity.getType().getSimpleName())));
|
||||
|
||||
return new JsonSchema(title, resolveMessage(metadata.getItemResourceDescription()), propertiesFor, descriptors);
|
||||
}
|
||||
|
||||
private List<JsonSchemaProperty<?>> getPropertiesFor(Class<?> type, final ResourceMetadata metadata,
|
||||
final Descriptors descriptors) {
|
||||
private List<AbstractJsonSchemaProperty<?>> getPropertiesFor(Class<?> type, final ResourceMetadata metadata,
|
||||
final Definitions descriptors) {
|
||||
|
||||
final PersistentEntity<?, ?> entity = entities.getPersistentEntity(type);
|
||||
final JacksonMetadata jackson = new JacksonMetadata(objectMapper, type);
|
||||
final AssociationLinks associationLinks = new AssociationLinks(mappings);
|
||||
|
||||
if (entity == null) {
|
||||
return Collections.<JsonSchemaProperty<?>> emptyList();
|
||||
return Collections.<AbstractJsonSchemaProperty<?>> emptyList();
|
||||
}
|
||||
|
||||
final List<JsonSchemaProperty<?>> properties = new ArrayList<JsonSchema.JsonSchemaProperty<?>>();
|
||||
JsonSchemaPropertyRegistrar registrar = new JsonSchemaPropertyRegistrar(jackson);
|
||||
|
||||
// final List<JsonSchemaProperty<?>> properties = new ArrayList<JsonSchema.JsonSchemaProperty<?>>();
|
||||
|
||||
for (BeanPropertyDefinition definition : jackson) {
|
||||
|
||||
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(definition.getInternalName());
|
||||
TypeInformation<?> propertyType = persistentProperty == null ? ClassTypeInformation.from(definition
|
||||
.getPrimaryMember().getRawType()) : persistentProperty.getTypeInformation();
|
||||
|
||||
// First pass, early drops to avoid unnecessary calculation
|
||||
if (persistentProperty != null) {
|
||||
|
||||
if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistentProperty.isVersionProperty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TypeInformation<?> propertyType = persistentProperty == null
|
||||
? ClassTypeInformation.from(definition.getPrimaryMember().getRawType())
|
||||
: persistentProperty.getTypeInformation();
|
||||
TypeInformation<?> actualPropertyType = propertyType.getActualType();
|
||||
Class<?> rawPropertyType = propertyType.getType();
|
||||
|
||||
JsonSchemaFormat format = configuration.metadataConfiguration().getSchemaFormatFor(rawPropertyType);
|
||||
ResourceDescription description = persistentProperty == null ? jackson.getFallbackDescription(definition)
|
||||
: getDescriptionFor(persistentProperty, metadata);
|
||||
Property property = getSchemaProperty(definition, propertyType, description);
|
||||
ResourceDescription description = persistentProperty == null
|
||||
? jackson.getFallbackDescription(metadata, definition) : getDescriptionFor(persistentProperty, metadata);
|
||||
JsonSchemaProperty property = getSchemaProperty(definition, propertyType, description);
|
||||
|
||||
if (persistentProperty != null && !persistentProperty.isWritable()) {
|
||||
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
|
||||
properties.add(property.with(format));
|
||||
registrar.register(property.withFormat(format), actualPropertyType);
|
||||
continue;
|
||||
}
|
||||
|
||||
Pattern pattern = configuration.metadataConfiguration().getPatternFor(rawPropertyType);
|
||||
|
||||
if (pattern != null) {
|
||||
properties.add(property.with(pattern));
|
||||
registrar.register(property.withPattern(pattern), actualPropertyType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (jackson.isValueType()) {
|
||||
properties.add(property.with(STRING_TYPE_INFORMATION));
|
||||
registrar.register(property.with(STRING_TYPE_INFORMATION), actualPropertyType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistentProperty == null) {
|
||||
properties.add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(type)) {
|
||||
registrar.register(property, actualPropertyType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (associationLinks.isLinkableAssociation(persistentProperty)) {
|
||||
properties.add(property.with(JsonSchemaFormat.URI));
|
||||
registrar.register(property.withFormat(JsonSchemaFormat.URI), null);
|
||||
} else {
|
||||
|
||||
if (persistentProperty.isEntity()) {
|
||||
|
||||
if (!descriptors.hasDescriptorFor(propertyType)) {
|
||||
descriptors.addDescriptor(propertyType,
|
||||
if (!descriptors.hasDefinitionFor(propertyType)) {
|
||||
descriptors.addDefinition(propertyType,
|
||||
new Item(propertyType, getNestedPropertiesFor(persistentProperty, descriptors)));
|
||||
}
|
||||
|
||||
properties.add(property.with(propertyType, Descriptors.getReference(propertyType)));
|
||||
registrar.register(property.with(propertyType, Definitions.getReference(propertyType)), actualPropertyType);
|
||||
|
||||
} else {
|
||||
|
||||
properties.add(property.with(propertyType));
|
||||
registrar.register(property.with(propertyType), actualPropertyType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
return registrar.getProperties();
|
||||
}
|
||||
|
||||
private Collection<JsonSchemaProperty<?>> getNestedPropertiesFor(PersistentProperty<?> property,
|
||||
Descriptors descriptors) {
|
||||
private Collection<AbstractJsonSchemaProperty<?>> getNestedPropertiesFor(PersistentProperty<?> property,
|
||||
Definitions descriptors) {
|
||||
|
||||
if (!property.isEntity()) {
|
||||
return Collections.emptyList();
|
||||
@@ -235,20 +261,24 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
return getPropertiesFor(property.getActualType(), mappings.getMetadataFor(property.getActualType()), descriptors);
|
||||
}
|
||||
|
||||
private Property getSchemaProperty(BeanPropertyDefinition definition, TypeInformation<?> type,
|
||||
private JsonSchemaProperty getSchemaProperty(BeanPropertyDefinition definition, TypeInformation<?> type,
|
||||
ResourceDescription description) {
|
||||
|
||||
String name = definition.getName();
|
||||
String title = resolveMessageWithDefault(
|
||||
new DefaultMessageSourceResolvable(description.getMessage().concat("._title")));
|
||||
String resolvedDescription = resolveMessage(description);
|
||||
boolean required = definition.isRequired();
|
||||
Class<?> rawType = type.getType();
|
||||
|
||||
if (!rawType.isEnum()) {
|
||||
return new Property(name, resolvedDescription, required);
|
||||
return new JsonSchemaProperty(name, title, resolvedDescription, required).with(type);
|
||||
}
|
||||
|
||||
return new EnumProperty(name, rawType, description.getDefaultMessage().equals(resolvedDescription) ? null
|
||||
: resolvedDescription, required);
|
||||
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) {
|
||||
@@ -257,6 +287,10 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
return propertyMapping.getDescription();
|
||||
}
|
||||
|
||||
private String resolveMessageWithDefault(MessageSourceResolvable resolvable) {
|
||||
return resolveMessage(new DefaultingMessageSourceResolvable(resolvable));
|
||||
}
|
||||
|
||||
private String resolveMessage(MessageSourceResolvable resolvable) {
|
||||
|
||||
if (resolvable == null) {
|
||||
@@ -274,4 +308,112 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to register {@link JsonSchemaProperty} instances after post-processing them.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.4
|
||||
*/
|
||||
private static 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(property);
|
||||
return;
|
||||
}
|
||||
|
||||
properties.add(((JsonSchemaPropertyCustomizer) serializer).customize(property, type));
|
||||
}
|
||||
|
||||
public List<AbstractJsonSchemaProperty<?>> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2015 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 static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty.Access;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link JacksonMetadata}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @soundtrack Four Sided Cube - Bad Day's Rememberance (Bunch of Sides)
|
||||
*/
|
||||
public class JacksonMetadataUnitTests {
|
||||
|
||||
/**
|
||||
* @see DATAREST-644
|
||||
*/
|
||||
@Test
|
||||
public void testname() {
|
||||
|
||||
MongoMappingContext context = new MongoMappingContext();
|
||||
MongoPersistentEntity<?> entity = context.getPersistentEntity(User.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
JacksonMetadata metadata = new JacksonMetadata(mapper, User.class);
|
||||
|
||||
MongoPersistentProperty property = entity.getPersistentProperty("username");
|
||||
|
||||
assertThat(metadata.isExported(property), is(true));
|
||||
assertThat(metadata.isReadOnly(property), is(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-644
|
||||
*/
|
||||
@Test
|
||||
public void detectsCustomSerializerFortType() {
|
||||
|
||||
JsonSerializer<?> serializer = new JacksonMetadata(new ObjectMapper(), SomeBean.class)
|
||||
.getTypeSerializer(SomeBean.class);
|
||||
|
||||
assertThat(serializer, is(instanceOf(SomeBeanSerializer.class)));
|
||||
}
|
||||
|
||||
static class User {
|
||||
|
||||
private String username;
|
||||
|
||||
@JsonProperty(access = Access.READ_ONLY)
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerialize(using = SomeBeanSerializer.class)
|
||||
static class SomeBean {}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class SomeBeanSerializer extends StdSerializer<SomeBean> {
|
||||
|
||||
public SomeBeanSerializer() {
|
||||
super(SomeBean.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(SomeBean value, JsonGenerator gen, SerializerProvider provider) throws IOException {}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchema.Property;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class JsonSchemaUnitTests {
|
||||
@Test
|
||||
public void considersNumberPrimitivesJsonSchemaNumbers() {
|
||||
|
||||
Property property = new JsonSchema.Property("foo", "bar", false);
|
||||
JsonSchemaProperty property = new JsonSchemaProperty("foo", null, "bar", false);
|
||||
|
||||
assertThat(property.with(type.getProperty("foo")).type, is("number"));
|
||||
}
|
||||
|
||||
@@ -114,12 +114,12 @@ public class PersistentEntityToJsonSchemaConverterUnitTests {
|
||||
constraints.add(new Constraint("$.properties.id", is(nullValue()), "Does NOT have descriptor for id property"));
|
||||
constraints.add(new Constraint("$.properties.firstname.type", is("string"), "Exposes firstname as String"));
|
||||
constraints
|
||||
.add(new Constraint("$.descriptors.address", is(notNullValue()), "Exposes nested objects as descriptors."));
|
||||
constraints.add(new Constraint("$.descriptors.address.type", is("object"), "Nested entity is of type 'object'"));
|
||||
.add(new Constraint("$.definitions.address", is(notNullValue()), "Exposes nested objects as definitions."));
|
||||
constraints.add(new Constraint("$.definitions.address.type", is("object"), "Nested entity is of type 'object'"));
|
||||
constraints.add(
|
||||
new Constraint("$.descriptors.address.properties.zipCode", is(notNullValue()), "Exposes nested properties"));
|
||||
new Constraint("$.definitions.address.properties.zipCode", is(notNullValue()), "Exposes nested properties"));
|
||||
constraints.add(
|
||||
new Constraint("$.descriptors.address.requiredProperties[0]", is("zipCode"), "Lists nested required property"));
|
||||
new Constraint("$.definitions.address.requiredProperties[0]", is("zipCode"), "Lists nested required property"));
|
||||
constraints.add(new Constraint("$.properties.gender.type", is("string"), "Enums are strings."));
|
||||
constraints.add(new Constraint("$.properties.gender.enum", is(notNullValue()), "Exposes enum values."));
|
||||
constraints
|
||||
@@ -136,12 +136,16 @@ public class PersistentEntityToJsonSchemaConverterUnitTests {
|
||||
new Constraint("$.properties.shippingAddresses.type", is("array"), "Exposes collection of complex types."));
|
||||
constraints
|
||||
.add(new Constraint("$.properties.shippingAddresses.uniqueItems", is(true), "Exposes uniqueness for Sets."));
|
||||
constraints.add(new Constraint("$.properties.shippingAddresses.items['$ref']", is("#/descriptors/address"),
|
||||
"References descriptor of complex element type."));
|
||||
constraints.add(new Constraint("$.properties.shippingAddresses.items['$ref']", is("#/definitions/address"),
|
||||
"References definition of complex element type."));
|
||||
|
||||
// DATAREST-531
|
||||
constraints.add(new Constraint("$.properties.email.readOnly", is(true), "Email is read-only property"));
|
||||
|
||||
// DATAREST-644
|
||||
constraints.add(new Constraint("$.properties.shippingAddresses.title", is("Shipping addresses"),
|
||||
"Defaults titles correctly (split at camel case)"));
|
||||
|
||||
assertConstraints(User.class, constraints);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user