DATAREST-354 - General rewrite of the JSONSchema support.
Significant overhaul of the JSONSchema support. This currently adds the following features: - Complex nested types are exposed as descriptors with the properties pointing to them whenever necessary. - Sets are treated as unique collections. - Enums are handled as expected (enum values are listed). - Renamings via @JsonProperty are considered. - @JsonProperty(required = true) is considered and added to required properties. - Date/time types (legacy Date, JSR-310, ThreeTenBP and Joda Time) are exposed with format "date-time". - Objects with @JsonValue methods are considered to be rendered as String value. - Formats and patterns can be manually configured on MetadataConfiguration. TODOs: - Implementation polish, JavaDoc - Automatically inspect ObjectMapper to detect customizations made through Mixins and custom Serializers.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.core.config;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* An enum to represent JSON Schema pre-defined formats.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
*/
|
||||
public enum JsonSchemaFormat {
|
||||
|
||||
EMAIL, DATE_TIME, HOSTNAME, IPV4, IPV6, URI;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Enum#toString()
|
||||
*/
|
||||
@JsonValue
|
||||
public String toString() {
|
||||
return name().toLowerCase(Locale.US).replaceAll("_", "-");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
* Copyright 2014-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.
|
||||
@@ -15,6 +15,13 @@
|
||||
*/
|
||||
package org.springframework.data.rest.core.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configuration for metadata exposure.
|
||||
*
|
||||
@@ -22,6 +29,8 @@ package org.springframework.data.rest.core.config;
|
||||
*/
|
||||
public class MetadataConfiguration {
|
||||
|
||||
private final Map<Class<?>, JsonSchemaFormat> schemaFormats = new HashMap<Class<?>, JsonSchemaFormat>();
|
||||
private final Map<Class<?>, Pattern> patterns = new HashMap<Class<?>, Pattern>();
|
||||
private boolean omitUnresolvableDescriptionKeys = true;
|
||||
private boolean alpsEnabled = true;
|
||||
|
||||
@@ -63,4 +72,56 @@ public class MetadataConfiguration {
|
||||
public boolean alpsEnabled() {
|
||||
return alpsEnabled;
|
||||
}
|
||||
|
||||
public void registerJsonSchemaFormat(JsonSchemaFormat format, Class<?>... types) {
|
||||
|
||||
Assert.notNull(format, "JsonSchemaFormat must not be null!");
|
||||
|
||||
for (Class<?> type : types) {
|
||||
schemaFormats.put(type, format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JsonSchemaFormat} to be used for the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JsonSchemaFormat getSchemaFormatFor(Class<?> type) {
|
||||
return schemaFormats.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given formatting patter for the given value type.
|
||||
*
|
||||
* @param pattern must not be {@literal null} or empty.
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public void registerFormattingPatternFor(String pattern, Class<?> type) {
|
||||
|
||||
Assert.hasText(pattern, "Pattern must not be null or empty!");
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
this.patterns.put(type, Pattern.compile(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Pattern} registered for the given value type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Pattern getPatternFor(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
for (Entry<Class<?>, Pattern> entry : this.patterns.entrySet()) {
|
||||
if (entry.getKey().isAssignableFrom(type)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return this.patterns.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright 2013-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.core.domain.jpa;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -5,6 +20,7 @@ import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
@@ -15,6 +31,7 @@ import javax.persistence.PrePersist;
|
||||
* An entity that represents a person.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Entity
|
||||
public class Person {
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* Controller to expose a JSON schema via {@code / repository}/schema}.
|
||||
* Controller to expose a JSON schema via {@code /repository/schema}.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
|
||||
@@ -337,7 +337,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
@Bean
|
||||
public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() {
|
||||
return new PersistentEntityToJsonSchemaConverter(persistentEntities(), resourceMappings(),
|
||||
resourceDescriptionMessageSourceAccessor(), entityLinks());
|
||||
resourceDescriptionMessageSourceAccessor(), objectMapper(), config());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,12 +19,17 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
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.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.databind.BeanDescription;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationConfig;
|
||||
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
|
||||
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
|
||||
/**
|
||||
@@ -36,6 +41,7 @@ import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
|
||||
private final List<BeanPropertyDefinition> definitions;
|
||||
private final boolean isValue;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JacksonMetadata} instance for the given {@link ObjectMapper} and type.
|
||||
@@ -53,6 +59,7 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
BeanDescription description = serializationConfig.introspect(javaType);
|
||||
|
||||
this.definitions = description.findProperties();
|
||||
this.isValue = description.findJsonValueMethod() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +82,23 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fallback {@link ResourceDescription} to be used for the given {@link BeanPropertyDefinition}.
|
||||
*
|
||||
* @param definition must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public ResourceDescription getFallbackDescription(BeanPropertyDefinition definition) {
|
||||
|
||||
Assert.notNull(definition, "BeanPropertyDefinition must not be null!");
|
||||
|
||||
AnnotatedMember member = definition.getPrimaryMember();
|
||||
Description description = member.getAnnotation(Description.class);
|
||||
ResourceDescription fallback = SimpleResourceDescription.defaultFor(definition.getName());
|
||||
|
||||
return description == null ? null : new AnnotationBasedResourceDescription(description, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given property for a type is available to be exported, i.e. serialized via Jackson.
|
||||
*
|
||||
@@ -85,6 +109,15 @@ public class JacksonMetadata implements Iterable<BeanPropertyDefinition> {
|
||||
return getDefinitionFor(property) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the backing type is considered a Jackson value type.
|
||||
*
|
||||
* @return the isValue
|
||||
*/
|
||||
public boolean isValueType() {
|
||||
return isValue;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
|
||||
@@ -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.
|
||||
@@ -16,13 +16,27 @@
|
||||
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.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.data.rest.core.config.JsonSchemaFormat;
|
||||
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.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
|
||||
/**
|
||||
* Model class to render JSON schema documents.
|
||||
@@ -30,89 +44,353 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class JsonSchema extends Resource<Map<String, JsonSchema.Property>> {
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
public class JsonSchema {
|
||||
|
||||
private final String name;
|
||||
private static List<Class<?>> INTEGER_TYPES = Arrays.<Class<?>> asList(Long.class, long.class, Integer.class,
|
||||
int.class, Short.class, short.class);
|
||||
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final PropertiesContainer container;
|
||||
private final Descriptors descriptors;
|
||||
|
||||
public JsonSchema(String name, String description) {
|
||||
super(new HashMap<String, Property>());
|
||||
this.name = name;
|
||||
/**
|
||||
* Creates a new {@link JsonSchema} instance for the given title, description, {@link JsonSchemaProperty}s and
|
||||
* {@link Descriptors}.
|
||||
*
|
||||
* @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}.
|
||||
*/
|
||||
public JsonSchema(String title, String description, Collection<JsonSchemaProperty> properties, Descriptors descriptors) {
|
||||
|
||||
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!");
|
||||
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.container = new PropertiesContainer(properties);
|
||||
this.descriptors = descriptors;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
@JsonProperty("$schema")
|
||||
public String getSchema() {
|
||||
return "http://json-schema.org/draft-04/schema#";
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return "object";
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@JsonProperty("properties")
|
||||
@Override
|
||||
public Map<String, JsonSchema.Property> getContent() {
|
||||
return super.getContent();
|
||||
@JsonUnwrapped
|
||||
public PropertiesContainer getContainer() {
|
||||
return container;
|
||||
}
|
||||
|
||||
public JsonSchema addProperty(String name, Property property) {
|
||||
getContent().put(name, property);
|
||||
return this;
|
||||
/**
|
||||
* @return the descriptors
|
||||
*/
|
||||
@JsonUnwrapped
|
||||
public Descriptors getDescriptors() {
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
public boolean isArrayProperty(String name) {
|
||||
return (getContent().containsKey(name) && getContent().get(name) instanceof ArrayProperty);
|
||||
/**
|
||||
* Turns the given {@link TypeInformation} into a JSON Schema type string.
|
||||
*
|
||||
* @param typeInformation
|
||||
* @return
|
||||
* @see http://json-schema.org/latest/json-schema-core.html#anchor8
|
||||
*/
|
||||
private static String toJsonSchemaType(TypeInformation<?> typeInformation) {
|
||||
|
||||
Class<?> type = typeInformation.getType();
|
||||
|
||||
if (type == null) {
|
||||
return null;
|
||||
} else if (typeInformation.isCollectionLike()) {
|
||||
return "array";
|
||||
} else if (Boolean.class.equals(type) || boolean.class.equals(type)) {
|
||||
return "boolean";
|
||||
} else if (String.class.equals(type) || isDate(typeInformation) || type.isEnum()) {
|
||||
return "string";
|
||||
} else if (INTEGER_TYPES.contains(type)) {
|
||||
return "integer";
|
||||
} else if (Number.class.isAssignableFrom(type)) {
|
||||
return "number";
|
||||
} else {
|
||||
return "object";
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayProperty getArrayProperty(String name) {
|
||||
return (ArrayProperty) getContent().get(name);
|
||||
/**
|
||||
* Returns whether the given {@link TypeInformation} represents a date.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean isDate(TypeInformation<?> type) {
|
||||
|
||||
Class<?> rawType = type.getType();
|
||||
|
||||
if (Date.class.equals(rawType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String datePackage : Arrays.asList("java.time", "org.threeten.bp", "org.joda.time")) {
|
||||
if (rawType.getName().startsWith(datePackage)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class Property {
|
||||
/**
|
||||
* A JSON Schema item.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class Item {
|
||||
|
||||
private final String type;
|
||||
private final String description;
|
||||
private final boolean required;
|
||||
private final PropertiesContainer properties;
|
||||
|
||||
public Property(String type, String description, boolean required) {
|
||||
this.type = type;
|
||||
this.description = description;
|
||||
this.required = required;
|
||||
/**
|
||||
* Creates a new {@link Item} for the given {@link TypeInformation} and properties.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param properties must not be {@literal null}.
|
||||
*/
|
||||
public Item(TypeInformation<?> type, Collection<JsonSchemaProperty> properties) {
|
||||
|
||||
this.type = toJsonSchemaType(type);
|
||||
this.properties = new PropertiesContainer(properties);
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
@JsonUnwrapped
|
||||
public PropertiesContainer getProperties() {
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object to represent a generic container of properties.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
*/
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
static class PropertiesContainer {
|
||||
|
||||
public final Map<String, JsonSchemaProperty> properties;
|
||||
public final Collection<String> requiredProperties;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PropertiesContainer} for the given {@link JsonSchemaProperty}s.
|
||||
*
|
||||
* @param properties must not be {@literal null}.
|
||||
*/
|
||||
public PropertiesContainer(Collection<JsonSchemaProperty> properties) {
|
||||
|
||||
Assert.notNull(properties, "JsonSchemaPropertys must not be null!");
|
||||
|
||||
this.properties = new HashMap<String, JsonSchema.JsonSchemaProperty>();
|
||||
this.requiredProperties = new ArrayList<String>();
|
||||
|
||||
for (JsonSchemaProperty property : properties) {
|
||||
this.properties.put(property.getName(), property);
|
||||
|
||||
if (property.isRequired()) {
|
||||
this.requiredProperties.add(property.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object to abstract a {@link Map} of JSON Schema descriptors.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class Descriptors {
|
||||
|
||||
private final Map<String, Item> descriptors;
|
||||
|
||||
public Descriptors() {
|
||||
this.descriptors = new HashMap<String, Item>();
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
/**
|
||||
* @return the descriptors
|
||||
*/
|
||||
public Map<String, Item> getDescriptors() {
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
boolean hasDescriptorFor(TypeInformation<?> type) {
|
||||
return this.descriptors.containsKey(typeKey(type));
|
||||
}
|
||||
|
||||
String addDescriptor(TypeInformation<?> type, Item item) {
|
||||
|
||||
String reference = typeKey(type);
|
||||
this.descriptors.put(reference, item);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
static String getReference(TypeInformation<?> type) {
|
||||
return String.format("#/descriptors/%s", typeKey(type));
|
||||
}
|
||||
|
||||
static String typeKey(TypeInformation<?> type) {
|
||||
return StringUtils.uncapitalize(type.getActualType().getType().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all property implementations.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
*/
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
abstract static class JsonSchemaProperty {
|
||||
|
||||
private final String name;
|
||||
private final boolean required;
|
||||
|
||||
protected JsonSchemaProperty(String name, boolean required) {
|
||||
this.name = name;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ArrayProperty extends Property {
|
||||
private List<Property> items = new ArrayList<Property>();
|
||||
/**
|
||||
* A JSON Schema property
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.3
|
||||
*/
|
||||
static class Property extends JsonSchemaProperty {
|
||||
|
||||
public ArrayProperty(String type, String description, boolean required) {
|
||||
super(type, description, required);
|
||||
private static final TypeInformation<?> STRING_TYPE_INFORMATION = ClassTypeInformation.from(String.class);
|
||||
|
||||
public String description;
|
||||
public String type;
|
||||
public JsonSchemaFormat format;
|
||||
public String pattern;
|
||||
public Boolean uniqueItems;
|
||||
public @JsonProperty("$ref") String reference;
|
||||
public Map<String, String> items;
|
||||
|
||||
public Property(String name, String description, boolean required) {
|
||||
|
||||
super(name, required);
|
||||
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<? extends Property> getItems() {
|
||||
return items;
|
||||
}
|
||||
Property with(TypeInformation<?> type) {
|
||||
|
||||
this.type = toJsonSchemaType(type);
|
||||
|
||||
if (isDate(type)) {
|
||||
return with(JsonSchemaFormat.DATE_TIME);
|
||||
}
|
||||
|
||||
if (type.isCollectionLike()) {
|
||||
|
||||
if (Set.class.equals(type.getType())) {
|
||||
this.uniqueItems = true;
|
||||
}
|
||||
|
||||
this.items = Collections.singletonMap("type", toJsonSchemaType(type.getActualType()));
|
||||
}
|
||||
|
||||
public ArrayProperty setItems(List<Property> items) {
|
||||
this.items = items;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <P extends Property> ArrayProperty addItem(P item) {
|
||||
this.items.add(item);
|
||||
return this;
|
||||
Property with(JsonSchemaFormat format) {
|
||||
this.format = format;
|
||||
return with(STRING_TYPE_INFORMATION);
|
||||
}
|
||||
|
||||
Property with(Pattern pattern) {
|
||||
this.pattern = pattern.toString();
|
||||
return with(STRING_TYPE_INFORMATION);
|
||||
}
|
||||
|
||||
Property with(TypeInformation<?> type, String reference) {
|
||||
|
||||
if (type.isCollectionLike()) {
|
||||
|
||||
if (Set.class.equals(type.getType())) {
|
||||
this.uniqueItems = true;
|
||||
}
|
||||
|
||||
this.type = toJsonSchemaType(type);
|
||||
this.items = Collections.singletonMap("$ref", reference);
|
||||
|
||||
return this;
|
||||
|
||||
} else {
|
||||
this.reference = reference;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Property} 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 {
|
||||
|
||||
private final List<String> values;
|
||||
|
||||
public EnumProperty(String name, Class<?> type, String description, boolean required) {
|
||||
|
||||
super(name, description, required);
|
||||
|
||||
this.values = new ArrayList<String>();
|
||||
|
||||
for (Object value : type.getEnumConstants()) {
|
||||
this.values.add(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@JsonProperty("enum")
|
||||
public List<String> getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -15,34 +15,45 @@
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.json;
|
||||
|
||||
import static org.springframework.util.StringUtils.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.NoSuchMessageException;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalGenericConverter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.SimplePropertyHandler;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.rest.core.Path;
|
||||
import org.springframework.data.rest.core.config.JsonSchemaFormat;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.core.mapping.MappingResourceMetadata;
|
||||
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.ArrayProperty;
|
||||
import org.springframework.data.rest.webmvc.json.JsonSchema.Descriptors;
|
||||
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.rest.webmvc.mapping.LinkCollectingAssociationHandler;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.hateoas.EntityLinks;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
|
||||
|
||||
/**
|
||||
* Converter to create {@link JsonSchema} instances for {@link PersistentEntity}s.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@@ -50,12 +61,14 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
|
||||
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 ResourceMappings mappings;
|
||||
private final PersistentEntities repositories;
|
||||
private final PersistentEntities entities;
|
||||
private final MessageSourceAccessor accessor;
|
||||
private final EntityLinks entityLinks;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RepositoryRestConfiguration configuration;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link PersistentEntities} and
|
||||
@@ -63,20 +76,24 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
* @param mappings must not be {@literal null}.
|
||||
* @param accessor
|
||||
* @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, ResourceMappings mappings,
|
||||
MessageSourceAccessor accessor, EntityLinks entityLinks) {
|
||||
MessageSourceAccessor accessor, ObjectMapper objectMapper, RepositoryRestConfiguration configuration) {
|
||||
|
||||
Assert.notNull(entities, "PersistentEntities must not be null!");
|
||||
Assert.notNull(mappings, "ResourceMappings must not be null!");
|
||||
Assert.notNull(accessor, "MessageSourceAccessor must not be null!");
|
||||
Assert.notNull(entityLinks, "EntityLinks must not be null!");
|
||||
Assert.notNull(objectMapper, "ObjectMapper must not be null!");
|
||||
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
|
||||
|
||||
this.repositories = entities;
|
||||
this.entities = entities;
|
||||
this.mappings = mappings;
|
||||
this.accessor = accessor;
|
||||
this.entityLinks = entityLinks;
|
||||
this.objectMapper = objectMapper;
|
||||
this.configuration = configuration;
|
||||
|
||||
for (TypeInformation<?> domainType : entities.getManagedTypes()) {
|
||||
convertiblePairs.add(new ConvertiblePair(domainType.getType(), JsonSchema.class));
|
||||
@@ -102,6 +119,12 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
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);
|
||||
}
|
||||
@@ -111,54 +134,144 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
|
||||
* @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
|
||||
*/
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
public JsonSchema convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
|
||||
final PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity((Class<?>) source);
|
||||
final PersistentEntity<?, ?> persistentEntity = entities.getPersistentEntity((Class<?>) source);
|
||||
final ResourceMetadata metadata = mappings.getMappingFor(persistentEntity.getType());
|
||||
final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(),
|
||||
resolveMessage(metadata.getItemResourceDescription()));
|
||||
|
||||
persistentEntity.doWithProperties(new SimplePropertyHandler() {
|
||||
Descriptors descriptors = new Descriptors();
|
||||
List<JsonSchemaProperty> propertiesFor = getPropertiesFor(persistentEntity.getType(), metadata, descriptors);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.PropertyHandler#doWithPersistentProperty(org.springframework.data.mapping.PersistentProperty)
|
||||
*/
|
||||
@Override
|
||||
public void doWithPersistentProperty(PersistentProperty<?> persistentProperty) {
|
||||
|
||||
Class<?> propertyType = persistentProperty.getType();
|
||||
String type = uncapitalize(propertyType.getSimpleName());
|
||||
|
||||
ResourceMapping propertyMapping = metadata.getMappingFor(persistentProperty);
|
||||
ResourceDescription description = propertyMapping.getDescription();
|
||||
String message = resolveMessage(description);
|
||||
|
||||
Property property = persistentProperty.isCollectionLike() ? //
|
||||
new ArrayProperty("array", message, false)
|
||||
: new Property(type, message, false);
|
||||
|
||||
jsonSchema.addProperty(persistentProperty.getName(), property);
|
||||
}
|
||||
});
|
||||
|
||||
Link link = entityLinks.linkToCollectionResource(persistentEntity.getType()).expand();
|
||||
|
||||
LinkCollectingAssociationHandler associationHandler = new LinkCollectingAssociationHandler(repositories, new Path(
|
||||
link.getHref()), new AssociationLinks(mappings));
|
||||
persistentEntity.doWithAssociations(associationHandler);
|
||||
|
||||
jsonSchema.add(associationHandler.getLinks());
|
||||
|
||||
return jsonSchema;
|
||||
return new JsonSchema(persistentEntity.getName(), resolveMessage(metadata.getItemResourceDescription()),
|
||||
propertiesFor, descriptors);
|
||||
}
|
||||
|
||||
private String resolveMessage(ResourceDescription description) {
|
||||
private List<JsonSchemaProperty> getPropertiesFor(Class<?> type, final ResourceMetadata metadata,
|
||||
final Descriptors 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();
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
|
||||
if (format != null) {
|
||||
|
||||
// Types with explicitly registered format -> value object with format
|
||||
properties.add(property.with(format));
|
||||
continue;
|
||||
}
|
||||
|
||||
Pattern pattern = configuration.metadataConfiguration().getPatternFor(rawPropertyType);
|
||||
|
||||
if (pattern != null) {
|
||||
properties.add(property.with(pattern));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (jackson.isValueType()) {
|
||||
properties.add(property.with(STRING_TYPE_INFORMATION));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistentProperty == null) {
|
||||
properties.add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(rawPropertyType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (associationLinks.isLinkableAssociation(persistentProperty)) {
|
||||
properties.add(property.with(JsonSchemaFormat.URI));
|
||||
} else {
|
||||
|
||||
if (persistentProperty.isEntity()) {
|
||||
|
||||
if (!descriptors.hasDescriptorFor(propertyType)) {
|
||||
descriptors.addDescriptor(propertyType,
|
||||
new Item(propertyType, getNestedPropertiesFor(persistentProperty, descriptors)));
|
||||
}
|
||||
|
||||
properties.add(property.with(propertyType, Descriptors.getReference(propertyType)));
|
||||
|
||||
} else {
|
||||
|
||||
properties.add(property.with(propertyType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private Collection<JsonSchemaProperty> getNestedPropertiesFor(PersistentProperty<?> property, Descriptors descriptors) {
|
||||
|
||||
if (!property.isEntity()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Class<?> actualType = property.getActualType();
|
||||
PersistentEntity<?, ?> propertyEntity = entities.getPersistentEntity(actualType);
|
||||
MappingResourceMetadata propertyMetadata = new MappingResourceMetadata(propertyEntity);
|
||||
|
||||
return getPropertiesFor(actualType, propertyMetadata, descriptors);
|
||||
}
|
||||
|
||||
private Property getSchemaProperty(BeanPropertyDefinition definition, TypeInformation<?> type,
|
||||
ResourceDescription description) {
|
||||
|
||||
String name = definition.getName();
|
||||
String resolvedDescription = resolveMessage(description);
|
||||
boolean required = definition.isRequired();
|
||||
Class<?> rawType = type.getType();
|
||||
|
||||
if (!rawType.isEnum()) {
|
||||
return new Property(name, resolvedDescription, required);
|
||||
}
|
||||
|
||||
return new EnumProperty(name, rawType, description.getDefaultMessage().equals(resolvedDescription) ? null
|
||||
: resolvedDescription, required);
|
||||
}
|
||||
|
||||
private ResourceDescription getDescriptionFor(PersistentProperty<?> property, ResourceMetadata metadata) {
|
||||
|
||||
ResourceMapping propertyMapping = metadata.getMappingFor(property);
|
||||
return propertyMapping.getDescription();
|
||||
}
|
||||
|
||||
private String resolveMessage(MessageSourceResolvable resolvable) {
|
||||
|
||||
if (resolvable == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return accessor.getMessage(description);
|
||||
return accessor.getMessage(resolvable);
|
||||
} catch (NoSuchMessageException o_O) {
|
||||
return description.getMessage();
|
||||
|
||||
if (configuration.metadataConfiguration().omitUnresolvableDescriptionKeys()) {
|
||||
return null;
|
||||
} else {
|
||||
throw o_O;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -23,6 +23,9 @@ import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToMany;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Entity
|
||||
public class Author {
|
||||
|
||||
@@ -31,7 +34,7 @@ public class Author {
|
||||
public String name;
|
||||
|
||||
@ManyToMany(mappedBy = "authors")//
|
||||
Set<Book> books = new HashSet<Book>();
|
||||
public Set<Book> books = new HashSet<Book>();
|
||||
|
||||
protected Author() {}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -24,16 +24,19 @@ import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToMany;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Entity
|
||||
public class Book {
|
||||
|
||||
@Id @GeneratedValue Long id;
|
||||
String isbn;
|
||||
public String isbn;
|
||||
|
||||
@ManyToMany(cascade = { CascadeType.MERGE })//
|
||||
Set<Author> authors;
|
||||
public Set<Author> authors;
|
||||
|
||||
String title;
|
||||
public String title;
|
||||
|
||||
protected Book() {}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
* Copyright 2014-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.
|
||||
@@ -15,29 +15,155 @@
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.json;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.rest.core.config.JsonSchemaFormat;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
|
||||
import org.springframework.data.rest.webmvc.TestMvcClient;
|
||||
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverterUnitTests.TestConfiguration;
|
||||
import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig;
|
||||
import org.springframework.data.rest.webmvc.mongodb.Profile;
|
||||
import org.springframework.data.rest.webmvc.mongodb.User;
|
||||
import org.springframework.data.rest.webmvc.mongodb.User.EmailAddress;
|
||||
import org.springframework.data.rest.webmvc.mongodb.User.TypeWithPattern;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@ContextConfiguration(classes = MongoDbRepositoryConfig.class)
|
||||
public class PersistentEntityToJsonSchemaConverterUnitTests extends AbstractControllerIntegrationTests {
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = { MongoDbRepositoryConfig.class, TestConfiguration.class })
|
||||
public class PersistentEntityToJsonSchemaConverterUnitTests {
|
||||
|
||||
@Autowired PersistentEntityToJsonSchemaConverter converter;
|
||||
@Autowired MessageSourceAccessor accessor;
|
||||
@Autowired RepositoryResourceMappings mappings;
|
||||
@Autowired RepositoryRestConfiguration configuration;
|
||||
@Autowired PersistentEntities entities;
|
||||
@Autowired @Qualifier("objectMapper") ObjectMapper objectMapper;
|
||||
|
||||
@Configuration
|
||||
static class TestConfiguration extends RepositoryRestMvcConfiguration {
|
||||
|
||||
@Override
|
||||
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
|
||||
config.metadataConfiguration().registerJsonSchemaFormat(JsonSchemaFormat.EMAIL, EmailAddress.class);
|
||||
config.metadataConfiguration().registerFormattingPatternFor("[A-Z]+", TypeWithPattern.class);
|
||||
}
|
||||
}
|
||||
|
||||
PersistentEntityToJsonSchemaConverter converter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
TestMvcClient.initWebTest();
|
||||
|
||||
converter = new PersistentEntityToJsonSchemaConverter(entities, mappings, accessor, objectMapper, configuration);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addsDescriptionToSchemaRoot() {
|
||||
public void fulfillsConstraintsForProfile() {
|
||||
|
||||
JsonSchema schema = converter.convert(Profile.class);
|
||||
List<Constraint> constraints = new ArrayList<Constraint>();
|
||||
constraints.add(new Constraint("$.description", is("Profile description"), "Adds description to schema root"));
|
||||
constraints.add(new Constraint("$.properties.renamed", is(notNullValue()), "Has descriptor for renamed property"));
|
||||
constraints.add(new Constraint("$.properties.aliased", is(nullValue()),
|
||||
"No descriptor for original name of renamed property"));
|
||||
|
||||
assertThat(schema.getDescription(), is("Profile description"));
|
||||
assertConstraints(Profile.class, constraints);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fulfilsConstraintsForUser() throws Exception {
|
||||
|
||||
List<Constraint> constraints = new ArrayList<Constraint>();
|
||||
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'"));
|
||||
constraints.add(new Constraint("$.descriptors.address.properties.zipCode", is(notNullValue()),
|
||||
"Exposes nested properties"));
|
||||
constraints.add(new Constraint("$.descriptors.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.add(new Constraint("$.properties.jodaDateTime.format", is("date-time"),
|
||||
"Exposes JodaTime dates in format."));
|
||||
constraints.add(new Constraint("$.properties.java8DateTime.format", is("date-time"),
|
||||
"Exposes Java 8 dates in format."));
|
||||
constraints.add(new Constraint("$.properties.nicknames.type", is("array"), "Exposes collection of simple types."));
|
||||
constraints.add(new Constraint("$.properties.nicknames.items.type", is("string"),
|
||||
"Exposes element type of collection of simple types."));
|
||||
constraints.add(new Constraint("$.properties.email.format", is("email"), "Uses manually configured format."));
|
||||
constraints.add(new Constraint("$.properties.email.type", is("string"), "Treats types with format as String."));
|
||||
|
||||
constraints.add(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."));
|
||||
|
||||
assertConstraints(User.class, constraints);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void assertConstraints(Class<?> type, Iterable<Constraint> constraints) {
|
||||
|
||||
String writeSchemaFor = writeSchemaFor(type);
|
||||
|
||||
System.out.println(writeSchemaFor);
|
||||
|
||||
for (Constraint constraint : constraints) {
|
||||
|
||||
try {
|
||||
assertThat(constraint.description, JsonPath.read(writeSchemaFor, constraint.selector), constraint.matcher);
|
||||
} catch (RuntimeException e) {
|
||||
assertThat(e, constraint.matcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String writeSchemaFor(Class<?> type) {
|
||||
|
||||
try {
|
||||
return objectMapper.writeValueAsString(converter.convert(type));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static class Constraint {
|
||||
|
||||
String selector;
|
||||
Matcher matcher;
|
||||
String description;
|
||||
|
||||
public Constraint(String selector, Matcher matcher, String description) {
|
||||
this.selector = selector;
|
||||
this.matcher = matcher;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.mongodb;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class Address {
|
||||
|
||||
public String street, zipCode;
|
||||
public String street;
|
||||
public @JsonProperty(required = true) String zipCode;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
@@ -16,8 +17,9 @@ public class Profile {
|
||||
|
||||
@Id private String id;
|
||||
private Long person;
|
||||
private String type;
|
||||
private @JsonProperty(required = true) String type;
|
||||
private @LastModifiedDate Date lastModifiedDate;
|
||||
private @JsonProperty("renamed") String aliased;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
@@ -50,4 +52,8 @@ public class Profile {
|
||||
public Date getLastModifiedDate() {
|
||||
return lastModifiedDate;
|
||||
}
|
||||
|
||||
public String getAliased() {
|
||||
return aliased;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,54 @@
|
||||
package org.springframework.data.rest.webmvc.mongodb;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Document
|
||||
public class User {
|
||||
|
||||
public static enum Gender {
|
||||
MALE, FEMALE;
|
||||
}
|
||||
|
||||
public BigInteger id;
|
||||
public String firstname, lastname;
|
||||
public Address address;
|
||||
|
||||
public Set<Address> shippingAddresses;
|
||||
public List<String> nicknames;
|
||||
public Gender gender;
|
||||
public EmailAddress email;
|
||||
public LocalDateTime java8DateTime;
|
||||
public org.joda.time.LocalDateTime jodaDateTime;
|
||||
public TypeWithPattern pattern;
|
||||
public @DBRef(lazy = true) List<User> colleagues;
|
||||
|
||||
public static class EmailAddress {
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* @param value
|
||||
*/
|
||||
public EmailAddress(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
@Override
|
||||
|
||||
@JsonValue
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TypeWithPattern {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user