diff --git a/pom.xml b/pom.xml index 1aa14183..4803f720 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,7 @@ 2.9.8 spring.hateoas 2.2.0 + 3.0.2 2.2.1 Californium-SR4 1.7.25 @@ -558,6 +559,12 @@ ${slf4j.version} + + com.google.code.findbugs + jsr305 + ${jsr305.version} + + org.slf4j jcl-over-slf4j diff --git a/src/main/java/org/springframework/hateoas/EntityModel.java b/src/main/java/org/springframework/hateoas/EntityModel.java index 8b6408ed..22b625a6 100644 --- a/src/main/java/org/springframework/hateoas/EntityModel.java +++ b/src/main/java/org/springframework/hateoas/EntityModel.java @@ -18,6 +18,7 @@ package org.springframework.hateoas; import java.util.Arrays; import java.util.Collection; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonUnwrapped; @@ -55,7 +56,7 @@ public class EntityModel extends RepresentationModel> { * @param content must not be {@literal null}. * @param links the links to add to the {@link EntityModel}. */ - public EntityModel(T content, Iterable links) { + public EntityModel(@Nullable T content, Iterable links) { Assert.notNull(content, "Content must not be null!"); Assert.isTrue(!(content instanceof Collection), "Content must not be a collection! Use Resources instead!"); @@ -69,6 +70,7 @@ public class EntityModel extends RepresentationModel> { * @return the content */ @JsonUnwrapped + @Nullable public T getContent() { return content; } diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java index 4c94fd85..04353307 100755 --- a/src/main/java/org/springframework/hateoas/Link.java +++ b/src/main/java/org/springframework/hateoas/Link.java @@ -33,6 +33,7 @@ import java.util.regex.Pattern; import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -238,8 +239,14 @@ public class Link implements Serializable { */ public Link andAffordance(HttpMethod httpMethod, ResolvableType inputType, List queryMethodParameters, ResolvableType outputType) { - return andAffordance(httpMethod.toString().toLowerCase() + inputType.resolve().getSimpleName(), httpMethod, - inputType, queryMethodParameters, outputType); + + String name = httpMethod.toString().toLowerCase(); + + if (inputType.resolve() != null) { + name += inputType.resolve().getSimpleName(); + } + + return andAffordance(name, httpMethod, inputType, queryMethodParameters, outputType); } /** @@ -458,6 +465,7 @@ public class Link implements Serializable { * @throws IllegalArgumentException if no {@code rel} attribute could be found. * @return */ + @Nullable public static Link valueOf(String element) { if (!StringUtils.hasText(element)) { diff --git a/src/main/java/org/springframework/hateoas/PagedModel.java b/src/main/java/org/springframework/hateoas/PagedModel.java index 68ecf6f4..e66b08ef 100644 --- a/src/main/java/org/springframework/hateoas/PagedModel.java +++ b/src/main/java/org/springframework/hateoas/PagedModel.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Optional; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -51,7 +52,7 @@ public class PagedModel extends CollectionModel { * @param metadata * @param links */ - public PagedModel(Collection content, PageMetadata metadata, Link... links) { + public PagedModel(Collection content, @Nullable PageMetadata metadata, Link... links) { this(content, metadata, Arrays.asList(links)); } @@ -62,7 +63,7 @@ public class PagedModel extends CollectionModel { * @param metadata * @param links */ - public PagedModel(Collection content, PageMetadata metadata, Iterable links) { + public PagedModel(Collection content, @Nullable PageMetadata metadata, Iterable links) { super(content, links); @@ -75,6 +76,7 @@ public class PagedModel extends CollectionModel { * @return the metadata */ @JsonProperty("page") + @Nullable public PageMetadata getMetadata() { return metadata; } diff --git a/src/main/java/org/springframework/hateoas/StringLinkRelation.java b/src/main/java/org/springframework/hateoas/StringLinkRelation.java index ac018cfe..462cf782 100644 --- a/src/main/java/org/springframework/hateoas/StringLinkRelation.java +++ b/src/main/java/org/springframework/hateoas/StringLinkRelation.java @@ -15,9 +15,7 @@ */ package org.springframework.hateoas; -import lombok.AccessLevel; import lombok.NonNull; -import lombok.RequiredArgsConstructor; import lombok.Value; import java.io.Serializable; @@ -36,7 +34,6 @@ import com.fasterxml.jackson.annotation.JsonValue; * @author Oliver Drotbohm */ @Value -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) class StringLinkRelation implements LinkRelation, Serializable { private static final long serialVersionUID = -3904935345545567957L; @@ -44,6 +41,13 @@ class StringLinkRelation implements LinkRelation, Serializable { @NonNull String relation; + private StringLinkRelation(String relation) { + + Assert.notNull(relation, "relation must not be null!"); + + this.relation = relation; + } + /** * Returns a (potentially cached) {@link LinkRelation} for the given value. * diff --git a/src/main/java/org/springframework/hateoas/UriTemplate.java b/src/main/java/org/springframework/hateoas/UriTemplate.java index 577067c6..26022d9e 100644 --- a/src/main/java/org/springframework/hateoas/UriTemplate.java +++ b/src/main/java/org/springframework/hateoas/UriTemplate.java @@ -27,6 +27,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.hateoas.TemplateVariable.VariableType; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; @@ -285,7 +286,7 @@ public class UriTemplate implements Iterable, Serializable { * @param variable must not be {@literal null}. * @param value can be {@literal null}. */ - private static void appendToBuilder(UriComponentsBuilder builder, TemplateVariable variable, Object value) { + private static void appendToBuilder(UriComponentsBuilder builder, TemplateVariable variable, @Nullable Object value) { if (value == null) { diff --git a/src/main/java/org/springframework/hateoas/client/Rels.java b/src/main/java/org/springframework/hateoas/client/Rels.java index f1794e7a..1299df0b 100644 --- a/src/main/java/org/springframework/hateoas/client/Rels.java +++ b/src/main/java/org/springframework/hateoas/client/Rels.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.springframework.hateoas.Link; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.jayway.jsonpath.JsonPath; @@ -60,7 +61,7 @@ class Rels { * @param mediaType * @return */ - Optional findInResponse(String representation, MediaType mediaType); + Optional findInResponse(@Nullable String representation, @Nullable MediaType mediaType); } /** @@ -141,7 +142,7 @@ class Rels { * @see org.springframework.hateoas.client.Rels.Rel#findInResponse(java.lang.String, org.springframework.http.MediaType) */ @Override - public Optional findInResponse(String representation, MediaType mediaType) { + public Optional findInResponse(@Nullable String representation, @Nullable MediaType mediaType) { return Optional.of(new Link(JsonPath.read(representation, jsonPath).toString(), rel)); } } diff --git a/src/main/java/org/springframework/hateoas/client/Traverson.java b/src/main/java/org/springframework/hateoas/client/Traverson.java index ad7ed7f6..db8cb907 100644 --- a/src/main/java/org/springframework/hateoas/client/Traverson.java +++ b/src/main/java/org/springframework/hateoas/client/Traverson.java @@ -40,6 +40,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; import org.springframework.web.client.RestOperations; @@ -139,7 +140,7 @@ public class Traverson { * @param operations * @return */ - public Traverson setRestOperations(RestOperations operations) { + public Traverson setRestOperations(@Nullable RestOperations operations) { this.operations = operations == null // ? createDefaultTemplate(this.mediaTypes) // @@ -155,7 +156,7 @@ public class Traverson { * @param discoverer can be {@literal null}. * @return */ - public Traverson setLinkDiscoverers(List discoverer) { + public Traverson setLinkDiscoverers(@Nullable List discoverer) { List defaultedDiscoverers = discoverer == null // ? DEFAULTS.getLinkDiscoverers(mediaTypes) // @@ -282,6 +283,7 @@ public class Traverson { * @param type must not be {@literal null}. * @return */ + @Nullable public T toObject(Class type) { Assert.notNull(type, "Target type must not be null!"); @@ -299,6 +301,7 @@ public class Traverson { * @param type must not be {@literal null}. * @return */ + @Nullable public T toObject(ParameterizedTypeReference type) { Assert.notNull(type, "Target type must not be null!"); diff --git a/src/main/java/org/springframework/hateoas/client/package-info.java b/src/main/java/org/springframework/hateoas/client/package-info.java index 50537d2d..0df51da2 100644 --- a/src/main/java/org/springframework/hateoas/client/package-info.java +++ b/src/main/java/org/springframework/hateoas/client/package-info.java @@ -1,5 +1,7 @@ /** * Client side support. */ +@NonNullApi package org.springframework.hateoas.client; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java index a19c1852..259eb887 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java @@ -17,6 +17,7 @@ package org.springframework.hateoas.config; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; @@ -43,9 +44,10 @@ class HypermediaConfigurationImportSelector implements ImportSelector { Map attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName()); - Collection types = Arrays.stream((HypermediaType[]) attributes.get("type")) // - .flatMap(it -> it.getMediaTypes().stream()) // - .collect(Collectors.toList()); + Collection types = attributes == null ? Collections.emptyList() + : Arrays.stream((HypermediaType[]) attributes.get("type")) // + .flatMap(it -> it.getMediaTypes().stream()) // + .collect(Collectors.toList()); Collection configurationProviders = SpringFactoriesLoader.loadFactories( MediaTypeConfigurationProvider.class, HypermediaConfigurationImportSelector.class.getClassLoader()); diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java b/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java index 830774c5..a5281239 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Optional; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; @@ -63,6 +64,7 @@ public interface HypermediaMappingInformation { * @return * @see #configureObjectMapper(ObjectMapper) */ + @Nullable default Module getJacksonModule() { return null; } diff --git a/src/main/java/org/springframework/hateoas/config/package-info.java b/src/main/java/org/springframework/hateoas/config/package-info.java index fef5e93f..a25e559c 100644 --- a/src/main/java/org/springframework/hateoas/config/package-info.java +++ b/src/main/java/org/springframework/hateoas/config/package-info.java @@ -1,5 +1,7 @@ /** * Spring container configuration support. */ +@NonNullApi package org.springframework.hateoas.config; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java b/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java index 7746dfd5..5ac33148 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java +++ b/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java @@ -22,6 +22,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -37,6 +38,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.support.WebStack; +import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -54,7 +56,11 @@ public class PropertyUtils { FIELDS_TO_IGNORE.add("links"); } - public static Map findProperties(Object object) { + public static Map findProperties(@Nullable Object object) { + + if (object == null) { + return Collections.emptyMap(); + } if (object.getClass().equals(EntityModel.class)) { return findProperties(((EntityModel) object).getContent()); @@ -83,8 +89,18 @@ public class PropertyUtils { } } + if (resolvableType.getRawClass() == null) { + return Collections.emptyList(); + } + if (resolvableType.getRawClass().equals(EntityModel.class)) { - return findPropertyNames(resolvableType.resolveGeneric(0)); + Class genericEntityModelParameter = resolvableType.resolveGeneric(0); + + if (genericEntityModelParameter == null) { + return Collections.emptyList(); + } + + return findPropertyNames(genericEntityModelParameter); } else { return findPropertyNames(resolvableType.getRawClass()); } @@ -143,6 +159,10 @@ public class PropertyUtils { Field descriptorField = ReflectionUtils.findField(clazz, descriptor.getName()); + if (descriptorField == null) { + return false; + } + return toBeIgnoredByJackson(AnnotationUtils.getAnnotations(descriptorField)); } @@ -162,7 +182,7 @@ public class PropertyUtils { * @param annotations * @return */ - private static boolean toBeIgnoredByJackson(Annotation[] annotations) { + private static boolean toBeIgnoredByJackson(@Nullable Annotation[] annotations) { if (annotations != null) { for (Annotation annotation : annotations) { @@ -184,12 +204,16 @@ public class PropertyUtils { */ private static boolean toBeIgnoredByJackson(Class clazz, String field) { - for (Annotation annotation : AnnotationUtils.getAnnotations(clazz)) { - if (annotation.annotationType().equals(JsonIgnoreProperties.class)) { - String[] namesOfPropertiesToIgnore = (String[]) AnnotationUtils.getAnnotationAttributes(annotation).get("value"); - for (String propertyToIgnore : namesOfPropertiesToIgnore) { - if (propertyToIgnore.equalsIgnoreCase(field)) { - return true; + Annotation[] annotations = AnnotationUtils.getAnnotations(clazz); + + if (annotations != null) { + for (Annotation annotation : annotations) { + if (annotation.annotationType().equals(JsonIgnoreProperties.class)) { + String[] namesOfPropertiesToIgnore = (String[]) AnnotationUtils.getAnnotationAttributes(annotation).get("value"); + for (String propertyToIgnore : namesOfPropertiesToIgnore) { + if (propertyToIgnore.equalsIgnoreCase(field)) { + return true; + } } } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/alps/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/alps/package-info.java index ba943974..07e85bf0 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/alps/package-info.java +++ b/src/main/java/org/springframework/hateoas/mediatype/alps/package-info.java @@ -3,5 +3,7 @@ * * @see https://alps.io */ +@NonNullApi package org.springframework.hateoas.mediatype.alps; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJson.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJson.java index 3ce784c2..31b7ca79 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJson.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJson.java @@ -26,6 +26,7 @@ import java.util.List; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.Links.MergeMode; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; @@ -52,12 +53,12 @@ class CollectionJson { @JsonCreator CollectionJson(@JsonProperty("version") String version, // - @JsonProperty("href") String href, // - @JsonProperty("links") Links links, // - @JsonProperty("items") List> items, // - @JsonProperty("queries") List queries, // - @JsonProperty("template") CollectionJsonTemplate template, // - @JsonProperty("error") CollectionJsonError error) { + @JsonProperty("href") @Nullable String href, // + @JsonProperty("links") @Nullable Links links, // + @JsonProperty("items") @Nullable List> items, // + @JsonProperty("queries") @Nullable List queries, // + @JsonProperty("template") @Nullable CollectionJsonTemplate template, // + @JsonProperty("error") @Nullable CollectionJsonError error) { this.version = version; this.href = href; diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonData.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonData.java index b6689407..7fd95ab7 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonData.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonData.java @@ -18,6 +18,8 @@ package org.springframework.hateoas.mediatype.collectionjson; import lombok.Value; import lombok.experimental.Wither; +import org.springframework.lang.Nullable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -42,9 +44,9 @@ class CollectionJsonData { private String prompt; @JsonCreator - CollectionJsonData(@JsonProperty("name") String name, @JsonProperty("value") Object value, - @JsonProperty("prompt") String prompt) { - + CollectionJsonData(@JsonProperty("name") @Nullable String name, @JsonProperty("value") @Nullable Object value, + @JsonProperty("prompt") @Nullable String prompt) { + this.name = name; this.value = value; this.prompt = prompt; diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonError.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonError.java index 1f7af474..6699e82b 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonError.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonError.java @@ -19,6 +19,8 @@ import lombok.AccessLevel; import lombok.Value; import lombok.experimental.Wither; +import org.springframework.lang.Nullable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -34,8 +36,8 @@ class CollectionJsonError { private String message; @JsonCreator - CollectionJsonError(@JsonProperty("title") String title, @JsonProperty("code") String code, - @JsonProperty("message") String message) { + CollectionJsonError(@JsonProperty("title") @Nullable String title, @JsonProperty("code") @Nullable String code, + @JsonProperty("message") @Nullable String message) { this.title = title; this.code = code; diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonItem.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonItem.java index f16fb0ba..171451b8 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonItem.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonItem.java @@ -30,6 +30,7 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.Links.MergeMode; import org.springframework.hateoas.mediatype.PropertyUtils; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -54,9 +55,9 @@ class CollectionJsonItem { private @Getter(onMethod = @__({ @JsonIgnore }), value = AccessLevel.PRIVATE) T rawData; @JsonCreator - CollectionJsonItem(@JsonProperty("href") String href, // - @JsonProperty("data") List data, // - @JsonProperty("links") Links links) { + CollectionJsonItem(@JsonProperty("href") @Nullable String href, // + @JsonProperty("data") @Nullable List data, // + @JsonProperty("links") @Nullable Links links) { this.href = href; this.data = data; @@ -84,7 +85,7 @@ class CollectionJsonItem { return this.data; } - if (PRIMITIVE_TYPES.contains(this.rawData.getClass())) { + if (this.rawData != null && PRIMITIVE_TYPES.contains(this.rawData.getClass())) { return Collections.singletonList(new CollectionJsonData().withValue(this.rawData)); } @@ -99,8 +100,13 @@ class CollectionJsonItem { * @param javaType - type of the object to create * @return */ + @Nullable public Object toRawData(JavaType javaType) { + if (this.data == null) { + return null; + } + if (PRIMITIVE_TYPES.contains(javaType.getRawClass())) { return this.data.get(0).getValue(); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonQuery.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonQuery.java index c40d0e92..ddf672da 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonQuery.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonQuery.java @@ -22,6 +22,8 @@ import lombok.experimental.Wither; import java.util.List; +import org.springframework.lang.Nullable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -33,21 +35,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; @Wither class CollectionJsonQuery { - @JsonInclude(Include.NON_NULL) - private String rel; + @JsonInclude(Include.NON_NULL) private String rel; - @JsonInclude(Include.NON_NULL) - private String href; - - @JsonInclude(Include.NON_NULL) - private String prompt; + @JsonInclude(Include.NON_NULL) private String href; - @JsonInclude(Include.NON_EMPTY) - private List data; + @JsonInclude(Include.NON_NULL) private String prompt; + + @JsonInclude(Include.NON_EMPTY) private List data; @JsonCreator - CollectionJsonQuery(@JsonProperty("rel") String rel, @JsonProperty("href") String href, - @JsonProperty("prompt") String prompt, @JsonProperty("data") List data) { + CollectionJsonQuery(@JsonProperty("rel") @Nullable String rel, @JsonProperty("href") @Nullable String href, + @JsonProperty("prompt") @Nullable String prompt, @JsonProperty("data") @Nullable List data) { this.rel = rel; this.href = href; diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonTemplate.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonTemplate.java index 8cc1cc0f..f283c808 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonTemplate.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonTemplate.java @@ -20,6 +20,8 @@ import lombok.experimental.Wither; import java.util.List; +import org.springframework.lang.Nullable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -33,7 +35,7 @@ class CollectionJsonTemplate { private List data; @JsonCreator - CollectionJsonTemplate(@JsonProperty("data") List data) { + CollectionJsonTemplate(@JsonProperty("data") @Nullable List data) { this.data = data; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonModule.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonModule.java index 1e14196b..8f864c1a 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonModule.java @@ -38,6 +38,7 @@ import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.JacksonHelper; import org.springframework.hateoas.mediatype.PropertyUtils; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -137,6 +138,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -155,6 +157,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) */ @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -171,7 +174,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { this(null); } - CollectionJsonResourceSupportSerializer(BeanProperty property) { + CollectionJsonResourceSupportSerializer(@Nullable BeanProperty property) { super(RepresentationModel.class, false); this.property = property; @@ -211,11 +214,13 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -226,6 +231,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -242,7 +248,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { this(null); } - CollectionJsonResourceSerializer(BeanProperty property) { + CollectionJsonResourceSerializer(@Nullable BeanProperty property) { super(EntityModel.class, false); this.property = property; @@ -278,11 +284,13 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -293,6 +301,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -333,6 +342,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -342,6 +352,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -369,6 +380,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) */ @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -385,7 +397,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { this(null); } - CollectionJsonPagedResourcesSerializer(BeanProperty property) { + CollectionJsonPagedResourcesSerializer(@Nullable BeanProperty property) { super(CollectionModel.class, false); this.property = property; @@ -414,11 +426,13 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -434,6 +448,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -452,6 +467,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -461,6 +477,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -509,6 +526,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -518,6 +536,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) */ @Override + @Nullable public RepresentationModel deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { TypeFactory typeFactory = ctxt.getTypeFactory(); @@ -544,8 +563,12 @@ class Jackson2CollectionJsonModule extends SimpleModule { CollectionJsonItem firstItem = items.get(0).withOwnSelfLink(); RepresentationModel resource = (RepresentationModel) firstItem.toRawData(this.contentType); - return resource.add(firstItem.getLinks().merge(merged)); + if (resource != null) { + resource.add(firstItem.getLinks().merge(merged)); + } + + return resource; } if (withOwnSelfLink.getTemplate() != null) { @@ -599,6 +622,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { } @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -689,6 +713,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -819,6 +844,7 @@ class Jackson2CollectionJsonModule extends SimpleModule { * @param resource * @return */ + @Nullable private static CollectionJsonTemplate findTemplate(RepresentationModel resource) { if (!resource.hasLink(IanaLinkRelations.SELF)) { diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/package-info.java new file mode 100644 index 00000000..bda30ccd --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/package-info.java @@ -0,0 +1,7 @@ +/** + * Value objects to build Collection+JSON representations. + */ +@NonNullApi +package org.springframework.hateoas.mediatype.collectionjson; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/CurieProvider.java b/src/main/java/org/springframework/hateoas/mediatype/hal/CurieProvider.java index a3bbaad2..e61157df 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/CurieProvider.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/CurieProvider.java @@ -20,6 +20,7 @@ import java.util.Collection; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.Links; +import org.springframework.lang.Nullable; /** * API to provide HAL curie information for links. @@ -48,6 +49,7 @@ public interface CurieProvider { * @return * @since 0.17 */ + @Nullable HalLinkRelation getNamespacedRelFor(LinkRelation rel); /** diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/DefaultCurieProvider.java b/src/main/java/org/springframework/hateoas/mediatype/hal/DefaultCurieProvider.java index b4abc2a9..05590b5e 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/DefaultCurieProvider.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/DefaultCurieProvider.java @@ -26,6 +26,7 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.Links; import org.springframework.hateoas.UriTemplate; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -75,7 +76,7 @@ public class DefaultCurieProvider implements CurieProvider { * @param defaultCurieName can be {@literal null}. * @since 0.19 */ - public DefaultCurieProvider(Map curies, String defaultCurieName) { + public DefaultCurieProvider(Map curies, @Nullable String defaultCurieName) { Assert.notNull(curies, "Curies must not be null!"); @@ -109,6 +110,7 @@ public class DefaultCurieProvider implements CurieProvider { * @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(org.springframework.hateoas.Link) */ @Override + @Nullable public HalLinkRelation getNamespacedRelFrom(Link link) { return getNamespacedRelFor(link.getRel()); } @@ -118,6 +120,7 @@ public class DefaultCurieProvider implements CurieProvider { * @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(java.lang.String) */ @Override + @Nullable public HalLinkRelation getNamespacedRelFor(LinkRelation relation) { HalLinkRelation result = HalLinkRelation.of(relation); diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java index 1d13a167..77a22615 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalEmbeddedBuilder.java @@ -22,11 +22,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.hateoas.server.core.EmbeddedWrapper; import org.springframework.hateoas.server.core.EmbeddedWrappers; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -47,7 +48,8 @@ class HalEmbeddedBuilder { private final EmbeddedWrappers wrappers; /** - * Creates a new {@link HalEmbeddedBuilder} using the given {@link LinkRelationProvider} and prefer collection rels flag. + * Creates a new {@link HalEmbeddedBuilder} using the given {@link LinkRelationProvider} and prefer collection rels + * flag. * * @param provider can be {@literal null}. * @param preferCollectionRels whether to prefer to ask the provider for collection rels. @@ -99,7 +101,7 @@ class HalEmbeddedBuilder { } @SuppressWarnings("unchecked") - private Collection asCollection(Object source) { + private Collection asCollection(@Nullable Object source) { return source instanceof Collection // ? (Collection) source // diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java index 15169f25..fa032df7 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkRelation.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; import org.springframework.context.MessageSourceResolvable; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.LinkRelation; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonCreator; @@ -53,7 +54,7 @@ public class HalLinkRelation implements LinkRelation, MessageSourceResolvable { * @param relation must not be {@literal null}. * @return */ - public static HalLinkRelation of(LinkRelation relation) { + public static HalLinkRelation of(@Nullable LinkRelation relation) { Assert.notNull(relation, "LinkRelation must not be null!"); diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index 8723aa29..2c730b64 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -37,6 +37,7 @@ import org.springframework.hateoas.Links; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.hal.HalConfiguration.RenderSingleLinks; import org.springframework.hateoas.server.LinkRelationProvider; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonInclude; @@ -114,13 +115,13 @@ public class Jackson2HalModule extends SimpleModule { private final MessageSourceAccessor accessor; private final HalConfiguration halConfiguration; - public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor, - HalConfiguration halConfiguration) { + public HalLinkListSerializer(@Nullable CurieProvider curieProvider, EmbeddedMapper mapper, + @Nullable MessageSourceAccessor accessor, HalConfiguration halConfiguration) { this(null, curieProvider, mapper, accessor, halConfiguration); } - public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper, - MessageSourceAccessor accessor, HalConfiguration halConfiguration) { + public HalLinkListSerializer(@Nullable BeanProperty property, @Nullable CurieProvider curieProvider, + @Nullable EmbeddedMapper mapper, @Nullable MessageSourceAccessor accessor, HalConfiguration halConfiguration) { super(TypeFactory.defaultInstance().constructType(Links.class)); @@ -220,6 +221,7 @@ public class Jackson2HalModule extends SimpleModule { * @param relation must not be {@literal null} or empty. * @return */ + @Nullable private String getTitle(HalLinkRelation relation) { Assert.notNull(relation, "Local relation must not be null or empty!"); @@ -246,6 +248,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -255,6 +258,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -282,14 +286,15 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) */ @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } } /** - * Custom {@link JsonSerializer} to render {@link EntityModel}-Lists in HAL compatible JSON. Renders the - * list as a Map. + * Custom {@link JsonSerializer} to render {@link EntityModel}-Lists in HAL compatible JSON. Renders the list as a + * Map. * * @author Alexander Baetz * @author Oliver Gierke @@ -306,7 +311,7 @@ public class Jackson2HalModule extends SimpleModule { this(null, embeddedMapper); } - public HalResourcesSerializer(BeanProperty property, EmbeddedMapper embeddedMapper) { + public HalResourcesSerializer(@Nullable BeanProperty property, EmbeddedMapper embeddedMapper) { super(TypeFactory.defaultInstance().constructType(Collection.class)); @@ -344,11 +349,13 @@ public class Jackson2HalModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -364,6 +371,7 @@ public class Jackson2HalModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -446,6 +454,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -455,6 +464,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -522,6 +532,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -531,6 +542,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -588,7 +600,7 @@ public class Jackson2HalModule extends SimpleModule { this(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, vc), vc); } - private HalResourcesDeserializer(JavaType type, JavaType contentType) { + private HalResourcesDeserializer(JavaType type, @Nullable JavaType contentType) { super(type); this.contentType = contentType; @@ -599,6 +611,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -608,6 +621,7 @@ public class Jackson2HalModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -670,22 +684,22 @@ public class Jackson2HalModule extends SimpleModule { } /** - * Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider} and - * {@link MessageSourceAccessor}. Registers a prepared {@link HalResourcesSerializer} and + * Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider} + * and {@link MessageSourceAccessor}. Registers a prepared {@link HalResourcesSerializer} and * {@link HalLinkListSerializer} falling back to instantiation expecting a default constructor. * * @param provider must not be {@literal null}. * @param curieProvider can be {@literal null}. * @param messageSourceAccessor can be {@literal null}. */ - public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, + public HalHandlerInstantiator(LinkRelationProvider provider, @Nullable CurieProvider curieProvider, MessageSourceAccessor messageSourceAccessor, HalConfiguration halConfiguration) { this(provider, curieProvider, messageSourceAccessor, true, halConfiguration); } /** - * Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider} and - * {@link MessageSourceAccessor} and whether to enforce embedded collections. Registers a prepared + * Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider} + * and {@link MessageSourceAccessor} and whether to enforce embedded collections. Registers a prepared * {@link HalResourcesSerializer} and {@link HalLinkListSerializer} falling back to instantiation expecting a * default constructor. * @@ -694,13 +708,15 @@ public class Jackson2HalModule extends SimpleModule { * @param accessor can be {@literal null}. * @param enforceEmbeddedCollections */ - public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, - boolean enforceEmbeddedCollections, HalConfiguration halConfiguration) { + public HalHandlerInstantiator(@Nullable LinkRelationProvider provider, @Nullable CurieProvider curieProvider, + @Nullable MessageSourceAccessor accessor, boolean enforceEmbeddedCollections, + HalConfiguration halConfiguration) { this(provider, curieProvider, accessor, enforceEmbeddedCollections, null, halConfiguration); } - private HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, - boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate, HalConfiguration halConfiguration) { + private HalHandlerInstantiator(@Nullable LinkRelationProvider provider, @Nullable CurieProvider curieProvider, + @Nullable MessageSourceAccessor accessor, boolean enforceEmbeddedCollections, + @Nullable AutowireCapableBeanFactory delegate, HalConfiguration halConfiguration) { Assert.notNull(provider, "RelProvider must not be null!"); @@ -835,14 +851,15 @@ public class Jackson2HalModule extends SimpleModule { private boolean preferCollectionRels; /** - * Creates a new {@link EmbeddedMapper} for the given {@link LinkRelationProvider}, {@link CurieProvider} and flag whether to - * prefer collection relations. + * Creates a new {@link EmbeddedMapper} for the given {@link LinkRelationProvider}, {@link CurieProvider} and flag + * whether to prefer collection relations. * * @param relProvider must not be {@literal null}. * @param curieProvider can be {@literal null}. * @param preferCollectionRels */ - public EmbeddedMapper(LinkRelationProvider relProvider, CurieProvider curieProvider, boolean preferCollectionRels) { + public EmbeddedMapper(LinkRelationProvider relProvider, @Nullable CurieProvider curieProvider, + boolean preferCollectionRels) { Assert.notNull(relProvider, "RelProvider must not be null!"); @@ -888,7 +905,8 @@ public class Jackson2HalModule extends SimpleModule { private final Link link; private final String title; - public HalLink(Link link, String title) { + public HalLink(Link link, @Nullable String title) { + this.link = link; this.title = title; } @@ -899,6 +917,7 @@ public class Jackson2HalModule extends SimpleModule { } @JsonInclude(Include.NON_NULL) + @Nullable public String getTitle() { return title; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java index e541be02..faf19336 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; @@ -91,6 +92,7 @@ class HalFormsDeserializers { } @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -120,6 +122,7 @@ class HalFormsDeserializers { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -129,6 +132,7 @@ class HalFormsDeserializers { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java index 78146fca..ddf38a6c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java @@ -32,6 +32,7 @@ import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.mediatype.hal.HalLinkRelation; import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; import org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsModule.HalFormsLinksDeserializer; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -58,7 +59,7 @@ public class HalFormsDocument { @JsonUnwrapped // @JsonInclude(Include.NON_NULL) // - @Wither(AccessLevel.PRIVATE) // + @Wither(value = AccessLevel.PRIVATE, onMethod = @__({ @Nullable })) // private T resource; @JsonInclude(Include.NON_EMPTY) @JsonIgnore // @@ -71,6 +72,7 @@ public class HalFormsDocument { @JsonProperty("page") // @JsonInclude(Include.NON_NULL) // + @Wither(onMethod = @__({ @Nullable })) // private PagedModel.PageMetadata pageMetadata; @Singular // @@ -95,7 +97,7 @@ public class HalFormsDocument { * @param resource can be {@literal null}. * @return */ - public static HalFormsDocument forResource(T resource) { + public static HalFormsDocument forResource(@Nullable T resource) { return new HalFormsDocument().withResource(resource); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java index 5c7a9539..c5e1da35 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java @@ -32,6 +32,7 @@ import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.hal.HalLinkRelation; import org.springframework.hateoas.mediatype.hal.Jackson2HalModule; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanProperty; @@ -60,7 +61,7 @@ class HalFormsSerializers { private final BeanProperty property; - HalFormsResourceSerializer(BeanProperty property) { + HalFormsResourceSerializer(@Nullable BeanProperty property) { super(EntityModel.class, false); this.property = property; @@ -82,11 +83,13 @@ class HalFormsSerializers { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -97,6 +100,7 @@ class HalFormsSerializers { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer typeSerializer) { return null; } @@ -119,7 +123,7 @@ class HalFormsSerializers { private final BeanProperty property; private final Jackson2HalModule.EmbeddedMapper embeddedMapper; - HalFormsResourcesSerializer(BeanProperty property, Jackson2HalModule.EmbeddedMapper embeddedMapper) { + HalFormsResourcesSerializer(@Nullable BeanProperty property, Jackson2HalModule.EmbeddedMapper embeddedMapper) { super(CollectionModel.class, false); @@ -159,11 +163,13 @@ class HalFormsSerializers { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -174,6 +180,7 @@ class HalFormsSerializers { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer typeSerializer) { return null; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplate.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplate.java index c3315261..2b8a6062 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplate.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplate.java @@ -30,6 +30,7 @@ import java.util.List; import org.springframework.hateoas.mediatype.hal.forms.HalFormsDeserializers.MediaTypesDeserializer; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -115,6 +116,7 @@ public class HalFormsTemplate { this.contentTypes = mediaTypes; } + @Nullable String getMethod() { return this.httpMethod == null ? null : this.httpMethod.toString().toLowerCase(); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java index 361fee07..b1cfca1e 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java @@ -40,6 +40,7 @@ import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFo import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.hateoas.server.mvc.JacksonSerializers.MediaTypeDeserializer; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -127,6 +128,7 @@ class Jackson2HalFormsModule extends SimpleModule { * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return delegate.getContentDeserializer(); } @@ -148,7 +150,7 @@ class Jackson2HalFormsModule extends SimpleModule { private final Map, Object> serializers = new HashMap<>(); - public HalFormsHandlerInstantiator(LinkRelationProvider resolver, CurieProvider curieProvider, + public HalFormsHandlerInstantiator(LinkRelationProvider resolver, @Nullable CurieProvider curieProvider, MessageSourceAccessor accessor, boolean enforceEmbeddedCollections, HalFormsConfiguration halFormsConfiguration) { @@ -168,6 +170,7 @@ class Jackson2HalFormsModule extends SimpleModule { beanFactory.getBean(HalFormsConfiguration.class)); } + @Nullable private Object findInstance(Class type) { return this.serializers.get(type); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/package-info.java new file mode 100644 index 00000000..f55e42ed --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/package-info.java @@ -0,0 +1,7 @@ +/** + * HAL-FORMS extension media type. + */ +@NonNullApi +package org.springframework.hateoas.mediatype.hal.forms; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/hal/package-info.java index d7e2139c..34131164 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/package-info.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/package-info.java @@ -3,5 +3,7 @@ * * @see http://stateless.co/hal_specification.html */ +@NonNullApi package org.springframework.hateoas.mediatype.hal; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/package-info.java new file mode 100644 index 00000000..938c85fd --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/package-info.java @@ -0,0 +1,7 @@ +/** + * Spring container configuration support. + */ +@NonNullApi +package org.springframework.hateoas.mediatype; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/Jackson2UberModule.java b/src/main/java/org/springframework/hateoas/mediatype/uber/Jackson2UberModule.java index f95ed1a0..c033265f 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/Jackson2UberModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/Jackson2UberModule.java @@ -35,6 +35,7 @@ import org.springframework.hateoas.PagedModel.PageMetadata; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.JacksonHelper; import org.springframework.hateoas.mediatype.PropertyUtils; +import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.fasterxml.jackson.core.JsonGenerator; @@ -128,7 +129,7 @@ public class Jackson2UberModule extends SimpleModule { private static final long serialVersionUID = -572866287910993300L; private final BeanProperty property; - UberRepresentationModelSerializer(BeanProperty property) { + UberRepresentationModelSerializer(@Nullable BeanProperty property) { super(RepresentationModel.class, false); this.property = property; @@ -153,11 +154,13 @@ public class Jackson2UberModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -168,6 +171,7 @@ public class Jackson2UberModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -182,14 +186,13 @@ public class Jackson2UberModule extends SimpleModule { /** * Custom {@link JsonSerializer} to render {@link EntityModel} into {@literal UBER+JSON}. */ - static class UberEntityModelSerializer extends ContainerSerializer> - implements ContextualSerializer { + static class UberEntityModelSerializer extends ContainerSerializer> implements ContextualSerializer { private static final long serialVersionUID = -5538560800604582741L; private final BeanProperty property; - UberEntityModelSerializer(BeanProperty property) { + UberEntityModelSerializer(@Nullable BeanProperty property) { super(EntityModel.class, false); this.property = property; @@ -200,8 +203,7 @@ public class Jackson2UberModule extends SimpleModule { } @Override - public void serialize(EntityModel value, JsonGenerator gen, SerializerProvider provider) - throws IOException { + public void serialize(EntityModel value, JsonGenerator gen, SerializerProvider provider) throws IOException { UberDocument doc = new UberDocument().withUber(new Uber() // .withVersion("1.0") // @@ -213,11 +215,13 @@ public class Jackson2UberModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -228,6 +232,7 @@ public class Jackson2UberModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -249,7 +254,7 @@ public class Jackson2UberModule extends SimpleModule { private BeanProperty property; - UberCollectionModelSerializer(BeanProperty property) { + UberCollectionModelSerializer(@Nullable BeanProperty property) { super(CollectionModel.class, false); this.property = property; @@ -264,8 +269,7 @@ public class Jackson2UberModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) */ @Override - public void serialize(CollectionModel value, JsonGenerator gen, SerializerProvider provider) - throws IOException { + public void serialize(CollectionModel value, JsonGenerator gen, SerializerProvider provider) throws IOException { UberDocument doc = new UberDocument() // .withUber(new Uber() // @@ -278,11 +282,13 @@ public class Jackson2UberModule extends SimpleModule { } @Override + @Nullable public JavaType getContentType() { return null; } @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -293,6 +299,7 @@ public class Jackson2UberModule extends SimpleModule { } @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -307,14 +314,13 @@ public class Jackson2UberModule extends SimpleModule { /** * Custom {@link JsonSerializer} to render {@link PagedModel} into {@literal UBER+JSON}. */ - static class UberPagedModelSerializer extends ContainerSerializer> - implements ContextualSerializer { + static class UberPagedModelSerializer extends ContainerSerializer> implements ContextualSerializer { private static final long serialVersionUID = -7892297813593085984L; private BeanProperty property; - UberPagedModelSerializer(BeanProperty property) { + UberPagedModelSerializer(@Nullable BeanProperty property) { super(PagedModel.class, false); this.property = property; @@ -329,8 +335,7 @@ public class Jackson2UberModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) */ @Override - public void serialize(PagedModel value, JsonGenerator gen, SerializerProvider provider) - throws IOException { + public void serialize(PagedModel value, JsonGenerator gen, SerializerProvider provider) throws IOException { UberDocument doc = new UberDocument() // .withUber(new Uber() // @@ -347,6 +352,7 @@ public class Jackson2UberModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override + @Nullable public JavaType getContentType() { return null; } @@ -356,6 +362,7 @@ public class Jackson2UberModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override + @Nullable public JsonSerializer getContentSerializer() { return null; } @@ -374,6 +381,7 @@ public class Jackson2UberModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) */ @Override + @Nullable protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return null; } @@ -383,8 +391,7 @@ public class Jackson2UberModule extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty) */ @Override - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) - throws JsonMappingException { + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { return new UberPagedModelSerializer(property); } } @@ -475,6 +482,7 @@ public class Jackson2UberModule extends SimpleModule { * Accessor for deserializer use for deserializing content values. */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -567,6 +575,7 @@ public class Jackson2UberModule extends SimpleModule { * Accesor for deserializer use for deserializing content values. */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -630,6 +639,7 @@ public class Jackson2UberModule extends SimpleModule { * Accesor for deserializer use for deserializing content values. */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -696,6 +706,7 @@ public class Jackson2UberModule extends SimpleModule { * Accesor for deserializer use for deserializing content values. */ @Override + @Nullable public JsonDeserializer getContentDeserializer() { return null; } @@ -709,8 +720,7 @@ public class Jackson2UberModule extends SimpleModule { * @param contentType * @return */ - private static CollectionModel extractResources(UberDocument doc, JavaType rootType, - JavaType contentType) { + private static CollectionModel extractResources(UberDocument doc, JavaType rootType, JavaType contentType) { List content = new ArrayList<>(); @@ -785,15 +795,17 @@ public class Jackson2UberModule extends SimpleModule { } } - private static boolean isPrimitiveType(List data) { + private static boolean isPrimitiveType(@Nullable List data) { return data != null && data.size() == 1 && data.get(0).getName() == null; } + @Nullable private static PageMetadata extractPagingMetadata(UberDocument doc) { - return doc.getUber().getData().stream() - .filter(uberData -> uberData.getName() != null && uberData.getName().equals("page")).findFirst() - .map(Jackson2UberModule::convertUberDataToPageMetaData).orElse(null); + return doc.getUber().getData().stream() // + .filter(uberData -> uberData.getName() != null && uberData.getName().equals("page")) // + .findFirst().map(Jackson2UberModule::convertUberDataToPageMetaData) // + .orElse(null); } @NotNull diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/Uber.java b/src/main/java/org/springframework/hateoas/mediatype/uber/Uber.java index 1fb8d303..aba282a4 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/Uber.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/Uber.java @@ -22,6 +22,7 @@ import lombok.experimental.Wither; import java.util.List; import org.springframework.hateoas.Links; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -45,8 +46,8 @@ class Uber { private UberError error; @JsonCreator - Uber(@JsonProperty("version") String version, @JsonProperty("data") List data, - @JsonProperty("error") UberError error) { + Uber(@JsonProperty("version") String version, @JsonProperty("data") @Nullable List data, + @JsonProperty("error") @Nullable UberError error) { this.version = version; this.data = data; @@ -65,6 +66,10 @@ class Uber { @JsonIgnore Links getLinks() { + if (data == null) { + return Links.NONE; + } + return data.stream() // .flatMap(uberData -> uberData.getLinks().stream()) // .collect(Links.collector()); diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAction.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAction.java index f5c03c38..8d769008 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAction.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAction.java @@ -20,6 +20,7 @@ import java.util.Arrays; import org.springframework.hateoas.mediatype.uber.Jackson2UberModule.UberActionDeserializer; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -100,6 +101,7 @@ enum UberAction { * @param method to map * @return action, or null for GET */ + @Nullable static UberAction forRequestMethod(HttpMethod method) { return HttpMethod.GET == method ? null : fromMethod(method); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java index 208dccc4..b2785b7c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberAffordanceModel.java @@ -32,6 +32,7 @@ import org.springframework.hateoas.QueryParameter; import org.springframework.hateoas.mediatype.PropertyUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; /** * {@link AffordanceModel} for {@literal UBER+JSON}. @@ -89,6 +90,7 @@ class UberAffordanceModel extends AffordanceModel { } } + @Nullable UberAction getAction() { return UberAction.forRequestMethod(getHttpMethod()); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberData.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberData.java index 9272ad50..cd7a88af 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberData.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberData.java @@ -41,6 +41,7 @@ import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.PropertyUtils; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonCreator; @@ -76,12 +77,13 @@ class UberData { private List data; @JsonCreator - UberData(@JsonProperty("id") String id, @JsonProperty("name") String name, @JsonProperty("label") String label, - @JsonProperty("rel") List rel, @JsonProperty("url") String url, - @JsonProperty("action") UberAction action, @JsonProperty("transclude") boolean transclude, - @JsonProperty("model") String model, @JsonProperty("sending") List sending, - @JsonProperty("accepting") List accepting, @JsonProperty("value") Object value, - @JsonProperty("data") List data) { + UberData(@JsonProperty("id") @Nullable String id, @JsonProperty("name") @Nullable String name, + @JsonProperty("label") @Nullable String label, @JsonProperty("rel") @Nullable List rel, + @JsonProperty("url") @Nullable String url, @JsonProperty("action") UberAction action, + @JsonProperty("transclude") boolean transclude, @JsonProperty("model") @Nullable String model, + @JsonProperty("sending") @Nullable List sending, + @JsonProperty("accepting") @Nullable List accepting, @JsonProperty("value") @Nullable Object value, + @JsonProperty("data") @Nullable List data) { this.id = id; this.name = name; @@ -104,6 +106,7 @@ class UberData { /** * Don't render if it's {@link UberAction#READ}. */ + @Nullable public UberAction getAction() { return action == UberAction.READ ? null : action; } @@ -111,6 +114,7 @@ class UberData { /* * Use a {@link Boolean} to support returning {@literal null}, and if it is {@literal null}, don't render. */ + @Nullable public Boolean isTemplated() { return Optional.ofNullable(this.url) // @@ -121,8 +125,9 @@ class UberData { /* * Use a {@link Boolean} to support returning {@literal null}, and if it is {@literal null}, don't render. */ + @Nullable public Boolean isTransclude() { - return this.transclude ? this.transclude : null; + return this.transclude ? true : null; } /** @@ -131,6 +136,10 @@ class UberData { @JsonIgnore public List getLinks() { + if (this.url == null) { + return Links.NONE.toList(); + } + return Optional.ofNullable(this.rel) // .map(rels -> rels.stream() // .map(rel -> new Link(this.url, rel)) // @@ -146,8 +155,8 @@ class UberData { /** * Set of all Spring HATEOAS resource types. */ - private static final HashSet> RESOURCE_TYPES = new HashSet<>(Arrays.asList(RepresentationModel.class, - EntityModel.class, CollectionModel.class, PagedModel.class)); + private static final HashSet> RESOURCE_TYPES = new HashSet<>( + Arrays.asList(RepresentationModel.class, EntityModel.class, CollectionModel.class, PagedModel.class)); /** * Convert a {@link RepresentationModel} into a list of {@link UberData}s, containing links and content. @@ -180,8 +189,7 @@ class UberData { } /** - * Convert {@link CollectionModel} into a list of {@link UberData}, with each item nested in a - * sub-UberData. + * Convert {@link CollectionModel} into a list of {@link UberData}, with each item nested in a sub-UberData. * * @param resources * @return @@ -255,9 +263,9 @@ class UberData { * @param content * @return */ - private static Optional extractContent(Object content) { + private static Optional extractContent(@Nullable Object content) { - return Optional.of(content) // + return Optional.ofNullable(content) // .filter(it -> !RESOURCE_TYPES.contains(content.getClass())) // .map(it -> new UberData() // .withName(StringUtils.uncapitalize(it.getClass().getSimpleName())) // diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberDocument.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberDocument.java index 3ecd7245..6c89571e 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberDocument.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberDocument.java @@ -22,6 +22,8 @@ import lombok.experimental.Wither; import java.util.List; +import org.springframework.lang.Nullable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -39,12 +41,12 @@ class UberDocument { private Uber uber; @JsonCreator - UberDocument(@JsonProperty("version") String version, @JsonProperty("data") List data, - @JsonProperty("error") UberError error) { + UberDocument(@JsonProperty("version") String version, @JsonProperty("data") @Nullable List data, + @JsonProperty("error") @Nullable UberError error) { this.uber = new Uber(version, data, error); } UberDocument() { this("1.0", null, null); } -} \ No newline at end of file +} diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberError.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberError.java index 29d7b5b1..de161e5a 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberError.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberError.java @@ -21,6 +21,8 @@ import lombok.experimental.Wither; import java.util.List; +import org.springframework.lang.Nullable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -37,7 +39,7 @@ class UberError { private List data; @JsonCreator - UberError(@JsonProperty("data") List data) { + UberError(@JsonProperty("data") @Nullable List data) { this.data = data; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/uber/package-info.java new file mode 100644 index 00000000..cb2da5d8 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/package-info.java @@ -0,0 +1,7 @@ +/** + * UBER media type objects. + */ +@NonNullApi +package org.springframework.hateoas.mediatype.uber; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mediatype/vnderrors/package-info.java b/src/main/java/org/springframework/hateoas/mediatype/vnderrors/package-info.java new file mode 100644 index 00000000..43c8f5a1 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/vnderrors/package-info.java @@ -0,0 +1,7 @@ +/** + * Vnd.Error media type. + */ +@NonNullApi +package org.springframework.hateoas.mediatype.vnderrors; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/package-info.java b/src/main/java/org/springframework/hateoas/package-info.java index a1c9fc0b..ae0fa1eb 100644 --- a/src/main/java/org/springframework/hateoas/package-info.java +++ b/src/main/java/org/springframework/hateoas/package-info.java @@ -21,4 +21,7 @@ * @author Jens Schauder * @author Greg Turnquist */ +@NonNullApi package org.springframework.hateoas; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java b/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java index 9644b52c..9748dd71 100644 --- a/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java +++ b/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java @@ -20,6 +20,7 @@ import java.lang.reflect.AnnotatedElement; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -47,7 +48,7 @@ public class AnnotationAttribute { * @param annotationType must not be {@literal null}. * @param attributeName can be {@literal null}, defaults to {@code value}. */ - public AnnotationAttribute(Class annotationType, String attributeName) { + public AnnotationAttribute(Class annotationType, @Nullable String attributeName) { Assert.notNull(annotationType, "AnnotationType must not be null!"); @@ -70,6 +71,7 @@ public class AnnotationAttribute { * @param parameter must not be {@literal null}. * @return */ + @Nullable public String getValueFrom(MethodParameter parameter) { Assert.notNull(parameter, "MethodParameter must not be null!"); @@ -83,6 +85,7 @@ public class AnnotationAttribute { * @param annotatedElement must not be {@literal null}. * @return */ + @Nullable public String getValueFrom(AnnotatedElement annotatedElement) { Assert.notNull(annotatedElement, "Annotated element must not be null!"); @@ -96,6 +99,7 @@ public class AnnotationAttribute { * @param annotation must not be {@literal null}. * @return */ + @Nullable public String getValueFrom(Annotation annotation) { Assert.notNull(annotation, "Annotation must not be null!"); diff --git a/src/main/java/org/springframework/hateoas/server/core/AnnotationLinkRelationProvider.java b/src/main/java/org/springframework/hateoas/server/core/AnnotationLinkRelationProvider.java index 1e183643..1b8cb8f6 100644 --- a/src/main/java/org/springframework/hateoas/server/core/AnnotationLinkRelationProvider.java +++ b/src/main/java/org/springframework/hateoas/server/core/AnnotationLinkRelationProvider.java @@ -22,6 +22,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.server.LinkRelationProvider; +import org.springframework.lang.Nullable; /** * @author Oliver Gierke @@ -37,6 +38,7 @@ public class AnnotationLinkRelationProvider implements LinkRelationProvider, Ord * @see org.springframework.hateoas.server.LinkRelationProvider#getCollectionResourceRelFor(java.lang.Class) */ @Override + @Nullable public LinkRelation getCollectionResourceRelFor(Class type) { Relation annotation = lookupAnnotation(type); @@ -53,6 +55,7 @@ public class AnnotationLinkRelationProvider implements LinkRelationProvider, Ord * @see org.springframework.hateoas.server.LinkRelationProvider#getItemResourceRelFor(java.lang.Class) */ @Override + @Nullable public LinkRelation getItemResourceRelFor(Class type) { Relation annotation = lookupAnnotation(type); @@ -82,6 +85,7 @@ public class AnnotationLinkRelationProvider implements LinkRelationProvider, Ord return lookupAnnotation(delimiter) != null; } + @Nullable private Relation lookupAnnotation(Class type) { return annotationCache.computeIfAbsent(type, key -> AnnotationUtils.getAnnotation(key, Relation.class)); } diff --git a/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java index 3243c46e..42ea4f31 100644 --- a/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java @@ -22,10 +22,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMethod; @@ -59,7 +61,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { * @param annotation must not be {@literal null}. * @param mappingAttributeName if {@literal null}, it defaults to {@code value}. */ - public AnnotationMappingDiscoverer(Class annotation, String mappingAttributeName) { + public AnnotationMappingDiscoverer(Class annotation, @Nullable String mappingAttributeName) { Assert.notNull(annotation, "Annotation must not be null!"); @@ -72,6 +74,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class) */ @Override + @Nullable public String getMapping(Class type) { Assert.notNull(type, "Type must not be null!"); @@ -86,6 +89,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.reflect.Method) */ @Override + @Nullable public String getMapping(Method method) { Assert.notNull(method, "Method must not be null!"); @@ -97,6 +101,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class, java.lang.reflect.Method) */ @Override + @Nullable public String getMapping(Class type, Method method) { Assert.notNull(type, "Type must not be null!"); @@ -105,7 +110,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { String[] mapping = getMappingFrom(findMergedAnnotation(method, annotationType)); String typeMapping = getMapping(type); - if (mapping == null || mapping.length == 0) { + if (mapping.length == 0) { return typeMapping; } @@ -131,6 +136,10 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { RequestMethod[] requestMethods = (RequestMethod[]) value; + if (requestMethods == null) { + return Collections.emptyList(); + } + List requestMethodNames = new ArrayList<>(); for (RequestMethod requestMethod : requestMethods) { @@ -140,7 +149,7 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { return requestMethodNames; } - private String[] getMappingFrom(Annotation annotation) { + private String[] getMappingFrom(@Nullable Annotation annotation) { if (annotation == null) { return new String[0]; diff --git a/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java index 2b54a75e..6aab5168 100644 --- a/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Map; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.StringUtils; @@ -84,7 +85,7 @@ public class CachingMappingDiscoverer implements MappingDiscoverer { return METHODS.computeIfAbsent(key(type, method), __ -> delegate.getRequestMethod(type, method)); } - private static String key(Class type, Method method) { + private static String key(Class type, @Nullable Method method) { StringBuilder builder = new StringBuilder(type.getName()); diff --git a/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java b/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java index 4c77be59..14c81139 100644 --- a/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java +++ b/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java @@ -27,6 +27,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.cglib.proxy.Enhancer; +import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisStd; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -84,6 +85,7 @@ public class DummyInvocationUtils { * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) */ @Override + @Nullable public Object invoke(org.aopalliance.intercept.MethodInvocation invocation) { Method method = invocation.getMethod(); diff --git a/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrapper.java b/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrapper.java index ac2682b5..67a6a745 100644 --- a/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrapper.java +++ b/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrapper.java @@ -17,8 +17,9 @@ package org.springframework.hateoas.server.core; import java.util.Optional; -import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.LinkRelation; +import org.springframework.lang.Nullable; /** * A wrapper to handle values to be embedded into a {@link EntityModel}. @@ -66,5 +67,6 @@ public interface EmbeddedWrapper { * * @return */ + @Nullable Class getRelTargetType(); } diff --git a/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrappers.java b/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrappers.java index 4045202d..25513cec 100644 --- a/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrappers.java +++ b/src/main/java/org/springframework/hateoas/server/core/EmbeddedWrappers.java @@ -22,6 +22,7 @@ import java.util.Optional; import org.springframework.aop.support.AopUtils; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.EntityModel; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,7 +49,8 @@ public class EmbeddedWrappers { * @param source * @return */ - public EmbeddedWrapper wrap(Object source) { + @Nullable + public EmbeddedWrapper wrap(@Nullable Object source) { return wrap(source, AbstractEmbeddedWrapper.NO_REL); } @@ -70,7 +72,8 @@ public class EmbeddedWrappers { * @return */ @SuppressWarnings("unchecked") - public EmbeddedWrapper wrap(Object source, LinkRelation rel) { + @Nullable + public EmbeddedWrapper wrap(@Nullable Object source, LinkRelation rel) { if (source == null) { return null; @@ -134,16 +137,17 @@ public class EmbeddedWrappers { */ @Override @SuppressWarnings("unchecked") + @Nullable public Class getRelTargetType() { Object peek = peek(); + peek = peek instanceof EntityModel ? ((EntityModel) peek).getContent() : peek; + if (peek == null) { return null; } - peek = peek instanceof EntityModel ? ((EntityModel) peek).getContent() : peek; - return AopUtils.getTargetClass(peek); } @@ -152,6 +156,7 @@ public class EmbeddedWrappers { * * @return */ + @Nullable protected abstract Object peek(); } @@ -245,6 +250,7 @@ public class EmbeddedWrappers { * @see org.springframework.hateoas.core.EmbeddedWrappers.AbstractEmbeddedWrapper#peek() */ @Override + @Nullable protected Object peek() { return value.isEmpty() ? null : value.iterator().next(); } diff --git a/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java b/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java index 49491912..b48971d4 100644 --- a/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java +++ b/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java @@ -17,6 +17,7 @@ package org.springframework.hateoas.server.core; import lombok.experimental.UtilityClass; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.util.UriUtils; @@ -39,7 +40,7 @@ class EncodingUtils { * @param source must not be {@literal null}. * @return */ - public static String encodePath(Object source) { + public static String encodePath(@Nullable Object source) { Assert.notNull(source, "Path value must not be null!"); @@ -56,7 +57,7 @@ class EncodingUtils { * @param source must not be {@literal null}. * @return */ - public static String encodeParameter(Object source) { + public static String encodeParameter(@Nullable Object source) { Assert.notNull(source, "Request parameter value must not be null!"); diff --git a/src/main/java/org/springframework/hateoas/server/core/HeaderLinksResponseEntity.java b/src/main/java/org/springframework/hateoas/server/core/HeaderLinksResponseEntity.java index f895d2bd..f95aace8 100644 --- a/src/main/java/org/springframework/hateoas/server/core/HeaderLinksResponseEntity.java +++ b/src/main/java/org/springframework/hateoas/server/core/HeaderLinksResponseEntity.java @@ -44,7 +44,9 @@ public class HeaderLinksResponseEntity> extends private HeaderLinksResponseEntity(ResponseEntity entity) { super(entity.getBody(), getHeadersWithLinks(entity), entity.getStatusCode()); - entity.getBody().removeLinks(); + if (entity.getBody() != null) { + entity.getBody().removeLinks(); + } } /** @@ -98,7 +100,7 @@ public class HeaderLinksResponseEntity> extends */ private static > HttpHeaders getHeadersWithLinks(ResponseEntity entity) { - Links links = entity.getBody().getLinks(); + Links links = entity.getBody() != null ? entity.getBody().getLinks() : Links.NONE; HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(entity.getHeaders()); diff --git a/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java index d9fb7abe..bf4260b2 100644 --- a/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Collection; import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; /** * Strategy interface to discover a URI mapping and related {@link org.springframework.hateoas.Affordance}s for either a @@ -35,6 +36,7 @@ public interface MappingDiscoverer { * @param type must not be {@literal null}. * @return the type-level mapping or {@literal null} in case none is present. */ + @Nullable String getMapping(Class type); /** @@ -53,6 +55,7 @@ public interface MappingDiscoverer { * @param method must not be {@literal null}. * @return the method mapping including the type-level one or {@literal null} if neither of them present. */ + @Nullable String getMapping(Class type, Method method); /** diff --git a/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java b/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java index 75b2622b..37c66746 100644 --- a/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java +++ b/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java @@ -27,6 +27,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -58,7 +59,7 @@ public class MethodParameters { * @param method must not be {@literal null}. * @param namingAnnotation can be {@literal null}. */ - public MethodParameters(Method method, AnnotationAttribute namingAnnotation) { + public MethodParameters(Method method, @Nullable AnnotationAttribute namingAnnotation) { Assert.notNull(method, "Method must not be null!"); @@ -145,7 +146,7 @@ public class MethodParameters { * @param parameterIndex * @param attribute can be {@literal null} */ - public AnnotationNamingMethodParameter(Method method, int parameterIndex, AnnotationAttribute attribute) { + public AnnotationNamingMethodParameter(Method method, int parameterIndex, @Nullable AnnotationAttribute attribute) { super(method, parameterIndex); this.attribute = attribute; diff --git a/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java b/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java index 78bed68c..dd5ee2e2 100644 --- a/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java @@ -17,6 +17,7 @@ package org.springframework.hateoas.server.core; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.springframework.core.ResolvableType; @@ -69,6 +70,7 @@ public class SpringAffordanceBuilder { List queryMethodParameters = invocationMethodParameters.getParametersWith(RequestParam.class) .stream() // .map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class)) // + .filter(Objects::nonNull) // .map(requestParam -> new QueryParameter(requestParam.name(), requestParam.value(), requestParam.required())) // .collect(Collectors.toList()); diff --git a/src/main/java/org/springframework/hateoas/server/core/TypeReferences.java b/src/main/java/org/springframework/hateoas/server/core/TypeReferences.java index a1c5276a..8d5e042e 100644 --- a/src/main/java/org/springframework/hateoas/server/core/TypeReferences.java +++ b/src/main/java/org/springframework/hateoas/server/core/TypeReferences.java @@ -22,6 +22,7 @@ import java.util.HashMap; import org.springframework.core.GenericTypeResolver; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -186,6 +187,7 @@ public class TypeReferences { * @see java.lang.reflect.ParameterizedType#getOwnerType() */ @Override + @Nullable public Type getOwnerType() { return null; } diff --git a/src/main/java/org/springframework/hateoas/server/core/UriTemplateFactory.java b/src/main/java/org/springframework/hateoas/server/core/UriTemplateFactory.java index e7db344e..b483b556 100644 --- a/src/main/java/org/springframework/hateoas/server/core/UriTemplateFactory.java +++ b/src/main/java/org/springframework/hateoas/server/core/UriTemplateFactory.java @@ -17,6 +17,7 @@ package org.springframework.hateoas.server.core; import java.util.Map; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.web.util.UriTemplate; @@ -37,7 +38,7 @@ public class UriTemplateFactory { * @param mapping must not be {@literal null} or empty. * @return */ - public static UriTemplate templateFor(String mapping) { + public static UriTemplate templateFor(@Nullable String mapping) { Assert.hasText(mapping, "Mapping must not be null or empty!"); diff --git a/src/main/java/org/springframework/hateoas/server/core/WebHandler.java b/src/main/java/org/springframework/hateoas/server/core/WebHandler.java index a4e670ab..ac32a9f4 100644 --- a/src/main/java/org/springframework/hateoas/server/core/WebHandler.java +++ b/src/main/java/org/springframework/hateoas/server/core/WebHandler.java @@ -42,6 +42,7 @@ import org.springframework.hateoas.Affordance; import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.server.LinkBuilder; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.MultiValueMap; @@ -80,7 +81,7 @@ public class WebHandler { public static Function, T> linkTo( Object invocationValue, LinkBuilderCreator creator, - BiFunction additionalUriHandler) { + @Nullable BiFunction additionalUriHandler) { Assert.isInstanceOf(LastInvocationAware.class, invocationValue); @@ -184,17 +185,23 @@ public class WebHandler { } else if (value instanceof Collection) { for (Object element : (Collection) value) { - builder.queryParam(key, encodeParameter(element)); + if (key != null) { + builder.queryParam(key, encodeParameter(element)); + } } } else if (SKIP_VALUE.equals(value)) { if (parameter.isRequired()) { - builder.queryParam(key, String.format("{%s}", parameter.getVariableName())); + if (key != null) { + builder.queryParam(key, String.format("{%s}", parameter.getVariableName())); + } } } else { - builder.queryParam(key, encodeParameter(parameter.asString())); + if (key != null) { + builder.queryParam(key, encodeParameter(parameter.asString())); + } } } @@ -233,7 +240,7 @@ public class WebHandler { return false; } - return annotation.required() // + return annotation != null && annotation.required() // && annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE); } }; @@ -244,6 +251,7 @@ public class WebHandler { * @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#verifyParameterValue(org.springframework.core.MethodParameter, java.lang.Object) */ @Override + @Nullable protected Object verifyParameterValue(MethodParameter parameter, Object value) { RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class); @@ -254,7 +262,7 @@ public class WebHandler { return value; } - if (!annotation.required() || parameter.isOptional()) { + if (!(annotation != null && annotation.required()) || parameter.isOptional()) { return SKIP_VALUE; } @@ -325,6 +333,7 @@ public class WebHandler { * @param value could be {@literal null}. * @return the verified value. */ + @Nullable protected Object verifyParameterValue(MethodParameter parameter, Object value) { return value; } @@ -377,6 +386,7 @@ public class WebHandler { * * @return */ + @Nullable public String getVariableName() { if (attribute == null) { @@ -384,7 +394,7 @@ public class WebHandler { } Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType()); - String annotationAttributeValue = attribute.getValueFrom(annotation); + String annotationAttributeValue = annotation != null ? attribute.getValueFrom(annotation) : ""; return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName(); } @@ -403,6 +413,7 @@ public class WebHandler { * * @return */ + @Nullable public String asString() { return value == null // diff --git a/src/main/java/org/springframework/hateoas/server/core/package-info.java b/src/main/java/org/springframework/hateoas/server/core/package-info.java index 2bcd529d..80e70acb 100644 --- a/src/main/java/org/springframework/hateoas/server/core/package-info.java +++ b/src/main/java/org/springframework/hateoas/server/core/package-info.java @@ -1,5 +1,7 @@ /** * Implementations of core API interfaces. */ +@NonNullApi package org.springframework.hateoas.server.core; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorHandlerMethodReturnValueHandler.java b/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorHandlerMethodReturnValueHandler.java index d43e96cc..d7e4da32 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorHandlerMethodReturnValueHandler.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorHandlerMethodReturnValueHandler.java @@ -52,7 +52,9 @@ public class RepresentationModelProcessorHandlerMethodReturnValueHandler impleme static final Field CONTENT_FIELD = ReflectionUtils.findField(CollectionModel.class, "content"); static { - ReflectionUtils.makeAccessible(CONTENT_FIELD); + if (CONTENT_FIELD != null) { + ReflectionUtils.makeAccessible(CONTENT_FIELD); + } } private final @NonNull HandlerMethodReturnValueHandler delegate; diff --git a/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvoker.java b/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvoker.java index 08186b60..6513aa93 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvoker.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvoker.java @@ -28,6 +28,7 @@ import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.hateoas.server.core.EmbeddedWrapper; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -61,12 +62,14 @@ public class RepresentationModelProcessorInvoker { ResolvableType processorType = ResolvableType.forClass(RepresentationModelProcessor.class, processor.getClass()); Class rawType = processorType.getGeneric(0).resolve(); - if (EntityModel.class.isAssignableFrom(rawType)) { - this.processors.add(new ResourceProcessorWrapper(processor)); - } else if (CollectionModel.class.isAssignableFrom(rawType)) { - this.processors.add(new ResourcesProcessorWrapper(processor)); - } else { - this.processors.add(new DefaultProcessorWrapper(processor)); + if (rawType != null) { + if (EntityModel.class.isAssignableFrom(rawType)) { + this.processors.add(new ResourceProcessorWrapper(processor)); + } else if (CollectionModel.class.isAssignableFrom(rawType)) { + this.processors.add(new ResourcesProcessorWrapper(processor)); + } else { + this.processors.add(new DefaultProcessorWrapper(processor)); + } } } @@ -104,8 +107,8 @@ public class RepresentationModelProcessorInvoker { if (RepresentationModelProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(referenceType)) { CollectionModel resources = (CollectionModel) value; - ResolvableType elementTargetType = ResolvableType - .forClass(CollectionModel.class, referenceType.getRawClass()).getGeneric(0); + ResolvableType elementTargetType = ResolvableType.forClass(CollectionModel.class, referenceType.getRawClass()) + .getGeneric(0); List result = new ArrayList<>(resources.getContent().size()); for (Object element : resources) { @@ -119,8 +122,10 @@ public class RepresentationModelProcessorInvoker { result.add(invokeProcessorsFor(element, elementTargetType)); } - ReflectionUtils.setField(RepresentationModelProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD, resources, - result); + if (RepresentationModelProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD != null) { + ReflectionUtils.setField(RepresentationModelProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD, resources, + result); + } } return (T) invokeProcessorsFor(Object.class.cast(value), referenceType); @@ -147,11 +152,18 @@ public class RepresentationModelProcessorInvoker { return currentValue; } - private static boolean isRawTypeAssignable(ResolvableType left, Class right) { + private static boolean isRawTypeAssignable(@Nullable ResolvableType left, @Nullable Class right) { + + Assert.notNull(right, "right cannot be null!"); + return getRawType(left).isAssignableFrom(right); } - private static Class getRawType(ResolvableType type) { + private static Class getRawType(@Nullable ResolvableType type) { + + if (type == null) { + return Object.class; + } Class rawType = type.getRawClass(); return rawType == null ? Object.class : rawType; @@ -247,8 +259,8 @@ public class RepresentationModelProcessorInvoker { } /** - * {@link ProcessorWrapper} to deal with {@link RepresentationModelProcessor}s for {@link EntityModel}s. - * Will fall back to peeking into the {@link EntityModel}'s content for type resolution. + * {@link ProcessorWrapper} to deal with {@link RepresentationModelProcessor}s for {@link EntityModel}s. Will fall + * back to peeking into the {@link EntityModel}'s content for type resolution. * * @author Oliver Gierke */ @@ -278,15 +290,14 @@ public class RepresentationModelProcessorInvoker { } /** - * Returns whether the given {@link EntityModel} matches the given target {@link ResolvableType}. We - * inspect the {@link EntityModel}'s value to determine the match. + * Returns whether the given {@link EntityModel} matches the given target {@link ResolvableType}. We inspect the + * {@link EntityModel}'s value to determine the match. * * @param resource * @param target must not be {@literal null}. - * @return whether the given {@link EntityModel} can be assigned to the given target - * {@link ResolvableType} + * @return whether the given {@link EntityModel} can be assigned to the given target {@link ResolvableType} */ - private static boolean isValueTypeMatch(EntityModel resource, ResolvableType target) { + private static boolean isValueTypeMatch(@Nullable EntityModel resource, @Nullable ResolvableType target) { if (resource == null || !isRawTypeAssignable(target, resource.getClass())) { return false; @@ -302,7 +313,12 @@ public class RepresentationModelProcessorInvoker { return type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass())); } - private static ResolvableType findGenericType(ResolvableType source, Class type) { + @Nullable + private static ResolvableType findGenericType(@Nullable ResolvableType source, Class type) { + + if (source == null) { + return null; + } Class rawType = getRawType(source); @@ -319,8 +335,8 @@ public class RepresentationModelProcessorInvoker { } /** - * {@link ProcessorWrapper} for {@link RepresentationModelProcessor}s targeting {@link CollectionModel}. - * Will peek into the content of the {@link CollectionModel} for type matching decisions if needed. + * {@link ProcessorWrapper} for {@link RepresentationModelProcessor}s targeting {@link CollectionModel}. Will peek + * into the content of the {@link CollectionModel} for type matching decisions if needed. * * @author Oliver Gierke */ @@ -350,15 +366,14 @@ public class RepresentationModelProcessorInvoker { } /** - * Returns whether the given {@link CollectionModel} instance matches the given - * {@link ResolvableType}. We predict this by inspecting the first element of the content of the - * {@link CollectionModel}. + * Returns whether the given {@link CollectionModel} instance matches the given {@link ResolvableType}. We predict + * this by inspecting the first element of the content of the {@link CollectionModel}. * * @param resources the {@link CollectionModel} to inspect. * @param target that target {@link ResolvableType}. * @return */ - static boolean isValueTypeMatch(CollectionModel resources, ResolvableType target) { + static boolean isValueTypeMatch(@Nullable CollectionModel resources, ResolvableType target) { if (resources == null) { return false; @@ -372,8 +387,7 @@ public class RepresentationModelProcessorInvoker { ResolvableType superType = null; - for (Class resourcesType : Arrays.> asList(resources.getClass(), - CollectionModel.class)) { + for (Class resourcesType : Arrays.> asList(resources.getClass(), CollectionModel.class)) { superType = getSuperType(target, resourcesType); @@ -407,18 +421,18 @@ public class RepresentationModelProcessorInvoker { */ private static ResolvableType getSuperType(ResolvableType source, Class superType) { - if (source.getRawClass().equals(superType)) { + if (source.getRawClass() != null && source.getRawClass().equals(superType)) { return source; } ResolvableType candidate = source.getSuperType(); - if (superType.isAssignableFrom(candidate.getRawClass())) { + if (candidate.getRawClass() != null && superType.isAssignableFrom(candidate.getRawClass())) { return candidate; } for (ResolvableType interfaces : source.getInterfaces()) { - if (superType.isAssignableFrom(interfaces.getRawClass())) { + if (interfaces.getRawClass() != null && superType.isAssignableFrom(interfaces.getRawClass())) { return interfaces; } } diff --git a/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvokingHandlerAdapter.java b/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvokingHandlerAdapter.java index 7d993f1f..b3cb7c87 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvokingHandlerAdapter.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/RepresentationModelProcessorInvokingHandlerAdapter.java @@ -18,21 +18,19 @@ package org.springframework.hateoas.server.mvc; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import org.springframework.hateoas.server.RepresentationModelProcessor; -import org.springframework.util.ReflectionUtils; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; /** * Special {@link RequestMappingHandlerAdapter} that tweaks the {@link HandlerMethodReturnValueHandlerComposite} to be - * proxied by a {@link RepresentationModelProcessorHandlerMethodReturnValueHandler} which will invoke the {@link RepresentationModelProcessor} - * s found in the application context and eventually delegate to the originally configured - * {@link HandlerMethodReturnValueHandler}. + * proxied by a {@link RepresentationModelProcessorHandlerMethodReturnValueHandler} which will invoke the + * {@link RepresentationModelProcessor} s found in the application context and eventually delegate to the originally + * configured {@link HandlerMethodReturnValueHandler}. *

* This is a separate component as it might make sense to deploy it in a standalone SpringMVC application to enable post * processing. It would actually make most sense in Spring HATEOAS project. @@ -45,9 +43,6 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl @RequiredArgsConstructor public class RepresentationModelProcessorInvokingHandlerAdapter extends RequestMappingHandlerAdapter { - private static final Method RETURN_VALUE_HANDLER_METHOD = ReflectionUtils - .findMethod(RepresentationModelProcessorInvokingHandlerAdapter.class, "getReturnValueHandlers"); - private @NonNull final RepresentationModelProcessorInvoker invoker; /* @@ -79,7 +74,7 @@ public class RepresentationModelProcessorInvokingHandlerAdapter extends RequestM @SuppressWarnings("unchecked") private HandlerMethodReturnValueHandlerComposite getReturnValueHandlersComposite() { - Object handlers = ReflectionUtils.invokeMethod(RETURN_VALUE_HANDLER_METHOD, this); + Object handlers = this.getReturnValueHandlers(); if (handlers instanceof HandlerMethodReturnValueHandlerComposite) { return (HandlerMethodReturnValueHandlerComposite) handlers; diff --git a/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java b/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java index b777446a..07373b8e 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java @@ -17,8 +17,7 @@ package org.springframework.hateoas.server.mvc; import java.net.URI; -import javax.servlet.http.HttpServletRequest; - +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -39,9 +38,8 @@ class UriComponentsBuilderFactory { /** * Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping with scheme tweaked in case the - * request contains an {@code X-Forwarded-Ssl} header, which is not (yet) supported by the underlying - * {@link UriComponentsBuilder}. If no {@link RequestContextHolder} exists (you're outside a Spring Web call), fall - * back to relative URIs. + * request contains an {@code X-Forwarded-Ssl} header. If no {@link RequestContextHolder} exists (you're outside a + * Spring Web call), fall back to relative URIs. * * @return */ @@ -55,22 +53,7 @@ class UriComponentsBuilderFactory { return baseUri != null // ? UriComponentsBuilder.fromUri(baseUri) // - : cacheBaseUri(ServletUriComponentsBuilder.fromServletMapping(getCurrentRequest())); - } - - /** - * Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110 gets fixed. - * - * @return - */ - private static HttpServletRequest getCurrentRequest() { - - RequestAttributes requestAttributes = getRequestAttributes(); - HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); - - Assert.state(servletRequest != null, "Could not find current HttpServletRequest"); - - return servletRequest; + : cacheBaseUri(ServletUriComponentsBuilder.fromCurrentServletMapping()); } private static RequestAttributes getRequestAttributes() { @@ -92,6 +75,7 @@ class UriComponentsBuilderFactory { return builder; } + @Nullable private static URI getCachedBaseUri() { return (URI) getRequestAttributes().getAttribute(CACHE_KEY, RequestAttributes.SCOPE_REQUEST); } diff --git a/src/main/java/org/springframework/hateoas/server/mvc/package-info.java b/src/main/java/org/springframework/hateoas/server/mvc/package-info.java index 91f79e60..3d35a742 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/package-info.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/package-info.java @@ -2,5 +2,7 @@ * Spring MVC helper classes to build {@link org.springframework.hateoas.Link}s and assemble * {@link org.springframework.hateoas.RepresentationModel} types. */ +@NonNullApi package org.springframework.hateoas.server.mvc; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/server/package-info.java b/src/main/java/org/springframework/hateoas/server/package-info.java new file mode 100644 index 00000000..de7998f1 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/server/package-info.java @@ -0,0 +1,7 @@ +/** + * Server-side components for hypermedia handling. + */ +@NonNullApi +package org.springframework.hateoas.server; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java b/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java index 166d203c..5211d8b0 100644 --- a/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java @@ -18,11 +18,11 @@ package org.springframework.hateoas.server.reactive; import static org.springframework.hateoas.server.reactive.HypermediaWebFilter.*; import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Mono; import java.util.List; import java.util.function.Function; +import reactor.core.publisher.Mono; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; @@ -31,6 +31,7 @@ import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.server.core.DummyInvocationUtils; import org.springframework.hateoas.server.core.TemplateVariableAwareLinkBuilderSupport; import org.springframework.hateoas.server.core.WebHandler; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponents; @@ -230,7 +231,7 @@ public class WebFluxLinkBuilder extends TemplateVariableAwareLinkBuilderSupport< * * @param exchange */ - private static UriComponentsBuilder getBuilder(ServerWebExchange exchange) { + private static UriComponentsBuilder getBuilder(@Nullable ServerWebExchange exchange) { return exchange == null // ? UriComponentsBuilder.fromPath("/") // diff --git a/src/main/java/org/springframework/hateoas/server/reactive/package-info.java b/src/main/java/org/springframework/hateoas/server/reactive/package-info.java new file mode 100644 index 00000000..5d949733 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/server/reactive/package-info.java @@ -0,0 +1,8 @@ +/** + * Spring WebFlux components to build {@link org.springframework.hateoas.Link}s and assemble + * {@link org.springframework.hateoas.RepresentationModel} types. + */ +@NonNullApi +package org.springframework.hateoas.server.reactive; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/support/package-info.java b/src/main/java/org/springframework/hateoas/support/package-info.java new file mode 100644 index 00000000..ded6a191 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/support/package-info.java @@ -0,0 +1,7 @@ +/** + * Support utilities for hypermedia handling. + */ +@NonNullApi +package org.springframework.hateoas.support; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/src/test/java/org/springframework/hateoas/LinkUnitTest.java b/src/test/java/org/springframework/hateoas/LinkUnitTest.java index 04b62f7e..252b8b44 100755 --- a/src/test/java/org/springframework/hateoas/LinkUnitTest.java +++ b/src/test/java/org/springframework/hateoas/LinkUnitTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.springframework.core.ResolvableType; import org.springframework.hateoas.support.Employee; import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; /** * Unit tests for {@link Link}. @@ -451,4 +452,17 @@ public class LinkUnitTest { assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> new Link("/{segment}/path").toUri()); } + @Test + public void foo() { + String foo = getIt(); + + if (foo == null) { + System.out.println("Yo"); + } + } + + private String getIt() { + return null; + } + }