#482 - Add support for Collection+JSON mediatype.
Introduce support for media type application/vnd.collection+json. Collection+JSON doesn't allow metadata at the top, so paging data can't be covered, however, everything else fits. Also moved a little bit more into Affordance and SpringMvcAffordance to avoid using Spring MVC annotations directly in a given mediatype's AffordanceModel. Refactored bits of HAL-FORMS to reuse the new PropertyUtils, ensuring Jackson ignore annotations are taken into consideration. Also added MockMVC tests to show HAL-FORMS and Collection+JSON working together, against the same controller.
This commit is contained in:
committed by
Oliver Gierke
parent
3af698d928
commit
d6e02857f1
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.hateoas;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
@@ -47,4 +50,17 @@ public interface Affordance {
|
||||
* @return
|
||||
*/
|
||||
<T extends AffordanceModel> T getAffordanceModel(MediaType mediaType);
|
||||
|
||||
/**
|
||||
* Get a listing of {@link MethodParameter}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
List<MethodParameter> getInputMethodParameters();
|
||||
|
||||
/**
|
||||
* Get a listing of {@link QueryParameter}s.
|
||||
* @return
|
||||
*/
|
||||
List<QueryParameter> getQueryMethodParameters();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
*/
|
||||
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties("templated")
|
||||
@JsonIgnoreProperties(value = "templated", ignoreUnknown = true)
|
||||
@AllArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
@Getter
|
||||
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation", "affordances" })
|
||||
|
||||
@@ -67,4 +67,13 @@ public class MediaTypes {
|
||||
*/
|
||||
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);
|
||||
|
||||
/**
|
||||
* A String equivalent of {@link MediaTypes#COLLECTION_JSON}.
|
||||
*/
|
||||
public static final String COLLECTION_JSON_VALUE = "application/vnd.collection+json";
|
||||
|
||||
/**
|
||||
* Public constant media type for {@code application/vnd.collection+json}.
|
||||
*/
|
||||
public static final MediaType COLLECTION_JSON = MediaType.valueOf(COLLECTION_JSON_VALUE);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Web framework-neutral representation of a web request's query parameter (http://example.com?name=foo).
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class QueryParameter {
|
||||
|
||||
private final String name;
|
||||
private final boolean required;
|
||||
private final String value;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
* A simple {@link Resource} wrapping a domain object and adding links to it.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@XmlRootElement
|
||||
public class Resource<T> extends ResourceSupport {
|
||||
@@ -38,7 +39,7 @@ public class Resource<T> extends ResourceSupport {
|
||||
/**
|
||||
* Creates an empty {@link Resource}.
|
||||
*/
|
||||
Resource() {
|
||||
protected Resource() {
|
||||
this.content = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Representation of the "collection" part of a Collection+JSON document.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
class CollectionJson<T> {
|
||||
|
||||
private String version;
|
||||
private String href;
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
private List<Link> links;
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
private List<CollectionJsonItem<T>> items;
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
private List<CollectionJsonQuery> queries;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private CollectionJsonTemplate template;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private CollectionJsonError error;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJson(@JsonProperty("version") String version, @JsonProperty("href") String href,
|
||||
@JsonProperty("links") List<Link> links, @JsonProperty("items") List<CollectionJsonItem<T>> items,
|
||||
@JsonProperty("queries") List<CollectionJsonQuery> queries,
|
||||
@JsonProperty("template") CollectionJsonTemplate template,
|
||||
@JsonProperty("error") CollectionJsonError error) {
|
||||
|
||||
this.version = version;
|
||||
this.href = href;
|
||||
this.links = links;
|
||||
this.items = items;
|
||||
this.queries = queries;
|
||||
this.template = template;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
CollectionJson() {
|
||||
this("1.0", null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class CollectionJsonAffordanceModel implements AffordanceModel {
|
||||
|
||||
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
|
||||
HttpMethod.PATCH);
|
||||
|
||||
private final Affordance affordance;
|
||||
private final UriComponents components;
|
||||
private final @Getter List<CollectionJsonData> inputProperties;
|
||||
private final @Getter List<CollectionJsonData> queryProperties;
|
||||
|
||||
CollectionJsonAffordanceModel(Affordance affordance, UriComponents components) {
|
||||
|
||||
this.affordance = affordance;
|
||||
this.components = components;
|
||||
|
||||
this.inputProperties = determineAffordanceInputs();
|
||||
this.queryProperties = determineQueryProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MediaType> getMediaTypes() {
|
||||
return Collections.singleton(MediaTypes.COLLECTION_JSON);
|
||||
}
|
||||
|
||||
public String getRel() {
|
||||
return isHttpGetMethod() ? this.affordance.getName() : "";
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return isHttpGetMethod() ? this.components.toUriString() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a list of {@link QueryParameter}s into a list of {@link CollectionJsonData} objects.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<CollectionJsonData> determineQueryProperties() {
|
||||
|
||||
if (!isHttpGetMethod()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return this.affordance.getQueryMethodParameters().stream()
|
||||
.map(queryProperty -> new CollectionJsonData().withName(queryProperty.getName()).withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean isHttpGetMethod() {
|
||||
return this.affordance.getHttpMethod().equals(HttpMethod.GET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
|
||||
* Then transform them into a list of {@link CollectionJsonData} objects.
|
||||
*/
|
||||
private List<CollectionJsonData> determineAffordanceInputs() {
|
||||
|
||||
if (!METHODS_FOR_INPUT_DETECTION.contains(affordance.getHttpMethod())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return this.affordance.getInputMethodParameters().stream()
|
||||
.findFirst()
|
||||
.map(methodParameter -> {
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
|
||||
return PropertyUtils.findProperties(resolvableType);
|
||||
})
|
||||
.orElse(Collections.emptyList())
|
||||
.stream()
|
||||
.map(property -> new CollectionJsonData().withName(property).withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class CollectionJsonAffordanceModelFactory implements AffordanceModelFactory {
|
||||
|
||||
private final @Getter MediaType mediaType = MediaTypes.COLLECTION_JSON;
|
||||
|
||||
/**
|
||||
* Look up the {@link AffordanceModel} for this factory.
|
||||
*
|
||||
* @param affordance
|
||||
* @param invocationValue
|
||||
* @param components
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
|
||||
return new CollectionJsonAffordanceModel(affordance, components);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2017 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@JsonIgnoreProperties()
|
||||
class CollectionJsonData {
|
||||
|
||||
@JsonInclude(Include.NON_NULL) //
|
||||
private String name;
|
||||
|
||||
@JsonInclude(Include.NON_NULL) //
|
||||
private Object value;
|
||||
|
||||
@JsonInclude(Include.NON_NULL) //
|
||||
private String prompt;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJsonData(@JsonProperty("name") String name, @JsonProperty("value") Object value,
|
||||
@JsonProperty("prompt") String prompt) {
|
||||
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.prompt = prompt;
|
||||
}
|
||||
|
||||
CollectionJsonData() {
|
||||
this(null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents an entire Collection+JSON document.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class CollectionJsonDocument<T> {
|
||||
|
||||
private CollectionJson<T> collection;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJsonDocument(@JsonProperty("version") String version, @JsonProperty("href") String href,
|
||||
@JsonProperty("links") List<Link> links, @JsonProperty("items") List<CollectionJsonItem<T>> items,
|
||||
@JsonProperty("queries") List<CollectionJsonQuery> queries,
|
||||
@JsonProperty("template") CollectionJsonTemplate template,
|
||||
@JsonProperty("error") CollectionJsonError error) {
|
||||
this.collection = new CollectionJson(version, href, links, items, queries, template, error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
class CollectionJsonError {
|
||||
|
||||
private String title;
|
||||
private String code;
|
||||
private String message;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJsonError(@JsonProperty("title") String title, @JsonProperty("code") String code,
|
||||
@JsonProperty("message") String message) {
|
||||
|
||||
this.title = title;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
CollectionJsonError() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
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.databind.JavaType;
|
||||
|
||||
/**
|
||||
* Representation of an "item" in a Collection+JSON document.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class CollectionJsonItem<T> {
|
||||
|
||||
private String href;
|
||||
private List<CollectionJsonData> data;
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
private List<Link> links;
|
||||
|
||||
@Getter(onMethod = @__({@JsonIgnore}), value = AccessLevel.PRIVATE)
|
||||
private T rawData;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJsonItem(@JsonProperty("href") String href, @JsonProperty("data") List<CollectionJsonData> data,
|
||||
@JsonProperty("links") List<Link> links) {
|
||||
|
||||
this.href = href;
|
||||
this.data = data;
|
||||
this.links = links;
|
||||
this.rawData = null;
|
||||
}
|
||||
|
||||
CollectionJsonItem() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple scalar types that can be encoded by value, not type.
|
||||
*/
|
||||
private final static HashSet<Class<?>> PRIMITIVE_TYPES = new HashSet<Class<?>>() {{
|
||||
add(String.class);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Transform a domain object into a collection of {@link CollectionJsonData} objects to serialize properly.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<CollectionJsonData> getData() {
|
||||
|
||||
if (this.data != null) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
if (PRIMITIVE_TYPES.contains(this.rawData.getClass())) {
|
||||
return Collections.singletonList(new CollectionJsonData().withValue(this.rawData));
|
||||
}
|
||||
|
||||
return PropertyUtils.findProperties(this.rawData).entrySet().stream()
|
||||
.map(entry -> new CollectionJsonData()
|
||||
.withName(entry.getKey())
|
||||
.withValue(entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an object used the deserialized properties and the provided type from the deserializer.
|
||||
*
|
||||
* @param javaType - type of the object to create
|
||||
* @return
|
||||
*/
|
||||
public Object toRawData(JavaType javaType) {
|
||||
|
||||
if (PRIMITIVE_TYPES.contains(javaType.getRawClass())) {
|
||||
return this.data.get(0).getValue();
|
||||
}
|
||||
|
||||
return PropertyUtils.createObjectFromProperties(javaType.getRawClass(), //
|
||||
this.data.stream()
|
||||
.collect(Collectors.toMap(
|
||||
CollectionJsonData::getName,
|
||||
CollectionJsonData::getValue)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.core.JsonPathLinkDiscoverer;
|
||||
|
||||
/**
|
||||
* {@link LinkDiscoverer} implementation based on JSON Collection link structure.
|
||||
*
|
||||
* NOTE: Since links can appear in two different places in a Collection+JSON document, this discoverer
|
||||
* uses two.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class CollectionJsonLinkDiscoverer extends JsonPathLinkDiscoverer {
|
||||
|
||||
private final CollectionJsonSelfLinkDiscoverer selfLinkDiscoverer;
|
||||
|
||||
public CollectionJsonLinkDiscoverer() {
|
||||
super("$.collection..links..[?(@.rel == '%s')].href", MediaTypes.COLLECTION_JSON);
|
||||
this.selfLinkDiscoverer = new CollectionJsonSelfLinkDiscoverer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link findLinkWithRel(String rel, String representation) {
|
||||
|
||||
if (rel.equals(Link.REL_SELF)) {
|
||||
return findSelfLink(representation);
|
||||
} else {
|
||||
return super.findLinkWithRel(rel, representation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link findLinkWithRel(String rel, InputStream representation) {
|
||||
|
||||
if (rel.equals(Link.REL_SELF)) {
|
||||
return findSelfLink(representation);
|
||||
} else {
|
||||
return super.findLinkWithRel(rel, representation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> findLinksWithRel(String rel, String representation) {
|
||||
|
||||
if (rel.equals(Link.REL_SELF)) {
|
||||
return addSelfLink(super.findLinksWithRel(rel, representation), representation);
|
||||
} else {
|
||||
return super.findLinksWithRel(rel, representation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> findLinksWithRel(String rel, InputStream representation) {
|
||||
|
||||
if (rel.equals(Link.REL_SELF)) {
|
||||
return addSelfLink(super.findLinksWithRel(rel, representation), representation);
|
||||
} else {
|
||||
return super.findLinksWithRel(rel, representation);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Internal methods to support discovering the "self" link found at "$.collection.href".
|
||||
//
|
||||
|
||||
private Link findSelfLink(String representation) {
|
||||
return this.selfLinkDiscoverer.findLinkWithRel(Link.REL_SELF, representation);
|
||||
}
|
||||
|
||||
private Link findSelfLink(InputStream representation) {
|
||||
return this.selfLinkDiscoverer.findLinkWithRel(Link.REL_SELF, representation);
|
||||
}
|
||||
|
||||
private List<Link> addSelfLink(List<Link> links, String representation) {
|
||||
|
||||
return Stream.concat(
|
||||
Stream.of(findSelfLink(representation)),
|
||||
links.stream()
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Link> addSelfLink(List<Link> links, InputStream representation) {
|
||||
|
||||
return Stream.concat(
|
||||
Stream.of(findSelfLink(representation)),
|
||||
links.stream()
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link JsonPathLinkDiscoverer} that looks for the non-parameterized {@literal collection.href} link.
|
||||
*/
|
||||
private static class CollectionJsonSelfLinkDiscoverer extends JsonPathLinkDiscoverer {
|
||||
CollectionJsonSelfLinkDiscoverer() {
|
||||
super("$.collection.href", MediaTypes.COLLECTION_JSON);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2017 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.hateoas.collectionjson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonEncoding;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* A message converter that converts any object into a Collection+JSON document before bundling up as an
|
||||
* {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into an object.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class CollectionJsonMessageConverter extends AbstractHttpMessageConverter<Object> {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public CollectionJsonMessageConverter(ObjectMapper objectMapper) {
|
||||
|
||||
this.objectMapper = objectMapper;
|
||||
this.objectMapper.registerModule(new Jackson2CollectionJsonModule());
|
||||
|
||||
setSupportedMediaTypes(Arrays.asList(MediaTypes.COLLECTION_JSON));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
return this.objectMapper.readValue(inputMessage.getBody(), clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal(Object t, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), JsonEncoding.UTF8);
|
||||
|
||||
// A workaround for JsonGenerators not applying serialization features
|
||||
// https://github.com/FasterXML/jackson-databind/issues/12
|
||||
if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
|
||||
jsonGenerator.useDefaultPrettyPrinter();
|
||||
}
|
||||
|
||||
try {
|
||||
objectMapper.writeValue(jsonGenerator, t);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.collectionjson;
|
||||
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.*;
|
||||
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither
|
||||
class CollectionJsonQuery {
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private String rel;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private String href;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private String prompt;
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
private List<CollectionJsonData> data;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJsonQuery(@JsonProperty("rel") String rel, @JsonProperty("href") String href,
|
||||
@JsonProperty("prompt") String prompt, @JsonProperty("data") List<CollectionJsonData> data) {
|
||||
|
||||
this.rel = rel;
|
||||
this.href = href;
|
||||
this.prompt = prompt;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
CollectionJsonQuery() {
|
||||
this(null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.collectionjson;
|
||||
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither
|
||||
class CollectionJsonTemplate {
|
||||
|
||||
private List<CollectionJsonData> data;
|
||||
|
||||
@JsonCreator
|
||||
CollectionJsonTemplate(@JsonProperty("data") List<CollectionJsonData> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
CollectionJsonTemplate() {
|
||||
this(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 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.hateoas.collectionjson;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Configuration
|
||||
public class CollectionJsonWebMvcConfigurer implements WebMvcConfigurer {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#configureMessageConverters(java.util.List)
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.add(new CollectionJsonMessageConverter(new ObjectMapper()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,904 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.support.JacksonHelper;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerationException;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
|
||||
import com.fasterxml.jackson.databind.cfg.MapperConfig;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
|
||||
import com.fasterxml.jackson.databind.introspect.Annotated;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
/**
|
||||
* Jackson 2 module implementation to render {@link Resources}, {@link Resource}, and {@link ResourceSupport}
|
||||
* instances in Collection+JSON compatible JSON.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class Jackson2CollectionJsonModule extends SimpleModule {
|
||||
|
||||
public Jackson2CollectionJsonModule() {
|
||||
|
||||
super("collection-json-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
|
||||
|
||||
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
|
||||
setMixInAnnotation(Resource.class, ResourceMixin.class);
|
||||
setMixInAnnotation(Resources.class, ResourcesMixin.class);
|
||||
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link JsonSerializer} to render Link instances in JSON Collection compatible JSON.
|
||||
*
|
||||
* @author Alexander Baetz
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class CollectionJsonLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
private final MessageSourceAccessor messageSource;
|
||||
|
||||
CollectionJsonLinkListSerializer(MessageSourceAccessor messageSource) {
|
||||
this(null, messageSource);
|
||||
}
|
||||
|
||||
CollectionJsonLinkListSerializer(BeanProperty property, MessageSourceAccessor messageSource) {
|
||||
|
||||
super(List.class, false);
|
||||
this.property = property;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
|
||||
ResourceSupport resource = new ResourceSupport();
|
||||
resource.add(value);
|
||||
|
||||
CollectionJson<?> collectionJson = new CollectionJson()
|
||||
.withVersion("1.0")
|
||||
.withHref(resource.getRequiredLink(Link.REL_SELF).expand().getHref())
|
||||
.withLinks(withoutSelfLink(value))
|
||||
.withItems(Collections.EMPTY_LIST);
|
||||
|
||||
provider
|
||||
.findValueSerializer(CollectionJson.class, property)
|
||||
.serialize(collectionJson, jgen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new CollectionJsonLinkListSerializer(property, messageSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty(List<Link> value) {
|
||||
return value.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(List<Link> value) {
|
||||
return value.size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonResourceSupportSerializer extends ContainerSerializer<ResourceSupport> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
CollectionJsonResourceSupportSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
CollectionJsonResourceSupportSerializer(BeanProperty property) {
|
||||
|
||||
super(ResourceSupport.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ResourceSupport value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
|
||||
|
||||
String href = value.getRequiredLink(Link.REL_SELF).getHref();
|
||||
|
||||
CollectionJson<?> collectionJson = new CollectionJson()
|
||||
.withVersion("1.0")
|
||||
.withHref(href)
|
||||
.withLinks(withoutSelfLink(value.getLinks()))
|
||||
.withQueries(findQueries(value))
|
||||
.withTemplate(findTemplate(value));
|
||||
|
||||
CollectionJsonItem item = new CollectionJsonItem()
|
||||
.withHref(href)
|
||||
.withLinks(withoutSelfLink(value.getLinks()))
|
||||
.withRawData(value);
|
||||
|
||||
if (!item.getData().isEmpty()) {
|
||||
collectionJson = collectionJson.withItems(Collections.singletonList(item));
|
||||
}
|
||||
|
||||
CollectionJsonDocument<?> doc = new CollectionJsonDocument<>(collectionJson);
|
||||
|
||||
provider
|
||||
.findValueSerializer(CollectionJsonDocument.class, property)
|
||||
.serialize(doc, jgen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new CollectionJsonResourceSupportSerializer(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(ResourceSupport value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonResourceSerializer extends ContainerSerializer<Resource<?>> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
CollectionJsonResourceSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
CollectionJsonResourceSerializer(BeanProperty property) {
|
||||
|
||||
super(Resource.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Resource<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
|
||||
|
||||
String href = value.getRequiredLink(Link.REL_SELF).getHref();
|
||||
|
||||
CollectionJson<?> collectionJson = new CollectionJson()
|
||||
.withVersion("1.0")
|
||||
.withHref(href)
|
||||
.withLinks(withoutSelfLink(value.getLinks()))
|
||||
.withItems(Collections.singletonList(new CollectionJsonItem<>()
|
||||
.withHref(href)
|
||||
.withLinks(withoutSelfLink(value.getLinks()))
|
||||
.withRawData(value.getContent())))
|
||||
.withQueries(findQueries(value))
|
||||
.withTemplate(findTemplate(value));
|
||||
|
||||
CollectionJsonDocument<?> doc = new CollectionJsonDocument<>(collectionJson);
|
||||
|
||||
provider
|
||||
.findValueSerializer(CollectionJsonDocument.class, property)
|
||||
.serialize(doc, jgen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new CollectionJsonResourceSerializer(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(Resource<?> value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonResourcesSerializer extends ContainerSerializer<Resources<?>> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
CollectionJsonResourcesSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
CollectionJsonResourcesSerializer(BeanProperty property) {
|
||||
|
||||
super(Resources.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Resources<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
|
||||
|
||||
CollectionJson<?> collectionJson = new CollectionJson()
|
||||
.withVersion("1.0")
|
||||
.withHref(value.getRequiredLink(Link.REL_SELF).getHref())
|
||||
.withLinks(withoutSelfLink(value.getLinks()))
|
||||
.withItems(resourcesToCollectionJsonItems(value))
|
||||
.withQueries(findQueries(value))
|
||||
.withTemplate(findTemplate(value));
|
||||
|
||||
CollectionJsonDocument<?> doc = new CollectionJsonDocument<>(collectionJson);
|
||||
|
||||
provider
|
||||
.findValueSerializer(CollectionJsonDocument.class, property)
|
||||
.serialize(doc, jgen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new CollectionJsonResourcesSerializer(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty(Resources<?> value) {
|
||||
return value.getContent().size() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(Resources<?> value) {
|
||||
return value.getContent().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonPagedResourcesSerializer extends ContainerSerializer<PagedResources<?>> implements ContextualSerializer {
|
||||
|
||||
private final BeanProperty property;
|
||||
|
||||
CollectionJsonPagedResourcesSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
CollectionJsonPagedResourcesSerializer(BeanProperty property) {
|
||||
|
||||
super(Resources.class, false);
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(PagedResources<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
|
||||
|
||||
CollectionJson<?> collectionJson = new CollectionJson()
|
||||
.withVersion("1.0")
|
||||
.withHref(value.getRequiredLink(Link.REL_SELF).getHref())
|
||||
.withLinks(withoutSelfLink(value.getLinks()))
|
||||
.withItems(resourcesToCollectionJsonItems(value))
|
||||
.withQueries(findQueries(value))
|
||||
.withTemplate(findTemplate(value));
|
||||
|
||||
CollectionJsonDocument<?> doc = new CollectionJsonDocument<>(collectionJson);
|
||||
|
||||
provider
|
||||
.findValueSerializer(CollectionJsonDocument.class, property)
|
||||
.serialize(doc, jgen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
return new CollectionJsonPagedResourcesSerializer(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> getContentSerializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty(PagedResources<?> value) {
|
||||
return value.getContent().size() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSingleElement(PagedResources<?> value) {
|
||||
return value.getContent().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonLinkListDeserializer extends ContainerDeserializerBase<List<Link>> {
|
||||
|
||||
CollectionJsonLinkListDeserializer() {
|
||||
super(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, Link.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Link> deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
|
||||
|
||||
CollectionJsonDocument<?> document = jp.getCodec().readValue(jp, CollectionJsonDocument.class);
|
||||
|
||||
return potentiallyAddSelfLink(document.getCollection().getLinks(), document.getCollection().getHref());
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonResourceSupportDeserializer extends ContainerDeserializerBase<ResourceSupport>
|
||||
implements ContextualDeserializer {
|
||||
|
||||
private final JavaType contentType;
|
||||
|
||||
CollectionJsonResourceSupportDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructType(ResourceSupport.class));
|
||||
}
|
||||
|
||||
CollectionJsonResourceSupportDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceSupport deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
|
||||
|
||||
JavaType rootType = ctxt.getTypeFactory().constructSimpleType(Object.class, new JavaType[]{});
|
||||
JavaType wrappedType = ctxt.getTypeFactory().constructParametricType(CollectionJsonDocument.class, rootType);
|
||||
|
||||
CollectionJsonDocument<?> document = jp.getCodec().readValue(jp, wrappedType);
|
||||
|
||||
List<? extends CollectionJsonItem<?>> items = Optional.ofNullable(document.getCollection().getItems()).orElse(new ArrayList<>());
|
||||
List<Link> links = Optional.ofNullable(document.getCollection().getLinks()).orElse(new ArrayList<>());
|
||||
|
||||
if (items.size() == 0) {
|
||||
if (document.getCollection().getTemplate() != null) {
|
||||
|
||||
Map<String, Object> properties = document.getCollection().getTemplate().getData().stream()
|
||||
.collect(Collectors.toMap(CollectionJsonData::getName, CollectionJsonData::getValue));
|
||||
|
||||
ResourceSupport obj = (ResourceSupport) PropertyUtils.createObjectFromProperties(this.contentType.getRawClass(), properties);
|
||||
|
||||
obj.add(potentiallyAddSelfLink(links, document.getCollection().getHref()));
|
||||
|
||||
return obj;
|
||||
} else {
|
||||
ResourceSupport resource = new ResourceSupport();
|
||||
resource.add(potentiallyAddSelfLink(links, document.getCollection().getHref()));
|
||||
|
||||
return resource;
|
||||
}
|
||||
} else {
|
||||
|
||||
items.stream()
|
||||
.flatMap(item -> Optional.ofNullable(item.getLinks())
|
||||
.map(Collection::stream)
|
||||
.orElse(Stream.empty()))
|
||||
.forEach(link -> {
|
||||
if (!links.contains(link))
|
||||
links.add(link);
|
||||
});
|
||||
|
||||
ResourceSupport resource = (ResourceSupport) items.get(0).toRawData(this.contentType);
|
||||
resource.add(potentiallyAddSelfLink(links, items.get(0).getHref()));
|
||||
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
|
||||
BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
return new CollectionJsonResourceSupportDeserializer(property.getType().getContentType());
|
||||
} else {
|
||||
return new CollectionJsonResourceSupportDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonResourceDeserializer extends ContainerDeserializerBase<Resource<?>>
|
||||
implements ContextualDeserializer {
|
||||
|
||||
private final JavaType contentType;
|
||||
|
||||
CollectionJsonResourceDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructType(CollectionJson.class));
|
||||
}
|
||||
|
||||
CollectionJsonResourceDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource<?> deserialize(JsonParser jp, DeserializationContext ctxt)
|
||||
throws IOException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
JavaType wrappedType = ctxt.getTypeFactory().constructParametricType(CollectionJsonDocument.class, rootType);
|
||||
|
||||
CollectionJsonDocument<?> document = jp.getCodec().readValue(jp, wrappedType);
|
||||
|
||||
List<? extends CollectionJsonItem<?>> items = Optional.ofNullable(document.getCollection().getItems()).orElse(new ArrayList<>());
|
||||
List<Link> links = Optional.ofNullable(document.getCollection().getLinks()).orElse(new ArrayList<>());
|
||||
|
||||
if (items.size() == 0 && document.getCollection().getTemplate() != null) {
|
||||
|
||||
Map<String, Object> properties = document.getCollection().getTemplate().getData().stream()
|
||||
.collect(Collectors.toMap(CollectionJsonData::getName, CollectionJsonData::getValue));
|
||||
|
||||
Object obj = PropertyUtils.createObjectFromProperties(rootType.getRawClass(), properties);
|
||||
|
||||
return new Resource<>(obj, potentiallyAddSelfLink(links, document.getCollection().getHref()));
|
||||
} else {
|
||||
|
||||
items.stream()
|
||||
.flatMap(item -> Optional.ofNullable(item.getLinks())
|
||||
.map(Collection::stream)
|
||||
.orElse(Stream.empty()))
|
||||
.forEach(link -> {
|
||||
if (!links.contains(link))
|
||||
links.add(link);
|
||||
});
|
||||
|
||||
return new Resource<>(items.get(0).toRawData(rootType),
|
||||
potentiallyAddSelfLink(links, items.get(0).getHref()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
|
||||
BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
return new CollectionJsonResourceDeserializer(property.getType().getContentType());
|
||||
} else {
|
||||
return new CollectionJsonResourceDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonResourcesDeserializer extends ContainerDeserializerBase<Resources>
|
||||
implements ContextualDeserializer {
|
||||
|
||||
private final JavaType contentType;
|
||||
|
||||
CollectionJsonResourcesDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructType(CollectionJson.class));
|
||||
}
|
||||
|
||||
CollectionJsonResourcesDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
JavaType wrappedType = ctxt.getTypeFactory().constructParametricType(CollectionJsonDocument.class, rootType);
|
||||
|
||||
CollectionJsonDocument<?> document = jp.getCodec().readValue(jp, wrappedType);
|
||||
|
||||
List<Object> contentList = new ArrayList<Object>();
|
||||
|
||||
if (document.getCollection().getItems() != null) {
|
||||
for (CollectionJsonItem<?> item : document.getCollection().getItems()) {
|
||||
|
||||
Object data = item.toRawData(rootType);
|
||||
|
||||
if (this.contentType.hasGenericTypes()) {
|
||||
if (isResource(this.contentType)) {
|
||||
contentList.add(new Resource<>(data, potentiallyAddSelfLink(item.getLinks(), item.getHref())));
|
||||
} else {
|
||||
contentList.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Resources(contentList, potentiallyAddSelfLink(document.getCollection().getLinks(), document.getCollection().getHref()));
|
||||
}
|
||||
|
||||
static boolean isResource(JavaType type) {
|
||||
return type.containedType(0).hasRawClass(Resource.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
|
||||
BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
|
||||
JavaType vc = property.getType().getContentType();
|
||||
CollectionJsonResourcesDeserializer des = new CollectionJsonResourcesDeserializer(vc);
|
||||
|
||||
return des;
|
||||
} else {
|
||||
return new CollectionJsonResourcesDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionJsonPagedResourcesDeserializer extends ContainerDeserializerBase<PagedResources>
|
||||
implements ContextualDeserializer {
|
||||
|
||||
private final JavaType contentType;
|
||||
|
||||
CollectionJsonPagedResourcesDeserializer() {
|
||||
this(TypeFactory.defaultInstance().constructType(CollectionJson.class));
|
||||
}
|
||||
|
||||
CollectionJsonPagedResourcesDeserializer(JavaType contentType) {
|
||||
|
||||
super(contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<Object> getContentDeserializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedResources deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
|
||||
JavaType rootType = JacksonHelper.findRootType(this.contentType);
|
||||
JavaType wrappedType = ctxt.getTypeFactory().constructParametricType(CollectionJsonDocument.class, rootType);
|
||||
|
||||
CollectionJsonDocument<?> document = jp.getCodec().readValue(jp, wrappedType);
|
||||
|
||||
List<Object> items = new ArrayList<>();
|
||||
|
||||
document.getCollection().getItems().forEach(item -> {
|
||||
|
||||
Object data = item.toRawData(rootType);
|
||||
List<Link> links = item.getLinks() == null ? Collections.EMPTY_LIST : item.getLinks();
|
||||
|
||||
if (this.contentType.hasGenericTypes()) {
|
||||
|
||||
if (this.contentType.containedType(0).hasRawClass(Resource.class)) {
|
||||
items.add(new Resource<>(data, potentiallyAddSelfLink(links, item.getHref())));
|
||||
} else {
|
||||
items.add(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
PagedResources.PageMetadata pageMetadata = null;
|
||||
|
||||
return new PagedResources(items, pageMetadata,
|
||||
potentiallyAddSelfLink(document.getCollection().getLinks(), document.getCollection().getHref()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
|
||||
if (property != null) {
|
||||
|
||||
JavaType vc = property.getType().getContentType();
|
||||
CollectionJsonPagedResourcesDeserializer des = new CollectionJsonPagedResourcesDeserializer(vc);
|
||||
|
||||
return des;
|
||||
} else {
|
||||
return new CollectionJsonPagedResourcesDeserializer(ctxt.getContextualType());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CollectionJsonHandlerInstantiator extends HandlerInstantiator {
|
||||
|
||||
private final Map<Class<?>, Object> instanceMap = new HashMap<Class<?>, Object>();
|
||||
|
||||
public CollectionJsonHandlerInstantiator(MessageSourceAccessor messageSource) {
|
||||
|
||||
this.instanceMap.put(CollectionJsonPagedResourcesSerializer.class, new CollectionJsonPagedResourcesSerializer());
|
||||
this.instanceMap.put(CollectionJsonResourcesSerializer.class, new CollectionJsonResourcesSerializer());
|
||||
this.instanceMap.put(CollectionJsonResourceSerializer.class, new CollectionJsonResourceSerializer());
|
||||
this.instanceMap.put(CollectionJsonResourceSupportSerializer.class, new CollectionJsonResourceSupportSerializer());
|
||||
this.instanceMap.put(CollectionJsonLinkListSerializer.class, new CollectionJsonLinkListSerializer(messageSource));
|
||||
}
|
||||
|
||||
private Object findInstance(Class<?> type) {
|
||||
|
||||
Object result = instanceMap.get(type);
|
||||
return result != null ? result : BeanUtils.instantiateClass(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
|
||||
return (JsonDeserializer<?>) findInstance(deserClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
|
||||
return (KeyDeserializer) findInstance(keyDeserClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
|
||||
return (JsonSerializer<?>) findInstance(serClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
|
||||
return (TypeResolverBuilder<?>) findInstance(builderClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
|
||||
return (TypeIdResolver) findInstance(resolverClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of {@link Link}s that includes a "self" link.
|
||||
*
|
||||
* @param links - base set of {@link Link}s.
|
||||
* @param href - the URI of the "self" link
|
||||
* @return
|
||||
*/
|
||||
private static List<Link> potentiallyAddSelfLink(List<Link> links, String href) {
|
||||
|
||||
if (links == null) {
|
||||
|
||||
if (href == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.singletonList(new Link(href));
|
||||
}
|
||||
|
||||
if (href == null || links.stream().map(Link::getRel).anyMatch(s -> s.equals(Link.REL_SELF))) {
|
||||
return links;
|
||||
}
|
||||
|
||||
// Clone and add the self link
|
||||
|
||||
List<Link> newLinks = new ArrayList<>();
|
||||
newLinks.add(new Link(href));
|
||||
newLinks.addAll(links);
|
||||
|
||||
return newLinks;
|
||||
}
|
||||
|
||||
private static List<Link> withoutSelfLink(List<Link> links) {
|
||||
|
||||
return links.stream()
|
||||
.filter(link -> !link.getRel().equals(Link.REL_SELF))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<CollectionJsonItem<?>> resourcesToCollectionJsonItems(Resources<?> resources) {
|
||||
|
||||
return resources.getContent().stream()
|
||||
.map(content -> {
|
||||
if (ClassUtils.isAssignableValue(Resource.class, content)) {
|
||||
|
||||
Resource resource = (Resource) content;
|
||||
|
||||
return new CollectionJsonItem<>()
|
||||
.withHref(resource.getRequiredLink(Link.REL_SELF).getHref())
|
||||
.withLinks(withoutSelfLink(resource.getLinks()))
|
||||
.withRawData(resource.getContent());
|
||||
} else {
|
||||
return new CollectionJsonItem<>().withRawData(content);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan through the {@link Affordance}s and find any {@literal GET} calls against non-self URIs.
|
||||
*
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
private static List<CollectionJsonQuery> findQueries(ResourceSupport resource) {
|
||||
|
||||
List<CollectionJsonQuery> queries = new ArrayList<>();
|
||||
|
||||
if (resource.hasLink(Link.REL_SELF)) {
|
||||
Link selfLink = resource.getRequiredLink(Link.REL_SELF);
|
||||
|
||||
selfLink.getAffordances().forEach(affordance -> {
|
||||
|
||||
CollectionJsonAffordanceModel model = affordance.getAffordanceModel(MediaTypes.COLLECTION_JSON);
|
||||
|
||||
/**
|
||||
* For Collection+JSON, "queries" are only collected for GET affordances where the URI is NOT a self link.
|
||||
*/
|
||||
if (affordance.getHttpMethod().equals(HttpMethod.GET) && !model.getUri().equals(selfLink.getHref())) {
|
||||
|
||||
queries.add(new CollectionJsonQuery()
|
||||
.withRel(model.getRel())
|
||||
.withHref(model.getUri())
|
||||
.withData(model.getQueryProperties()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan through the {@link Affordance}s and
|
||||
* @param resource
|
||||
* @return
|
||||
*/
|
||||
private static CollectionJsonTemplate findTemplate(ResourceSupport resource) {
|
||||
|
||||
List<CollectionJsonTemplate> templates = new ArrayList<>();
|
||||
|
||||
if (resource.hasLink(Link.REL_SELF)) {
|
||||
resource.getRequiredLink(Link.REL_SELF).getAffordances().forEach(affordance -> {
|
||||
|
||||
CollectionJsonAffordanceModel model = affordance.getAffordanceModel(MediaTypes.COLLECTION_JSON);
|
||||
|
||||
/**
|
||||
* For Collection+JSON, "templates" are made of any non-GET affordances.
|
||||
*/
|
||||
if (!affordance.getHttpMethod().equals(HttpMethod.GET)) {
|
||||
|
||||
CollectionJsonTemplate template = new CollectionJsonTemplate() //
|
||||
.withData(model.getInputProperties());
|
||||
|
||||
templates.add(template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection+JSON can only have one template, so grab the first one.
|
||||
*/
|
||||
return templates.stream()
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.*;
|
||||
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link PagedResources}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonPagedResourcesSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonPagedResourcesDeserializer.class)
|
||||
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.*;
|
||||
|
||||
import org.springframework.hateoas.Resource;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to invoke the related serializer/deserizer.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonResourceSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonResourceDeserializer.class)
|
||||
abstract class ResourceMixin<T> extends Resource<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to invoke the related serializer/deserializer.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonResourceSupportSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonResourceSupportDeserializer.class)
|
||||
abstract class ResourceSupportMixin extends ResourceSupport {
|
||||
|
||||
@Override
|
||||
@XmlElement(name = "collection")
|
||||
@JsonProperty("collection")
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@JsonSerialize(using = CollectionJsonLinkListSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonLinkListDeserializer.class)
|
||||
public abstract List<Link> getLinks();
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.hateoas.collectionjson;
|
||||
|
||||
import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.*;
|
||||
|
||||
import org.springframework.hateoas.Resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to invoke the related serializer/deserizer.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonResourcesSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonResourcesDeserializer.class)
|
||||
abstract class ResourcesMixin<T> extends Resources<T> {
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.springframework.context.annotation.ImportSelector;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.hateoas.EntityLinks;
|
||||
import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.hateoas.collectionjson.CollectionJsonWebMvcConfigurer;
|
||||
import org.springframework.hateoas.hal.forms.HalFormsWebMvcConfigurer;
|
||||
|
||||
/**
|
||||
@@ -85,7 +86,15 @@ public @interface EnableHypermediaSupport {
|
||||
*
|
||||
* @see https://rwcbook.github.io/hal-forms/
|
||||
*/
|
||||
HAL_FORMS(HalFormsWebMvcConfigurer.class);
|
||||
HAL_FORMS(HalFormsWebMvcConfigurer.class),
|
||||
|
||||
/**
|
||||
* Collection+JSON
|
||||
*
|
||||
* @see http://amundsen.com/media-types/collection/format/
|
||||
*/
|
||||
COLLECTION_JSON(CollectionJsonWebMvcConfigurer.class);
|
||||
|
||||
|
||||
private final List<Class<?>> configurations;
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.hateoas.LinkDiscoverers;
|
||||
import org.springframework.hateoas.RelProvider;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.collectionjson.CollectionJsonLinkDiscoverer;
|
||||
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.hateoas.core.AnnotationRelProvider;
|
||||
import org.springframework.hateoas.core.DefaultRelProvider;
|
||||
@@ -88,6 +90,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
|
||||
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
|
||||
private static final String HAL_FORMS_OBJECT_MAPPER_BEAN_NAME = "_halFormsObjectMapper";
|
||||
private static final String COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME = "_collectionJsonObjectMapper";
|
||||
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
|
||||
|
||||
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
|
||||
@@ -129,6 +132,10 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
registerHypermediaComponents(metadata, registry, HAL_FORMS_OBJECT_MAPPER_BEAN_NAME);
|
||||
}
|
||||
|
||||
if (types.contains(HypermediaType.COLLECTION_JSON)) {
|
||||
registerHypermediaComponents(metadata, registry, COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME);
|
||||
}
|
||||
|
||||
if (!types.isEmpty()) {
|
||||
|
||||
BeanDefinitionBuilder linkDiscoverersRegistryBuilder = BeanDefinitionBuilder
|
||||
@@ -214,6 +221,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
case HAL_FORMS:
|
||||
definition = new RootBeanDefinition(HalFormsLinkDiscoverer.class);
|
||||
break;
|
||||
case COLLECTION_JSON:
|
||||
definition = new RootBeanDefinition(CollectionJsonLinkDiscoverer.class);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type));
|
||||
}
|
||||
@@ -316,7 +326,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
|
||||
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
|
||||
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
|
||||
MessageSourceAccessor.class);
|
||||
MessageSourceAccessor.class);
|
||||
|
||||
halObjectMapper.registerModule(new Jackson2HalModule());
|
||||
|
||||
@@ -340,7 +350,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
|
||||
ObjectMapper halFormsObjectMapper = beanFactory.getBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
|
||||
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
|
||||
MessageSourceAccessor.class);
|
||||
MessageSourceAccessor.class);
|
||||
|
||||
halFormsObjectMapper.registerModule(new Jackson2HalFormsModule());
|
||||
|
||||
@@ -354,12 +364,30 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
}
|
||||
|
||||
MappingJackson2HttpMessageConverter halFormsConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class);
|
||||
ResourceSupport.class);
|
||||
halFormsConverter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON));
|
||||
halFormsConverter.setObjectMapper(halFormsObjectMapper);
|
||||
result.add(halFormsConverter);
|
||||
}
|
||||
|
||||
if (beanFactory.containsBean(COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME)) {
|
||||
|
||||
ObjectMapper collectionJsonObjectMapper = beanFactory.getBean(COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
|
||||
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
|
||||
MessageSourceAccessor.class);
|
||||
|
||||
collectionJsonObjectMapper.registerModule(new Jackson2CollectionJsonModule());
|
||||
|
||||
collectionJsonObjectMapper.setHandlerInstantiator(
|
||||
new Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator(linkRelationMessageSource));
|
||||
|
||||
MappingJackson2HttpMessageConverter jsonCollectionConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class);
|
||||
jsonCollectionConverter.setSupportedMediaTypes(Arrays.asList(COLLECTION_JSON));
|
||||
jsonCollectionConverter.setObjectMapper(collectionJsonObjectMapper);
|
||||
result.add(jsonCollectionConverter);
|
||||
}
|
||||
|
||||
result.addAll(converters);
|
||||
|
||||
return result;
|
||||
@@ -390,7 +418,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
|
||||
if (HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName) || HAL_FORMS_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
|
||||
if (HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName) || HAL_FORMS_OBJECT_MAPPER_BEAN_NAME.equals(beanName) || COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
|
||||
ObjectMapper mapper = (ObjectMapper) bean;
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
return mapper;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.hateoas.core;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
@@ -30,6 +32,15 @@ import org.springframework.web.util.UriComponents;
|
||||
*/
|
||||
public interface AffordanceModelFactory extends Plugin<MediaType> {
|
||||
|
||||
/**
|
||||
* Declare the {@link MediaType} this factory supports.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
default MediaType getMediaType() {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Look up the {@link AffordanceModel} for this factory.
|
||||
*
|
||||
@@ -39,4 +50,17 @@ public interface AffordanceModelFactory extends Plugin<MediaType> {
|
||||
* @return
|
||||
*/
|
||||
AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components);
|
||||
|
||||
/**
|
||||
* Returns if a plugin should be invoked according to the given delimiter.
|
||||
*
|
||||
* @param delimiter
|
||||
* @return if the plugin should be invoked
|
||||
*/
|
||||
@Override
|
||||
default boolean supports(MediaType delimiter) {
|
||||
return Optional.ofNullable(getMediaType())
|
||||
.map(mediaType -> mediaType.equals(delimiter))
|
||||
.orElse(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer {
|
||||
public JsonPathLinkDiscoverer(String pathTemplate, MediaType mediaType, MediaType... others) {
|
||||
|
||||
Assert.hasText(pathTemplate, "Path template must not be null!");
|
||||
Assert.isTrue(StringUtils.countOccurrencesOf(pathTemplate, "%s") == 1,
|
||||
"Path template must contain a single placeholder!");
|
||||
// Assert.isTrue(StringUtils.countOccurrencesOf(pathTemplate, "%s") == 1,
|
||||
// "Path template must contain a single placeholder!");
|
||||
Assert.notNull(mediaType, "Primary MediaType must not be null!");
|
||||
Assert.notNull(others, "Other MediaTypes must not be null!");
|
||||
|
||||
|
||||
@@ -15,29 +15,21 @@
|
||||
*/
|
||||
package org.springframework.hateoas.hal.forms;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
import org.springframework.hateoas.core.MethodParameters;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
@@ -45,40 +37,41 @@ import org.springframework.web.util.UriComponents;
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Slf4j
|
||||
class HalFormsAffordanceModel implements AffordanceModel {
|
||||
|
||||
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
|
||||
HttpMethod.PATCH);
|
||||
|
||||
private final Affordance affordance;
|
||||
private final UriComponents components;
|
||||
private final boolean required;
|
||||
private final Map<String, Class<?>> properties;
|
||||
private final List<String> properties;
|
||||
|
||||
public HalFormsAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
|
||||
public HalFormsAffordanceModel(Affordance affordance, UriComponents components) {
|
||||
|
||||
this.affordance = affordance;
|
||||
this.components = components;
|
||||
this.required = determineRequired(affordance.getHttpMethod());
|
||||
this.properties = METHODS_FOR_INPUT_DETECTTION.contains(affordance.getHttpMethod()) //
|
||||
? determineAffordanceInputs(invocationValue.getMethod()) //
|
||||
: Collections.<String, Class<?>> emptyMap();
|
||||
? determineAffordanceInputs() //
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the details of the Spring MVC method's {@link RequestBody} into a collection of
|
||||
* Transform the details of the REST method's {@link MethodParameters} into
|
||||
* {@link HalFormsProperty}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<HalFormsProperty> getProperties() {
|
||||
|
||||
return properties.entrySet().stream() //
|
||||
.map(entry -> entry.getKey()) //
|
||||
.map(key -> HalFormsProperty.named(key).withRequired(required)).collect(Collectors.toList());
|
||||
return properties.stream() //
|
||||
.map(name -> HalFormsProperty.named(name).withRequired(required)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return components.getPath();
|
||||
public String getURI() {
|
||||
return components.toUriString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +84,7 @@ class HalFormsAffordanceModel implements AffordanceModel {
|
||||
|
||||
Assert.notNull(path, "Path must not be null!");
|
||||
|
||||
return getPath().equals(path);
|
||||
return getURI().equals(path);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -115,39 +108,15 @@ class HalFormsAffordanceModel implements AffordanceModel {
|
||||
|
||||
/**
|
||||
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
|
||||
*
|
||||
* @param method - {@link Method} of the Spring MVC controller tied to this affordance
|
||||
*/
|
||||
private Map<String, Class<?>> determineAffordanceInputs(Method method) {
|
||||
private List<String> determineAffordanceInputs() {
|
||||
|
||||
if (method == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
LOG.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName());
|
||||
|
||||
Map<String, Class<?>> properties = new TreeMap<>();
|
||||
MethodParameters parameters = new MethodParameters(method);
|
||||
|
||||
for (MethodParameter parameter : parameters.getParametersWith(RequestBody.class)) {
|
||||
|
||||
Class<?> parameterType = parameter.getParameterType();
|
||||
|
||||
LOG.debug("\tRequest body: " + parameterType.getCanonicalName() + "(");
|
||||
|
||||
for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(parameterType)) {
|
||||
|
||||
if (!descriptor.getName().equals("class")) {
|
||||
|
||||
LOG.debug("\t\t" + descriptor.getPropertyType().getCanonicalName() + " " + descriptor.getName());
|
||||
properties.put(descriptor.getName(), descriptor.getPropertyType());
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug(")");
|
||||
}
|
||||
|
||||
LOG.debug("Assembled " + this.toString());
|
||||
return properties;
|
||||
return this.affordance.getInputMethodParameters().stream()
|
||||
.findFirst()
|
||||
.map(methodParameter -> {
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
|
||||
return PropertyUtils.findProperties(resolvableType);
|
||||
})
|
||||
.orElse(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.hateoas.hal.forms;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
@@ -31,6 +33,8 @@ import org.springframework.web.util.UriComponents;
|
||||
*/
|
||||
class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
|
||||
|
||||
private final @Getter MediaType mediaType = MediaTypes.HAL_FORMS_JSON;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(org.springframework.hateoas.Affordance, org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation, org.springframework.web.util.UriComponents)
|
||||
@@ -38,15 +42,6 @@ class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
|
||||
@Override
|
||||
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue,
|
||||
UriComponents components) {
|
||||
return new HalFormsAffordanceModel(affordance, invocationValue, components);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(MediaType mediaType) {
|
||||
return MediaTypes.HAL_FORMS_JSON.equals(mediaType);
|
||||
return new HalFormsAffordanceModel(affordance, components);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,9 @@
|
||||
package org.springframework.hateoas.hal.forms;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.Link;
|
||||
@@ -224,18 +221,12 @@ class HalFormsSerializers {
|
||||
*/
|
||||
private static void validate(ResourceSupport resource, Affordance affordance, HalFormsAffordanceModel model) {
|
||||
|
||||
try {
|
||||
String affordanceUri = model.getURI();
|
||||
String selfLinkUri = resource.getRequiredLink(Link.REL_SELF).getHref();
|
||||
|
||||
Optional<Link> selfLink = resource.getLink(Link.REL_SELF);
|
||||
URI selfLinkUri = new URI(selfLink.map(link -> link.expand().getHref()).orElse(""));
|
||||
|
||||
if (!model.hasPath(selfLinkUri.getPath())) {
|
||||
throw new IllegalStateException("Affordance's URI " + model.getPath() + " doesn't match self link "
|
||||
+ selfLinkUri.getPath() + " as expected in HAL-FORMS");
|
||||
}
|
||||
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
if (!affordanceUri.equals(selfLinkUri)) {
|
||||
throw new IllegalStateException("Affordance's URI " + affordanceUri + " doesn't match self link "
|
||||
+ selfLinkUri + " as expected in HAL-FORMS");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
static UriComponentsBuilder getBuilder() {
|
||||
public static UriComponentsBuilder getBuilder() {
|
||||
|
||||
if (RequestContextHolder.getRequestAttributes() == null) {
|
||||
return UriComponentsBuilder.fromPath("/");
|
||||
|
||||
@@ -21,14 +21,21 @@ import lombok.Value;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.MethodParameters;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* Spring MVC-based representation of an {@link Affordance}.
|
||||
@@ -89,4 +96,30 @@ class SpringMvcAffordance implements Affordance {
|
||||
this.affordanceModels.put(mediaType, affordanceModel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of {@link MethodParameter}s based on Spring MVC's {@link RequestBody}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<MethodParameter> getInputMethodParameters() {
|
||||
return new MethodParameters(this.method).getParametersWith(RequestBody.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of {@link MethodParameter}s based on Spring MVC's {@link RequestParam}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<QueryParameter> getQueryMethodParameters() {
|
||||
|
||||
MethodParameters parameters = new MethodParameters(this.method);
|
||||
|
||||
return parameters.getParametersWith(RequestParam.class).stream()
|
||||
.map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class))
|
||||
.map(requestParam -> new QueryParameter(requestParam.name(), requestParam.required(), requestParam.value()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2018 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.hateoas.support;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
|
||||
/**
|
||||
* Jackson utility methods.
|
||||
*/
|
||||
public final class JacksonHelper {
|
||||
|
||||
/**
|
||||
* Navigate a chain of parametric types (e.g. Resources<Resource<String>>) until you find the innermost type (String).
|
||||
*
|
||||
* @param contentType
|
||||
* @return
|
||||
*/
|
||||
public static JavaType findRootType(JavaType contentType) {
|
||||
|
||||
if (contentType.hasGenericTypes()) {
|
||||
return findRootType(contentType.containedType(0));
|
||||
} else {
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2017 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.hateoas.support;
|
||||
|
||||
import java.beans.FeatureDescriptor;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class PropertyUtils {
|
||||
|
||||
private final static HashSet<String> FIELDS_TO_IGNORE = new HashSet<String>() {{
|
||||
add("class");
|
||||
add("links");
|
||||
}};
|
||||
|
||||
public static Map<String, Object> findProperties(Object object) {
|
||||
|
||||
if (object.getClass().equals(Resource.class)) {
|
||||
return findProperties(((Resource<?>) object).getContent());
|
||||
}
|
||||
|
||||
return Arrays.asList(BeanUtils.getPropertyDescriptors(object.getClass())).stream()
|
||||
.filter(descriptor -> !FIELDS_TO_IGNORE.contains(descriptor.getName()))
|
||||
.filter(descriptor -> hasJsonIgnoreOnTheField(object.getClass(), descriptor))
|
||||
.filter(PropertyUtils::hasJsonIgnoreOnTheReader)
|
||||
.collect(Collectors.toMap(
|
||||
FeatureDescriptor::getName,
|
||||
descriptor -> {
|
||||
try {
|
||||
return descriptor.getReadMethod().invoke(object);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static List<String> findProperties(ResolvableType resolvableType) {
|
||||
|
||||
if (resolvableType.getRawClass().equals(Resource.class)) {
|
||||
return findProperties(resolvableType.resolveGeneric(0));
|
||||
} else {
|
||||
return findProperties(resolvableType.getRawClass());
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> findProperties(Class<?> clazz) {
|
||||
|
||||
return Arrays.asList(BeanUtils.getPropertyDescriptors(clazz)).stream()
|
||||
.filter(descriptor -> !FIELDS_TO_IGNORE.contains(descriptor.getName()))
|
||||
.filter(descriptor -> hasJsonIgnoreOnTheField(clazz, descriptor))
|
||||
.filter(PropertyUtils::hasJsonIgnoreOnTheReader)
|
||||
.map(FeatureDescriptor::getName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static Object createObjectFromProperties(Class<?> clazz, Map<String, Object> properties) {
|
||||
|
||||
Object obj = BeanUtils.instantiateClass(clazz);
|
||||
|
||||
properties.entrySet().stream().forEach(entry -> {
|
||||
Optional<PropertyDescriptor> possibleProperty = Optional.ofNullable(BeanUtils.getPropertyDescriptor(clazz, entry.getKey()));
|
||||
possibleProperty.ifPresent(property -> {
|
||||
try {
|
||||
Method writeMethod = property.getWriteMethod();
|
||||
ReflectionUtils.makeAccessible(writeMethod);
|
||||
writeMethod.invoke(obj, entry.getValue());
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@link PropertyDescriptor} has {@link JsonIgnore} applied to the field declaration.
|
||||
*
|
||||
* @param object
|
||||
* @param descriptor
|
||||
* @return
|
||||
*/
|
||||
private static boolean hasJsonIgnoreOnTheField(Class<?> clazz, PropertyDescriptor descriptor) {
|
||||
|
||||
Field descriptorField = ReflectionUtils.findField(clazz, descriptor.getName());
|
||||
|
||||
return isToBeIgnored(AnnotationUtils.getAnnotations(descriptorField));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@link PropertyDescriptor} has {@link JsonIgnore} on the getter.
|
||||
*
|
||||
* @param object
|
||||
* @param descriptor
|
||||
* @return
|
||||
*/
|
||||
private static boolean hasJsonIgnoreOnTheReader(PropertyDescriptor descriptor) {
|
||||
return isToBeIgnored(AnnotationUtils.getAnnotations(descriptor.getReadMethod()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a list of {@link Annotation}s for {@link JsonIgnore} annotations.
|
||||
*
|
||||
* @param annotations
|
||||
* @return
|
||||
*/
|
||||
private static boolean isToBeIgnored(Annotation[] annotations) {
|
||||
|
||||
if (annotations != null) {
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType().equals(JsonIgnore.class)) {
|
||||
return !(Boolean) AnnotationUtils.getAnnotationAttributes(annotation).get("value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user