28 changed files with 1244 additions and 107 deletions
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* Copyright 2014-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.geo; |
||||
|
||||
import tools.jackson.core.Version; |
||||
import tools.jackson.databind.annotation.JsonDeserialize; |
||||
import tools.jackson.databind.module.SimpleModule; |
||||
|
||||
import java.io.Serial; |
||||
import java.util.List; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
||||
/** |
||||
* Custom module to deserialize the geo-spatial value objects using Jackson 3. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @author Mark Paluch |
||||
* @since 4.0 |
||||
*/ |
||||
@SuppressWarnings("unused") |
||||
public class GeoJacksonModule extends SimpleModule { |
||||
|
||||
private static final @Serial long serialVersionUID = 1L; |
||||
|
||||
/** |
||||
* Creates a new {@link GeoJacksonModule} registering mixins for common geo-spatial types. |
||||
*/ |
||||
public GeoJacksonModule() { |
||||
|
||||
super("Spring Data Geo Mixins", new Version(1, 0, 0, null, "org.springframework.data", "spring-data-commons-geo")); |
||||
|
||||
setMixInAnnotation(Distance.class, DistanceMixin.class); |
||||
setMixInAnnotation(Point.class, PointMixin.class); |
||||
setMixInAnnotation(Box.class, BoxMixin.class); |
||||
setMixInAnnotation(Circle.class, CircleMixin.class); |
||||
setMixInAnnotation(Polygon.class, PolygonMixin.class); |
||||
} |
||||
|
||||
@JsonIgnoreProperties("unit") |
||||
static abstract class DistanceMixin { |
||||
|
||||
DistanceMixin(@JsonProperty("value") double value, |
||||
@JsonProperty("metric") @JsonDeserialize(as = Metrics.class) Metric metic) {} |
||||
|
||||
@JsonIgnore |
||||
abstract double getNormalizedValue(); |
||||
} |
||||
|
||||
static abstract class PointMixin { |
||||
PointMixin(@JsonProperty("x") double x, @JsonProperty("y") double y) {} |
||||
} |
||||
|
||||
static abstract class CircleMixin { |
||||
CircleMixin(@JsonProperty("center") Point center, @JsonProperty("radius") Distance radius) {} |
||||
} |
||||
|
||||
static abstract class BoxMixin { |
||||
BoxMixin(@JsonProperty("first") Point first, @JsonProperty("second") Point point) {} |
||||
} |
||||
|
||||
static abstract class PolygonMixin { |
||||
PolygonMixin(@JsonProperty("points") List<Point> points) {} |
||||
} |
||||
} |
||||
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
/* |
||||
* Copyright 2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.init; |
||||
|
||||
import tools.jackson.databind.ObjectMapper; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.beans.factory.FactoryBean; |
||||
|
||||
/** |
||||
* {@link FactoryBean} to set up a {@link ResourceReaderRepositoryPopulator} with a {@link JacksonResourceReader}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @author Oliver Gierke |
||||
* @author Christoph Strobl |
||||
* @since 4.0 |
||||
*/ |
||||
public class JacksonRepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean { |
||||
|
||||
private @Nullable ObjectMapper mapper; |
||||
|
||||
/** |
||||
* Configures the {@link ObjectMapper} to be used. |
||||
* |
||||
* @param mapper can be {@literal null}. |
||||
*/ |
||||
public void setMapper(@Nullable ObjectMapper mapper) { |
||||
this.mapper = mapper; |
||||
} |
||||
|
||||
@Override |
||||
protected ResourceReader getResourceReader() { |
||||
return mapper == null ? new JacksonResourceReader() : new JacksonResourceReader(mapper); |
||||
} |
||||
} |
||||
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright 2013-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.init; |
||||
|
||||
import tools.jackson.databind.DeserializationFeature; |
||||
import tools.jackson.databind.JsonNode; |
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.json.JsonMapper; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.util.ArrayList; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.core.io.Resource; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
/** |
||||
* A {@link ResourceReader} using Jackson to read JSON into objects. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @author Christoph Strobl |
||||
* @author Mark Paluch |
||||
* @author Johannes Englmeier |
||||
* @since 4.0 |
||||
*/ |
||||
public class JacksonResourceReader implements ResourceReader { |
||||
|
||||
private static final String DEFAULT_TYPE_KEY = "_class"; |
||||
private static final ObjectMapper DEFAULT_MAPPER = JsonMapper.builder() |
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build(); |
||||
|
||||
private final ObjectMapper mapper; |
||||
private String typeKey = DEFAULT_TYPE_KEY; |
||||
|
||||
/** |
||||
* Creates a new {@link JacksonResourceReader}. |
||||
*/ |
||||
public JacksonResourceReader() { |
||||
this(DEFAULT_MAPPER); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link JacksonResourceReader} using the given {@link ObjectMapper}. |
||||
* |
||||
* @param mapper |
||||
*/ |
||||
public JacksonResourceReader(ObjectMapper mapper) { |
||||
this.mapper = mapper; |
||||
} |
||||
|
||||
/** |
||||
* Configures the JSON document's key to look up the type to instantiate the object. Defaults to |
||||
* {@link JacksonResourceReader#DEFAULT_TYPE_KEY}. |
||||
* |
||||
* @param typeKey |
||||
*/ |
||||
public void setTypeKey(@Nullable String typeKey) { |
||||
this.typeKey = typeKey == null ? DEFAULT_TYPE_KEY : typeKey; |
||||
} |
||||
|
||||
@Override |
||||
public Object readFrom(Resource resource, @Nullable ClassLoader classLoader) throws Exception { |
||||
|
||||
Assert.notNull(resource, "Resource must not be null"); |
||||
|
||||
InputStream stream = resource.getInputStream(); |
||||
JsonNode node = mapper.readerFor(JsonNode.class).readTree(stream); |
||||
|
||||
if (node.isArray()) { |
||||
|
||||
Iterator<JsonNode> elements = node.iterator(); |
||||
List<Object> result = new ArrayList<>(); |
||||
|
||||
while (elements.hasNext()) { |
||||
JsonNode element = elements.next(); |
||||
result.add(readSingle(element, classLoader)); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
return readSingle(node, classLoader); |
||||
} |
||||
|
||||
/** |
||||
* Reads the given {@link JsonNode} into an instance of the type encoded in it using the configured type key. |
||||
* |
||||
* @param node must not be {@literal null}. |
||||
* @param classLoader can be {@literal null}. |
||||
* @return |
||||
*/ |
||||
private Object readSingle(JsonNode node, @Nullable ClassLoader classLoader) throws IOException { |
||||
|
||||
JsonNode typeNode = node.findValue(typeKey); |
||||
|
||||
if (typeNode == null) { |
||||
throw new IllegalArgumentException(String.format("Could not find type for type key '%s'", typeKey)); |
||||
} |
||||
|
||||
String typeName = typeNode.asString(); |
||||
Class<?> type = ClassUtils.resolveClassName(typeName, classLoader); |
||||
|
||||
return mapper.readerFor(type).readValue(node); |
||||
} |
||||
} |
||||
@ -0,0 +1,275 @@
@@ -0,0 +1,275 @@
|
||||
/* |
||||
* Copyright 2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.web; |
||||
|
||||
import tools.jackson.core.JsonGenerator; |
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.ObjectReader; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.io.StringWriter; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.beans.BeansException; |
||||
import org.springframework.beans.factory.BeanClassLoaderAware; |
||||
import org.springframework.beans.factory.BeanFactory; |
||||
import org.springframework.beans.factory.BeanFactoryAware; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.annotation.AnnotationUtils; |
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; |
||||
import org.springframework.http.HttpInputMessage; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.http.converter.HttpMessageNotReadableException; |
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import com.jayway.jsonpath.Configuration; |
||||
import com.jayway.jsonpath.InvalidJsonException; |
||||
import com.jayway.jsonpath.TypeRef; |
||||
import com.jayway.jsonpath.spi.json.AbstractJsonProvider; |
||||
import com.jayway.jsonpath.spi.mapper.MappingException; |
||||
import com.jayway.jsonpath.spi.mapper.MappingProvider; |
||||
|
||||
/** |
||||
* {@link HttpMessageConverter} implementation to enable projected JSON binding to interfaces annotated with |
||||
* {@link ProjectedPayload}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @author Oliver Gierke |
||||
* @author Christoph Strobl |
||||
* @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive) |
||||
* @since 4.0 |
||||
*/ |
||||
public class ProjectingJacksonHttpMessageConverter extends JacksonJsonHttpMessageConverter |
||||
implements BeanClassLoaderAware, BeanFactoryAware { |
||||
|
||||
private final SpelAwareProxyProjectionFactory projectionFactory; |
||||
private final Map<Class<?>, Boolean> supportedTypesCache = new ConcurrentHashMap<>(); |
||||
|
||||
/** |
||||
* Creates a new {@link ProjectingJacksonHttpMessageConverter} using a default {@link ObjectMapper}. |
||||
*/ |
||||
public ProjectingJacksonHttpMessageConverter() { |
||||
this.projectionFactory = initProjectionFactory(getObjectMapper()); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link ProjectingJacksonHttpMessageConverter} for the given {@link ObjectMapper}. |
||||
* |
||||
* @param mapper must not be {@literal null}. |
||||
*/ |
||||
public ProjectingJacksonHttpMessageConverter(ObjectMapper mapper) { |
||||
|
||||
super(mapper); |
||||
|
||||
this.projectionFactory = initProjectionFactory(mapper); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link SpelAwareProxyProjectionFactory} with the {@link JsonProjectingMethodInterceptorFactory} |
||||
* registered for the given {@link ObjectMapper}. |
||||
* |
||||
* @param mapper must not be {@literal null}. |
||||
* @return |
||||
*/ |
||||
private static SpelAwareProxyProjectionFactory initProjectionFactory(ObjectMapper mapper) { |
||||
|
||||
Assert.notNull(mapper, "ObjectMapper must not be null"); |
||||
|
||||
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); |
||||
projectionFactory.registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory( |
||||
new JacksonJsonProvider(mapper), new JacksonMappingProvider(mapper))); |
||||
|
||||
return projectionFactory; |
||||
} |
||||
|
||||
@Override |
||||
public void setBeanClassLoader(ClassLoader classLoader) { |
||||
projectionFactory.setBeanClassLoader(classLoader); |
||||
} |
||||
|
||||
@Override |
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException { |
||||
projectionFactory.setBeanFactory(beanFactory); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean supports(Class<?> clazz) { |
||||
|
||||
if (clazz.isInterface()) { |
||||
|
||||
Boolean result = supportedTypesCache.get(clazz); |
||||
|
||||
if (result != null) { |
||||
return result; |
||||
} |
||||
|
||||
result = AnnotationUtils.findAnnotation(clazz, ProjectedPayload.class) != null; |
||||
supportedTypesCache.put(clazz, result); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) { |
||||
|
||||
if (!super.canRead(type, mediaType)) { |
||||
return false; |
||||
} |
||||
|
||||
Class<?> clazz = type.resolve(); |
||||
if (clazz == null) { |
||||
return false; |
||||
} |
||||
|
||||
return supports(clazz); |
||||
} |
||||
|
||||
@Override |
||||
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints) |
||||
throws IOException, HttpMessageNotReadableException { |
||||
return projectionFactory.createProjection(type.resolve(Object.class), inputMessage.getBody()); |
||||
} |
||||
|
||||
record JacksonMappingProvider(ObjectMapper objectMapper) implements MappingProvider { |
||||
|
||||
@Override |
||||
public <T> @Nullable T map(@Nullable Object source, Class<T> targetType, Configuration configuration) { |
||||
if (source == null) { |
||||
return null; |
||||
} |
||||
try { |
||||
return objectMapper.convertValue(source, targetType); |
||||
} catch (Exception e) { |
||||
throw new MappingException(e); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public <T> @Nullable T map(@Nullable Object source, final TypeRef<T> targetType, Configuration configuration) { |
||||
if (source == null) { |
||||
return null; |
||||
} |
||||
|
||||
tools.jackson.databind.JavaType type = objectMapper.getTypeFactory().constructType(targetType.getType()); |
||||
|
||||
try { |
||||
return objectMapper.convertValue(source, type); |
||||
} catch (Exception e) { |
||||
throw new MappingException(e); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
static class JacksonJsonProvider extends AbstractJsonProvider { |
||||
|
||||
private static final ObjectMapper defaultObjectMapper = new ObjectMapper(); |
||||
private static final ObjectReader defaultObjectReader = defaultObjectMapper.reader().forType(Object.class); |
||||
|
||||
protected ObjectMapper objectMapper; |
||||
protected ObjectReader objectReader; |
||||
|
||||
public ObjectMapper getObjectMapper() { |
||||
return objectMapper; |
||||
} |
||||
|
||||
/** |
||||
* Initialize the JacksonProvider with the default ObjectMapper and ObjectReader |
||||
*/ |
||||
public JacksonJsonProvider() { |
||||
this(defaultObjectMapper, defaultObjectReader); |
||||
} |
||||
|
||||
/** |
||||
* Initialize the JacksonProvider with a custom ObjectMapper. |
||||
* |
||||
* @param objectMapper the ObjectMapper to use |
||||
*/ |
||||
public JacksonJsonProvider(ObjectMapper objectMapper) { |
||||
this(objectMapper, objectMapper.readerFor(Object.class)); |
||||
} |
||||
|
||||
/** |
||||
* Initialize the JacksonProvider with a custom ObjectMapper and ObjectReader. |
||||
* |
||||
* @param objectMapper the ObjectMapper to use |
||||
* @param objectReader the ObjectReader to use |
||||
*/ |
||||
public JacksonJsonProvider(ObjectMapper objectMapper, tools.jackson.databind.ObjectReader objectReader) { |
||||
this.objectMapper = objectMapper; |
||||
this.objectReader = objectReader; |
||||
} |
||||
|
||||
@Override |
||||
public Object parse(String json) throws InvalidJsonException { |
||||
return objectReader.readValue(json); |
||||
} |
||||
|
||||
@Override |
||||
public Object parse(InputStream jsonStream, String charset) throws InvalidJsonException { |
||||
try { |
||||
return objectReader.readValue(new InputStreamReader(jsonStream, charset)); |
||||
} catch (IOException e) { |
||||
throw new InvalidJsonException(e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public String toJson(Object obj) { |
||||
StringWriter writer = new StringWriter(); |
||||
try { |
||||
JsonGenerator generator = objectMapper.writer().createGenerator(writer); |
||||
objectMapper.writeValue(generator, obj); |
||||
writer.flush(); |
||||
writer.close(); |
||||
generator.close(); |
||||
return writer.getBuffer().toString(); |
||||
} catch (IOException e) { |
||||
throw new InvalidJsonException(e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public List<Object> createArray() { |
||||
return new LinkedList<>(); |
||||
} |
||||
|
||||
@Override |
||||
public Object createMap() { |
||||
return new LinkedHashMap<String, Object>(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,161 @@
@@ -0,0 +1,161 @@
|
||||
/* |
||||
* Copyright 2014-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.web.config; |
||||
|
||||
import tools.jackson.databind.annotation.JsonSerialize; |
||||
import tools.jackson.databind.module.SimpleModule; |
||||
import tools.jackson.databind.ser.BeanPropertyWriter; |
||||
import tools.jackson.databind.ser.ValueSerializerModifier; |
||||
import tools.jackson.databind.ser.std.ToStringSerializerBase; |
||||
import tools.jackson.databind.util.StdConverter; |
||||
|
||||
import java.io.Serial; |
||||
import java.util.List; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.data.domain.Page; |
||||
import org.springframework.data.domain.PageImpl; |
||||
import org.springframework.data.geo.GeoModule; |
||||
import org.springframework.data.web.PagedModel; |
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; |
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
/** |
||||
* JavaConfig class to export Jackson 3-specific configuration. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @author Mark Paluch |
||||
* @since 4.0 |
||||
*/ |
||||
public class SpringDataJackson3Configuration implements SpringDataJackson3Modules { |
||||
|
||||
@Nullable |
||||
@Autowired(required = false) SpringDataWebSettings settings; |
||||
|
||||
@Bean |
||||
public GeoModule jackson3GeoModule() { |
||||
return new GeoModule(); |
||||
} |
||||
|
||||
@Bean |
||||
public PageModule jackson3pageModule() { |
||||
return new PageModule(settings); |
||||
} |
||||
|
||||
/** |
||||
* A Jackson module customizing the serialization of {@link PageImpl} instances depending on the |
||||
* {@link SpringDataWebSettings} handed into the instance. In case of {@link PageSerializationMode#DIRECT} being |
||||
* configured, a no-op {@link StdConverter} is registered to issue a one-time warning about the mode being used (as |
||||
* it's not recommended). {@link PageSerializationMode#VIA_DTO} would register a converter wrapping {@link PageImpl} |
||||
* instances into {@link PagedModel}. |
||||
* |
||||
* @author Oliver Drotbohm |
||||
*/ |
||||
public static class PageModule extends SimpleModule { |
||||
|
||||
private static final @Serial long serialVersionUID = 275254460581626332L; |
||||
|
||||
private static final String UNPAGED_TYPE_NAME = "org.springframework.data.domain.Unpaged"; |
||||
private static final Class<?> UNPAGED_TYPE; |
||||
|
||||
static { |
||||
UNPAGED_TYPE = ClassUtils.resolveClassName(UNPAGED_TYPE_NAME, PageModule.class.getClassLoader()); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PageModule} for the given {@link SpringDataWebSettings}. |
||||
* |
||||
* @param settings can be {@literal null}. |
||||
*/ |
||||
public PageModule(@Nullable SpringDataWebSettings settings) { |
||||
|
||||
addSerializer(UNPAGED_TYPE, new UnpagedAsInstanceSerializer()); |
||||
|
||||
if (settings == null || settings.pageSerializationMode() == PageSerializationMode.DIRECT) { |
||||
setSerializerModifier(new WarningLoggingModifier()); |
||||
|
||||
} else { |
||||
setMixInAnnotation(PageImpl.class, WrappingMixing.class); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A Jackson serializer rendering instances of {@code org.springframework.data.domain.Unpaged} as {@code INSTANCE} |
||||
* as it was previous rendered. |
||||
* |
||||
* @author Oliver Drotbohm |
||||
*/ |
||||
static class UnpagedAsInstanceSerializer extends ToStringSerializerBase { |
||||
|
||||
public UnpagedAsInstanceSerializer() { |
||||
super(Object.class); |
||||
} |
||||
|
||||
@Override |
||||
public String valueToString(@Nullable Object value) { |
||||
return "INSTANCE"; |
||||
} |
||||
} |
||||
|
||||
@JsonSerialize(converter = PageModelConverter.class) |
||||
abstract static class WrappingMixing {} |
||||
|
||||
static class PageModelConverter extends StdConverter<Page<?>, PagedModel<?>> { |
||||
|
||||
@Override |
||||
public @Nullable PagedModel<?> convert(@Nullable Page<?> value) { |
||||
return value == null ? null : new PagedModel<>(value); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A {@link ValueSerializerModifier} that logs a warning message if an instance of {@link Page} will be rendered. |
||||
* |
||||
* @author Oliver Drotbohm |
||||
*/ |
||||
static class WarningLoggingModifier extends ValueSerializerModifier { |
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WarningLoggingModifier.class); |
||||
private static final String MESSAGE = """ |
||||
Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure! |
||||
For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)) |
||||
or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.
|
||||
"""; |
||||
|
||||
private static final @Serial long serialVersionUID = 954857444010009875L; |
||||
|
||||
private boolean warningRendered = false; |
||||
|
||||
@Override |
||||
public List<BeanPropertyWriter> changeProperties(tools.jackson.databind.SerializationConfig config, |
||||
tools.jackson.databind.BeanDescription.Supplier beanDesc, List<BeanPropertyWriter> beanProperties) { |
||||
|
||||
if (Page.class.isAssignableFrom(beanDesc.getBeanClass()) && !warningRendered) { |
||||
|
||||
this.warningRendered = true; |
||||
LOGGER.warn(MESSAGE); |
||||
} |
||||
|
||||
return super.changeProperties(config, beanDesc, beanProperties); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* Copyright 2016-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.web.config; |
||||
|
||||
/** |
||||
* Marker interface to describe configuration classes that ship Jackson modules that are supposed to be added to the |
||||
* Jackson 3 {@link tools.jackson.databind.ObjectMapper} configured for {@link EnableSpringDataWebSupport}. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @author Mark Paluch |
||||
* @since 4.0 |
||||
*/ |
||||
public interface SpringDataJackson3Modules {} |
||||
@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
|
||||
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration |
||||
org.springframework.data.web.config.SpringDataJackson3Modules=org.springframework.data.web.config.SpringDataJackson3Configuration |
||||
org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections, \ |
||||
org.springframework.data.util.CustomCollections.EclipseCollections |
||||
org.springframework.beans.BeanInfoFactory=org.springframework.data.util.KotlinBeanInfoFactory |
||||
|
||||
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
/* |
||||
* Copyright 2014-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.geo; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.json.JsonMapper; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
/** |
||||
* Integration tests for {@link GeoModule}. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @author Mark Paluch |
||||
*/ |
||||
class GeoJacksonModuleIntegrationTests { |
||||
|
||||
ObjectMapper mapper; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
|
||||
this.mapper = JsonMapper.builder().addModule(new GeoJacksonModule()).build(); |
||||
} |
||||
|
||||
@Test // DATACMNS-475
|
||||
void deserializesDistance() throws Exception { |
||||
|
||||
var json = "{\"value\":10.0,\"metric\":\"KILOMETERS\"}"; |
||||
var reference = new Distance(10.0, Metrics.KILOMETERS); |
||||
|
||||
assertThat(mapper.readValue(json, Distance.class)).isEqualTo(reference); |
||||
assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); |
||||
} |
||||
|
||||
@Test // DATACMNS-475
|
||||
void deserializesPoint() throws Exception { |
||||
|
||||
var json = "{\"x\":10.0,\"y\":20.0}"; |
||||
var reference = new Point(10.0, 20.0); |
||||
|
||||
assertThat(mapper.readValue(json, Point.class)).isEqualTo(reference); |
||||
assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); |
||||
} |
||||
|
||||
@Test // DATACMNS-475
|
||||
void deserializesCircle() throws Exception { |
||||
|
||||
var json = "{\"center\":{\"x\":10.0,\"y\":20.0},\"radius\":{\"value\":10.0,\"metric\":\"KILOMETERS\"}}"; |
||||
var reference = new Circle(new Point(10.0, 20.0), new Distance(10, Metrics.KILOMETERS)); |
||||
|
||||
assertThat(mapper.readValue(json, Circle.class)).isEqualTo(reference); |
||||
assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); |
||||
} |
||||
|
||||
@Test // DATACMNS-475
|
||||
void deserializesBox() throws Exception { |
||||
|
||||
var json = "{\"first\":{\"x\":1.0,\"y\":2.0},\"second\":{\"x\":2.0,\"y\":3.0}}"; |
||||
var reference = new Box(new Point(1, 2), new Point(2, 3)); |
||||
|
||||
assertThat(mapper.readValue(json, Box.class)).isEqualTo(reference); |
||||
assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); |
||||
} |
||||
|
||||
@Test // DATACMNS-475
|
||||
void deserializesPolygon() throws Exception { |
||||
|
||||
var json = "{\"points\":[{\"x\":1.0,\"y\":2.0},{\"x\":2.0,\"y\":3.0},{\"x\":3.0,\"y\":4.0}]}"; |
||||
var reference = new Polygon(new Point(1, 2), new Point(2, 3), new Point(3, 4)); |
||||
|
||||
assertThat(mapper.readValue(json, Polygon.class)).isEqualTo(reference); |
||||
assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright 2013-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.init; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.Collection; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
|
||||
/** |
||||
* Integration tests for {@link JacksonResourceReader}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class JacksonResourceReaderIntegrationTests { |
||||
|
||||
@Test |
||||
void readsFileWithMultipleObjects() throws Exception { |
||||
|
||||
ResourceReader reader = new JacksonResourceReader(); |
||||
var result = reader.readFrom(new ClassPathResource("data.json", getClass()), null); |
||||
|
||||
assertThat(result).isInstanceOf(Collection.class); |
||||
assertThat((Collection<?>) result).hasSize(1); |
||||
} |
||||
} |
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
/* |
||||
* Copyright 2024-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.web; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.Collections; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.data.domain.PageImpl; |
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; |
||||
import org.springframework.data.web.config.SpringDataJacksonConfiguration; |
||||
import org.springframework.data.web.config.SpringDataWebSettings; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.jayway.jsonpath.JsonPath; |
||||
|
||||
/** |
||||
* Unit tests for PageImpl serialization. |
||||
* |
||||
* @author Oliver Drotbohm |
||||
* @author Mark Paluch |
||||
*/ |
||||
class PageImplJsonJackson2SerializationUnitTests { |
||||
|
||||
@Test // GH-3024
|
||||
void serializesPageImplAsJson() { |
||||
assertJsonRendering(PageSerializationMode.DIRECT, "$.pageable", "$.last", "$.first"); |
||||
} |
||||
|
||||
@Test // GH-3024
|
||||
void serializesPageImplAsPagedModel() { |
||||
assertJsonRendering(PageSerializationMode.VIA_DTO, "$.content", "$.page"); |
||||
} |
||||
|
||||
@Test // GH-3137
|
||||
void serializesCustomPageAsPageImpl() { |
||||
assertJsonRendering(PageSerializationMode.DIRECT, new Extension<>("header"), "$.pageable", "$.last", "$.first"); |
||||
} |
||||
|
||||
private static void assertJsonRendering(PageSerializationMode mode, String... jsonPaths) { |
||||
assertJsonRendering(mode, new PageImpl<>(Collections.emptyList()), jsonPaths); |
||||
} |
||||
|
||||
private static void assertJsonRendering(PageSerializationMode mode, PageImpl<?> page, String... jsonPaths) { |
||||
|
||||
SpringDataWebSettings settings = new SpringDataWebSettings(mode); |
||||
|
||||
ObjectMapper mapper = new ObjectMapper(); |
||||
mapper.registerModule(new SpringDataJacksonConfiguration.PageModule(settings)); |
||||
|
||||
assertThatNoException().isThrownBy(() -> { |
||||
|
||||
String result = mapper.writeValueAsString(page); |
||||
|
||||
for (String jsonPath : jsonPaths) { |
||||
assertThat(JsonPath.<Object> read(result, jsonPath)).isNotNull(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
static class Extension<T> extends PageImpl<T> { |
||||
|
||||
private Object header; |
||||
|
||||
public Extension(Object header) { |
||||
super(Collections.emptyList()); |
||||
} |
||||
|
||||
public Object getHeader() { |
||||
return header; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
/* |
||||
* Copyright 2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.web; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.http.MediaType; |
||||
|
||||
/** |
||||
* Unit tests for {@link ProjectingJacksonHttpMessageConverter}. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @author Mark Paluch |
||||
*/ |
||||
class ProjectingJacksonHttpMessageConverterUnitTests { |
||||
|
||||
ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter(); |
||||
MediaType ANYTHING_JSON = MediaType.parseMediaType("application/*+json"); |
||||
|
||||
@Test // DATCMNS-885
|
||||
void canReadJsonIntoAnnotatedInterface() { |
||||
assertThat(converter.canRead(SampleInterface.class, ANYTHING_JSON)).isTrue(); |
||||
} |
||||
|
||||
@Test // DATCMNS-885
|
||||
void cannotReadUnannotatedInterface() { |
||||
assertThat(converter.canRead(UnannotatedInterface.class, ANYTHING_JSON)).isFalse(); |
||||
} |
||||
|
||||
@Test // DATCMNS-885
|
||||
void cannotReadClass() { |
||||
assertThat(converter.canRead(SampleClass.class, ANYTHING_JSON)).isFalse(); |
||||
} |
||||
|
||||
@Test // DATACMNS-972
|
||||
void doesNotConsiderTypeVariableBoundTo() throws Throwable { |
||||
|
||||
var method = BaseController.class.getDeclaredMethod("createEntity", AbstractDto.class); |
||||
|
||||
assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse(); |
||||
} |
||||
|
||||
@Test // DATACMNS-972
|
||||
void genericTypeOnConcreteOne() throws Throwable { |
||||
|
||||
var method = ConcreteController.class.getMethod("createEntity", AbstractDto.class); |
||||
|
||||
assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse(); |
||||
} |
||||
|
||||
@ProjectedPayload |
||||
interface SampleInterface {} |
||||
|
||||
interface UnannotatedInterface {} |
||||
|
||||
class SampleClass {} |
||||
|
||||
class AbstractDto {} |
||||
|
||||
abstract class BaseController<D extends AbstractDto> { |
||||
public void createEntity(D dto) {} |
||||
} |
||||
|
||||
class ConcreteController extends BaseController<AbstractDto> {} |
||||
} |
||||
Loading…
Reference in new issue