Skip to content

Commit

Permalink
DATAREST-354 - General rewrite of the JSONSchema support.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Mar 5, 2015
1 parent ed5bde1 commit def74e6
Show file tree
Hide file tree
Showing 14 changed files with 825 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -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("_", "-");
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -15,13 +15,22 @@
*/
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.
*
* @author Oliver Gierke
*/
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;

Expand Down Expand Up @@ -63,4 +72,56 @@ public void setAlpsEnabled(boolean enableAlps) {
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);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
/*
* 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;
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;
Expand All @@ -15,6 +31,7 @@
* An entity that represents a person.
*
* @author Jon Brisbin
* @author Oliver Gierke
*/
@Entity
public class Person {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArg
@Bean
public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() {
return new PersistentEntityToJsonSchemaConverter(persistentEntities(), resourceMappings(),
resourceDescriptionMessageSourceAccessor(), entityLinks());
resourceDescriptionMessageSourceAccessor(), objectMapper(), config());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@
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;

/**
Expand All @@ -36,6 +41,7 @@
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.
Expand All @@ -53,6 +59,7 @@ public JacksonMetadata(ObjectMapper mapper, Class<?> type) {
BeanDescription description = serializationConfig.introspect(javaType);

this.definitions = description.findProperties();
this.isValue = description.findJsonValueMethod() != null;
}

/**
Expand All @@ -75,6 +82,23 @@ public BeanPropertyDefinition getDefinitionFor(PersistentProperty<?> property) {
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.
*
Expand All @@ -85,6 +109,15 @@ public boolean isExported(PersistentProperty<?> property) {
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()
Expand Down
Loading

0 comments on commit def74e6

Please sign in to comment.