From 29be609c877a7f30fa65043406b927f36f7947df Mon Sep 17 00:00:00 2001 From: Max Novik Date: Wed, 26 Oct 2022 17:03:21 +0300 Subject: [PATCH 1/4] Initial Spring web support. --- agrest-spring-boot-starter/pom.xml | 60 ++++++++++++ .../spring/starter/AgRuntimeCustomizer.java | 8 ++ .../starter/AgrestAutoConfiguration.java | 63 ++++++++++++ .../spring/starter/DataResponseWriter.java | 54 +++++++++++ .../starter/EntityUpdateCollectionReader.java | 82 ++++++++++++++++ .../spring/starter/EntityUpdateReader.java | 95 ++++++++++++++++++ .../starter/EntityUpdateReaderProcessor.java | 26 +++++ .../spring/starter/SimpleResponseWriter.java | 58 +++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../starter/AgrestAutoConfigurationTest.java | 53 ++++++++++ .../spring/starter/it/pojo/model/P1.java | 18 ++++ .../spring/starter/it/pojo/model/P10.java | 52 ++++++++++ .../spring/starter/it/pojo/model/P2.java | 28 ++++++ .../spring/starter/it/pojo/model/P3.java | 18 ++++ .../spring/starter/it/pojo/model/P4.java | 17 ++++ .../spring/starter/it/pojo/model/P6.java | 28 ++++++ .../spring/starter/it/pojo/model/P7.java | 28 ++++++ .../spring/starter/it/pojo/model/P8.java | 96 +++++++++++++++++++ .../spring/starter/it/pojo/model/P9.java | 42 ++++++++ .../it/pojo/runtime/PojoFetchStage.java | 83 ++++++++++++++++ .../PojoSelectProcessorFactoryProvider.java | 51 ++++++++++ .../starter/it/pojo/runtime/PojoStore.java | 26 +++++ .../resources/application-test.properties | 1 + pom.xml | 1 + 24 files changed, 989 insertions(+) create mode 100644 agrest-spring-boot-starter/pom.xml create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgRuntimeCustomizer.java create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/DataResponseWriter.java create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateCollectionReader.java create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReader.java create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReaderProcessor.java create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/SimpleResponseWriter.java create mode 100644 agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/AgrestAutoConfigurationTest.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java create mode 100644 agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java create mode 100644 agrest-spring-boot-starter/src/test/resources/application-test.properties diff --git a/agrest-spring-boot-starter/pom.xml b/agrest-spring-boot-starter/pom.xml new file mode 100644 index 000000000..eb1d2efe3 --- /dev/null +++ b/agrest-spring-boot-starter/pom.xml @@ -0,0 +1,60 @@ + + + + agrest-parent + io.agrest + 5.0.M8-SNAPSHOT + + 4.0.0 + + agrest-spring-boot-starter + + + 17 + 17 + + + + io.agrest + agrest-annotations + ${project.version} + + + io.agrest + agrest-engine + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.7.5 + pom + import + + + + \ No newline at end of file diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgRuntimeCustomizer.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgRuntimeCustomizer.java new file mode 100644 index 000000000..6e39bfe59 --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgRuntimeCustomizer.java @@ -0,0 +1,8 @@ +package io.agrest.spring.starter; + +import io.agrest.runtime.AgRuntimeBuilder; + +public interface AgRuntimeCustomizer { + + void customize(AgRuntimeBuilder builder); +} diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java new file mode 100644 index 000000000..a4e890e5a --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java @@ -0,0 +1,63 @@ +package io.agrest.spring.starter; + +import io.agrest.meta.AgSchema; +import io.agrest.runtime.AgRuntime; +import io.agrest.runtime.jackson.IJacksonService; +import io.agrest.runtime.protocol.IEntityUpdateParser; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnWebApplication +@ConditionalOnProperty(prefix = "agrest", name = "enabled") +public class AgrestAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AgRuntime agRuntime(ObjectProvider customizerProvider) { + var builder = AgRuntime.builder(); + customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + public IJacksonService jacksonService(AgRuntime runtime) { + return runtime.service(IJacksonService.class); + } + + @Bean + @ConditionalOnMissingBean + public DataResponseWriter dataResponseWriter(IJacksonService jacksonService) { + return new DataResponseWriter(jacksonService); + } + + @Bean + @ConditionalOnMissingBean + public EntityUpdateReaderProcessor entityUpdateReaderProcessor(AgRuntime runtime) { + return new EntityUpdateReaderProcessor(runtime.service(IEntityUpdateParser.class), runtime.service(AgSchema.class)); + } + + @Bean + @ConditionalOnMissingBean + public EntityUpdateReader entityUpdateReader(EntityUpdateReaderProcessor processor) { + return new EntityUpdateReader(processor); + } + + @Bean + @ConditionalOnMissingBean + public EntityUpdateCollectionReader entityUpdateCollectionReader(EntityUpdateReaderProcessor processor) { + return new EntityUpdateCollectionReader(processor); + } + + @Bean + @ConditionalOnMissingBean + public SimpleResponseWriter simpleResponseWriter(IJacksonService jacksonService) { + return new SimpleResponseWriter(jacksonService); + } +} diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/DataResponseWriter.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/DataResponseWriter.java new file mode 100644 index 000000000..0cd4fab42 --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/DataResponseWriter.java @@ -0,0 +1,54 @@ +package io.agrest.spring.starter; + +import com.fasterxml.jackson.core.JsonGenerator; +import io.agrest.DataResponse; +import io.agrest.runtime.jackson.IJacksonService; + +import java.io.IOException; +import java.util.List; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +public class DataResponseWriter implements HttpMessageConverter> { + + private final IJacksonService jacksonService; + + public DataResponseWriter(IJacksonService jacksonService) { + this.jacksonService = jacksonService; + } + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return DataResponse.class.isAssignableFrom(clazz); + } + + @Override + public List getSupportedMediaTypes() { + return null; + } + + @Override + public DataResponse read(Class> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + public void write(DataResponse dataResponse, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + var entityStream = outputMessage.getBody(); // todo: do we need to close this? + jacksonService.outputJson(out -> writeData(dataResponse, out), entityStream); + } + + protected void writeData(DataResponse t, JsonGenerator out) throws IOException { + t.getEncoder().encode(null, t, out); + } +} diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateCollectionReader.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateCollectionReader.java new file mode 100644 index 000000000..28bb41701 --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateCollectionReader.java @@ -0,0 +1,82 @@ +package io.agrest.spring.starter; + +import io.agrest.AgException; +import io.agrest.EntityUpdate; +import io.agrest.reflect.Types; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +public class EntityUpdateCollectionReader implements GenericHttpMessageConverter>> { + + private final EntityUpdateReaderProcessor reader; + + public EntityUpdateCollectionReader(EntityUpdateReaderProcessor reader) {this.reader = reader;} + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + var classForType = Types.getClassForType(type).orElse(Object.class); + if (!Collection.class.isAssignableFrom(classForType) || !MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { + return false; + } + + return Types.unwrapTypeArgument(type) + .filter(collectionParam -> collectionParam instanceof ParameterizedType) + .map(collectionParam -> (ParameterizedType) collectionParam) + .filter(collectionParam -> EntityUpdate.class.equals((collectionParam).getRawType())) + .isPresent(); + } + + @Override + public Collection> read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + Type entityUpdateType = Types.unwrapTypeArgument(type) + .orElseThrow(() -> AgException.internalServerError("Invalid request entity collection type: %s", type)); + + return reader.read(entityUpdateType, inputMessage.getBody()); + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + public void write(Collection> entityUpdates, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + } + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public List getSupportedMediaTypes() { + return null; + } + + @Override + public Collection> read(Class>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + public void write(Collection> entityUpdates, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + } +} diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReader.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReader.java new file mode 100644 index 000000000..3ca744b14 --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReader.java @@ -0,0 +1,95 @@ +package io.agrest.spring.starter; + +import io.agrest.AgException; +import io.agrest.EntityUpdate; +import io.agrest.reflect.Types; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +public class EntityUpdateReader implements GenericHttpMessageConverter> { + + private final EntityUpdateReaderProcessor reader; + + public EntityUpdateReader(EntityUpdateReaderProcessor reader) {this.reader = reader;} + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + // this is not called but we must implement it + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public List getSupportedMediaTypes() { + return List.of(MediaType.APPLICATION_JSON); + } + + @Override + public EntityUpdate read(Class> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + var entityStream = inputMessage.getBody(); + //todo this clazz here may be wrong + Collection> updates = reader.read(clazz, entityStream); + + if (updates.isEmpty()) { + throw AgException.badRequest("No update"); + } + + if (updates.size() > 1) { + throw AgException.badRequest("Expected single update. Found: %s", updates.size()); + } + + return updates.iterator().next(); + } + + @Override + public void write(EntityUpdate entityUpdate, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + throw new IllegalStateException("Should never be called!"); + } + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + return EntityUpdate.class.equals(Types.getClassForType(type).orElse(Object.class)) + && MediaType.APPLICATION_JSON.isCompatibleWith(mediaType); + } + + @Override + public EntityUpdate read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + var entityStream = inputMessage.getBody(); + //todo this clazz here may be wrong + Collection> updates = reader.read(type, entityStream); + + if (updates.isEmpty()) { + throw AgException.badRequest("No update"); + } + + if (updates.size() > 1) { + throw AgException.badRequest("Expected single update. Found: %s", updates.size()); + } + + return updates.iterator().next(); + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + public void write(EntityUpdate entityUpdate, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + throw new IllegalStateException("Should never be called!"); + } +} diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReaderProcessor.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReaderProcessor.java new file mode 100644 index 000000000..47af6e3c6 --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/EntityUpdateReaderProcessor.java @@ -0,0 +1,26 @@ +package io.agrest.spring.starter; + +import io.agrest.EntityUpdate; +import io.agrest.meta.AgSchema; +import io.agrest.reflect.Types; +import io.agrest.runtime.protocol.IEntityUpdateParser; + +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.Collection; + +class EntityUpdateReaderProcessor { + + private IEntityUpdateParser parser; + private AgSchema schema; + + EntityUpdateReaderProcessor(IEntityUpdateParser parser, AgSchema schema) { + this.parser = parser; + this.schema = schema; + } + + Collection> read(Type entityUpdateType, InputStream entityStream) { + Class typeClass = (Class) Types.getClassForTypeArgument(entityUpdateType).orElse(Object.class); + return parser.parse(schema.getEntity(typeClass), entityStream); + } +} diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/SimpleResponseWriter.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/SimpleResponseWriter.java new file mode 100644 index 000000000..e1724460c --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/SimpleResponseWriter.java @@ -0,0 +1,58 @@ +package io.agrest.spring.starter; + +import com.fasterxml.jackson.core.JsonGenerator; +import io.agrest.SimpleResponse; +import io.agrest.runtime.jackson.IJacksonService; + +import java.io.IOException; +import java.util.List; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +public class SimpleResponseWriter implements HttpMessageConverter { + + private final IJacksonService jacksonService; + + public SimpleResponseWriter(IJacksonService jacksonService) { + this.jacksonService = jacksonService; + } + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return SimpleResponse.class.isAssignableFrom(clazz); + } + + @Override + public List getSupportedMediaTypes() { + return null; + } + + @Override + public SimpleResponse read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + public void write(SimpleResponse simpleResponse, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + var entityStream = outputMessage.getBody(); + jacksonService.outputJson(out -> writeData(simpleResponse, out), entityStream); + } + + protected void writeData(SimpleResponse t, JsonGenerator out) throws IOException { + out.writeStartObject(); + if (t.getMessage() != null) { + out.writeStringField("message", t.getMessage()); + } + out.writeEndObject(); + } +} diff --git a/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories b/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..b9aeca4cf --- /dev/null +++ b/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=hornetq.autoconfigure.AgrestAutoConfiguration diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/AgrestAutoConfigurationTest.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/AgrestAutoConfigurationTest.java new file mode 100644 index 000000000..7df6e7267 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/AgrestAutoConfigurationTest.java @@ -0,0 +1,53 @@ +package io.agrest.spring.starter; + +import io.agrest.runtime.AgRuntime; +import io.agrest.runtime.AgRuntimeBuilder; +import io.agrest.runtime.jackson.IJacksonService; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + + +class AgrestAutoConfigurationTest { + + private final ConditionEvaluationReportLoggingListener initializer = new ConditionEvaluationReportLoggingListener( + LogLevel.INFO); + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withInitializer(initializer) + .withPropertyValues("agrest.enabled", "true") + .withConfiguration(AutoConfigurations.of(AgrestAutoConfiguration.class)); + + @Test + void contributesDefaultBeans() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(AgRuntime.class); + assertThat(context).hasSingleBean(IJacksonService.class); + assertThat(context).hasSingleBean(DataResponseWriter.class); + assertThat(context).hasSingleBean(EntityUpdateReaderProcessor.class); + assertThat(context).hasSingleBean(EntityUpdateReader.class); + assertThat(context).hasSingleBean(SimpleResponseWriter.class); + }); + } + + @Test + void backsOffInNonWebApplicationContexts() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(AgrestAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(AgRuntime.class)); + } + + @Test + void backsOffInNotEnabled() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AgrestAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(AgRuntime.class)); + } +} \ No newline at end of file diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java new file mode 100644 index 000000000..1072aaff5 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java @@ -0,0 +1,18 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; + +public class P1 { + + private String name; + + @AgAttribute + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java new file mode 100644 index 000000000..8c4fd8e0a --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java @@ -0,0 +1,52 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; +import io.agrest.annotation.AgId; + +import java.util.HashMap; +import java.util.Map; + +public class P10 { + + private int id1; + private String id2; + private String a1; + + public static Map id(int id1, String id2) { + Map id = new HashMap<>(); + id.put("id1", id1); + id.put("id2", id2); + return id; + } + + public Map id() { + return P10.id(id1, id2); + } + + @AgId + public int getId1() { + return id1; + } + + public void setId1(int id1) { + this.id1 = id1; + } + + @AgId + public String getId2() { + return id2; + } + + public void setId2(String id2) { + this.id2 = id2; + } + + @AgAttribute + public String getA1() { + return a1; + } + + public void setA1(String a1) { + this.a1 = a1; + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java new file mode 100644 index 000000000..471475790 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java @@ -0,0 +1,28 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; +import io.agrest.annotation.AgRelationship; + +public class P2 { + + private String name; + private P1 p1; + + @AgAttribute + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @AgRelationship + public P1 getP1() { + return p1; + } + + public void setP1(P1 p1) { + this.p1 = p1; + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java new file mode 100644 index 000000000..8205ae542 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java @@ -0,0 +1,18 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; + +public class P3 { + + private String name; + + @AgAttribute + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java new file mode 100644 index 000000000..873e458c9 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java @@ -0,0 +1,17 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgRelationship; + +public class P4 { + + private P3 p3; + + @AgRelationship + public P3 getP3() { + return p3; + } + + public void setP3(P3 p3) { + this.p3 = p3; + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java new file mode 100644 index 000000000..bdc91ea0a --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java @@ -0,0 +1,28 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; +import io.agrest.annotation.AgId; + +public class P6 { + + private String stringId; + private int intProp; + + @AgId + public String getStringId() { + return stringId; + } + + public void setStringId(String stringId) { + this.stringId = stringId; + } + + @AgAttribute + public int getIntProp() { + return intProp; + } + + public void setIntProp(int intProp) { + this.intProp = intProp; + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java new file mode 100644 index 000000000..d495eabb8 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java @@ -0,0 +1,28 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; +import io.agrest.annotation.AgId; + +public class P7 { + + int id; + String string; + + @AgId + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @AgAttribute + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java new file mode 100644 index 000000000..6265a48b2 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java @@ -0,0 +1,96 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; +import io.agrest.annotation.AgId; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class P8 { + public static final String STRING_SET = "stringSet"; + public static final String NUMBER_LIST = "numberList"; + public static final String WILDCARD_COLLECTION = "wildcardCollection"; + public static final String GENERIC_COLLECTION = "genericCollection"; + public static final String BOOLEANS = "booleans"; + public static final String DOUBLES = "doubles"; + public static final String CHARACTERS = "characters"; + + + private int id; + private Set stringSet; + private List numberList; + private Collection wildcardCollection; + private Collection booleans; + private Collection doubles; + private Collection characters; + + @AgId + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @AgAttribute + public Set getStringSet() { + return stringSet; + } + + public void setStringSet(Set stringSet) { + this.stringSet = stringSet; + } + + @AgAttribute + public List getNumberList() { + return numberList; + } + + public void setNumberList(List numberList) { + this.numberList = numberList; + } + + @AgAttribute + public Collection getWildcardCollection() { + return wildcardCollection; + } + + public void setWildcardCollection(Collection wildcardCollection) { + this.wildcardCollection = wildcardCollection; + } + + @AgAttribute + public Collection getGenericCollection() { + return Collections.emptyList(); + } + + @AgAttribute + public Collection getBooleans() { + return booleans; + } + + public void setBooleans(Collection booleans) { + this.booleans = booleans; + } + + @AgAttribute + public Collection getDoubles() { + return doubles; + } + + public void setDoubles(Collection doubles) { + this.doubles = doubles; + } + + @AgAttribute + public Collection getCharacters() { + return characters; + } + + public void setCharacters(Collection characters) { + this.characters = characters; + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java new file mode 100644 index 000000000..7b9e672e6 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java @@ -0,0 +1,42 @@ +package io.agrest.spring.starter.it.pojo.model; + +import io.agrest.annotation.AgAttribute; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +public class P9 { + + private String name; + + private OffsetDateTime created; + + private LocalDateTime createdLocal; + + @AgAttribute + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @AgAttribute + public OffsetDateTime getCreated() { + return created; + } + + public void setCreated(OffsetDateTime created) { + this.created = created; + } + + @AgAttribute + public LocalDateTime getCreatedLocal() { + return createdLocal; + } + + public void setCreatedLocal(LocalDateTime createdLocal) { + this.createdLocal = createdLocal; + } +} \ No newline at end of file diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java new file mode 100644 index 000000000..f0471e128 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java @@ -0,0 +1,83 @@ +package io.agrest.spring.starter.it.pojo.runtime; + +import io.agrest.PathConstants; +import io.agrest.meta.AgAttribute; +import io.agrest.meta.AgEntity; +import io.agrest.processor.Processor; +import io.agrest.processor.ProcessorOutcome; +import io.agrest.protocol.Sort; +import io.agrest.reader.DataReader; +import io.agrest.runtime.processor.select.SelectContext; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.apache.cayenne.di.Inject; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PojoFetchStage implements Processor> { + + private final PojoStore db; + + public PojoFetchStage(@Inject PojoStore db) { + this.db = db; + } + + @Override + public ProcessorOutcome execute(SelectContext context) { + findObjects(context); + return ProcessorOutcome.CONTINUE; + } + + void findObjects(SelectContext context) { + + Map typeBucket = db.bucket(context.getType()); + if (context.isById()) { + Map idMap = context.getId().asMap(context.getEntity().getAgEntity()); + Object id = idMap.size() > 1 ? idMap : idMap.values().iterator().next(); + T object = typeBucket.get(id); + // stores as a result into ResourceEntity + context.getEntity().setData(object != null ? List.of(object) : List.of()); + return; + } + + // clone the list and then filter/sort it as needed + List list = new ArrayList<>(typeBucket.values()); + + for (Sort s : context.getEntity().getOrderings()) { + list.sort(toComparator(context.getEntity().getAgEntity(), s)); + } + + // stores as a result into ResourceEntity + context.getEntity().setData(list); + } + + private Comparator toComparator(AgEntity entity, Sort s) { + + Function keyReader; + AgAttribute attribute = entity.getAttribute(s.getPath()); + if (attribute != null) { + keyReader = t -> (Comparable) attribute.getDataReader().read(t); + } else if (PathConstants.ID_PK_ATTRIBUTE.equals(s.getPath())) { + keyReader = t -> readId(t, entity.getIdReader()); + } else { + throw new RuntimeException("Can't find sort property reader for '" + s.getPath() + "'"); + } + + return (Comparator) Comparator.comparing(keyReader); + } + + private Comparable readId(Object object, DataReader idReader) { + if (object == null) { + return null; + } + + Map id = (Map) idReader.read(object); + assertEquals(1, id.size(), () -> "Unexpected id size " + id.size() + " for object " + object.getClass()); + return (Comparable) id.values().iterator().next(); + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java new file mode 100644 index 000000000..df919b393 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java @@ -0,0 +1,51 @@ +package io.agrest.spring.starter.it.pojo.runtime; + +import io.agrest.SelectStage; +import io.agrest.processor.Processor; +import io.agrest.processor.ProcessorOutcome; +import io.agrest.runtime.AgExceptionMappers; +import io.agrest.runtime.processor.select.SelectContext; +import io.agrest.runtime.processor.select.SelectProcessorFactory; +import io.agrest.runtime.processor.select.stage.SelectApplyServerParamsStage; +import io.agrest.runtime.processor.select.stage.SelectCreateResourceEntityStage; +import io.agrest.runtime.processor.select.stage.SelectEncoderInstallStage; +import io.agrest.runtime.processor.select.stage.SelectStartStage; + +import java.util.EnumMap; + +import org.apache.cayenne.di.DIRuntimeException; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.di.Provider; + +public class PojoSelectProcessorFactoryProvider implements Provider { + + private final AgExceptionMappers exceptionMappers; + private final EnumMap>> stages; + + public PojoSelectProcessorFactoryProvider( + @Inject SelectStartStage startStage, + @Inject SelectCreateResourceEntityStage createResourceEntityStage, + @Inject SelectApplyServerParamsStage applyServerParamsStage, + @Inject PojoFetchStage pojoFetchStage, + @Inject SelectEncoderInstallStage encoderStage, + + @Inject AgExceptionMappers exceptionMappers) { + + this.exceptionMappers = exceptionMappers; + + stages = new EnumMap<>(SelectStage.class); + stages.put(SelectStage.START, startStage); + stages.put(SelectStage.CREATE_ENTITY, createResourceEntityStage); + stages.put(SelectStage.APPLY_SERVER_PARAMS, applyServerParamsStage); + stages.put(SelectStage.ASSEMBLE_QUERY, c -> ProcessorOutcome.CONTINUE); + stages.put(SelectStage.FETCH_DATA, pojoFetchStage); + stages.put(SelectStage.ENCODE, encoderStage); + } + + @Override + public SelectProcessorFactory get() throws DIRuntimeException { + return new SelectProcessorFactory(stages, exceptionMappers); + } + + +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java new file mode 100644 index 000000000..832149080 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java @@ -0,0 +1,26 @@ +package io.agrest.spring.starter.it.pojo.runtime; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A key/value "database" that stores objects by type and id. + */ +public class PojoStore { + + private ConcurrentMap, Map> map; + + public PojoStore() { + this.map = new ConcurrentHashMap<>(); + } + + public void clear() { + map.clear(); + } + + @SuppressWarnings("unchecked") + public Map bucket(Class type) { + return (Map) map.computeIfAbsent(type, t -> new ConcurrentHashMap<>()); + } +} diff --git a/agrest-spring-boot-starter/src/test/resources/application-test.properties b/agrest-spring-boot-starter/src/test/resources/application-test.properties new file mode 100644 index 000000000..5899b9296 --- /dev/null +++ b/agrest-spring-boot-starter/src/test/resources/application-test.properties @@ -0,0 +1 @@ +agrest.enabled = true \ No newline at end of file diff --git a/pom.xml b/pom.xml index b2e720dd0..4e8371754 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ agrest-jaxrs2-openapi agrest-cayenne agrest-bom + agrest-spring-boot-starter From 5eeb3e182aef5f845ad31546b021f0e4ef8c3d49 Mon Sep 17 00:00:00 2001 From: Max Novik Date: Wed, 2 Nov 2022 17:34:59 +0300 Subject: [PATCH 2/4] Integration tests. --- agrest-spring-boot-starter/pom.xml | 23 +- .../main/resources/META-INF/spring.factories | 2 +- agrest-spring-it/pom.xml | 60 ++++ .../java/io/agrest/spring/it/ApiError.java | 17 ++ .../main/java/io/agrest/spring/it/ItApp.java | 64 +++++ .../RestResponseEntityExceptionHandler.java | 27 ++ .../ExceptionMappersController.java | 70 +++++ .../controllers/GetExceptionController.java | 57 ++++ .../it/controllers/GetPojoController.java | 74 +++++ .../GetPojoDataResponseController.java | 70 +++++ .../GetSimpleResponseController.java | 26 ++ .../spring/it/controllers/ParamUtils.java | 14 + .../io/agrest/spring}/it/pojo/model/P1.java | 2 +- .../io/agrest/spring}/it/pojo/model/P10.java | 2 +- .../io/agrest/spring}/it/pojo/model/P2.java | 2 +- .../io/agrest/spring}/it/pojo/model/P3.java | 2 +- .../io/agrest/spring}/it/pojo/model/P4.java | 2 +- .../io/agrest/spring}/it/pojo/model/P6.java | 2 +- .../io/agrest/spring}/it/pojo/model/P7.java | 2 +- .../io/agrest/spring}/it/pojo/model/P8.java | 2 +- .../io/agrest/spring}/it/pojo/model/P9.java | 2 +- .../it/pojo/runtime/PojoFetchStage.java | 5 +- .../PojoSelectProcessorFactoryProvider.java | 2 +- .../spring}/it/pojo/runtime/PojoStore.java | 4 +- .../src/main/resources/application.properties | 1 + .../ExceptionMappersControllerTest.java | 39 +++ .../GetExceptionControllerTest.java | 55 ++++ .../it/controllers/GetPojoControllerTest.java | 267 ++++++++++++++++++ .../GetPojoDataResponseControllerTest.java | 30 ++ .../GetSimpleResponseControllerTest.java | 38 +++ pom.xml | 6 +- 31 files changed, 945 insertions(+), 24 deletions(-) create mode 100644 agrest-spring-it/pom.xml create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ExceptionMappersController.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetExceptionController.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoController.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoDataResponseController.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetSimpleResponseController.java create mode 100644 agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ParamUtils.java rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P1.java (81%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P10.java (94%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P2.java (88%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P3.java (81%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P4.java (79%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P6.java (89%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P7.java (89%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P8.java (98%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/model/P9.java (93%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/runtime/PojoFetchStage.java (92%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java (97%) rename {agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter => agrest-spring-it/src/main/java/io/agrest/spring}/it/pojo/runtime/PojoStore.java (82%) create mode 100644 agrest-spring-it/src/main/resources/application.properties create mode 100644 agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/ExceptionMappersControllerTest.java create mode 100644 agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetExceptionControllerTest.java create mode 100644 agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoControllerTest.java create mode 100644 agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoDataResponseControllerTest.java create mode 100644 agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetSimpleResponseControllerTest.java diff --git a/agrest-spring-boot-starter/pom.xml b/agrest-spring-boot-starter/pom.xml index eb1d2efe3..e822f09b4 100644 --- a/agrest-spring-boot-starter/pom.xml +++ b/agrest-spring-boot-starter/pom.xml @@ -10,10 +10,12 @@ 4.0.0 agrest-spring-boot-starter + agrest-spring-boot-starter: Spring Boot starter for Agrest + Spring Boot starter for Agrest - 17 - 17 + 11 + 11 @@ -57,4 +59,21 @@ + + + + + org.apache.maven.plugins + maven-jar-plugin + + + build-tests + + test-jar + + + + + + \ No newline at end of file diff --git a/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories b/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories index b9aeca4cf..54c63a597 100644 --- a/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=hornetq.autoconfigure.AgrestAutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=io/agrest.spring.starter.AgrestAutoConfiguration diff --git a/agrest-spring-it/pom.xml b/agrest-spring-it/pom.xml new file mode 100644 index 000000000..a0fc061cb --- /dev/null +++ b/agrest-spring-it/pom.xml @@ -0,0 +1,60 @@ + + + + agrest-parent + io.agrest + 5.0.M8-SNAPSHOT + + 4.0.0 + + agrest-spring-it + + + 11 + 11 + + + + + + org.springframework.boot + spring-boot-dependencies + 2.7.5 + pom + import + + + + + + io.agrest + agrest-engine + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-starter-web + + + io.agrest + agrest-spring-boot-starter + 5.0.M8-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-test + + + + \ No newline at end of file diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java new file mode 100644 index 000000000..7299df04c --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java @@ -0,0 +1,17 @@ +package io.agrest.spring.it; + +import org.springframework.http.HttpStatus; + +public class ApiError { + + private final String message; + + public ApiError(String message) { + super(); + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java new file mode 100644 index 000000000..d8edde2e2 --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java @@ -0,0 +1,64 @@ +package io.agrest.spring.it; + +import io.agrest.AgException; +import io.agrest.runtime.AgRuntime; +import io.agrest.runtime.AgRuntimeBuilder; +import io.agrest.runtime.processor.delete.DeleteProcessorFactory; +import io.agrest.runtime.processor.select.SelectProcessorFactory; +import io.agrest.runtime.processor.unrelate.UnrelateProcessorFactory; +import io.agrest.runtime.processor.update.CreateOrUpdateProcessorFactory; +import io.agrest.runtime.processor.update.CreateProcessorFactory; +import io.agrest.runtime.processor.update.IdempotentCreateOrUpdateProcessorFactory; +import io.agrest.runtime.processor.update.IdempotentFullSyncProcessorFactory; +import io.agrest.runtime.processor.update.UpdateProcessorFactory; +import io.agrest.spi.AgExceptionMapper; +import io.agrest.spring.it.controllers.ExceptionMappersController.TestAgExceptionMapper; +import io.agrest.spring.it.controllers.ExceptionMappersController.TestException; +import io.agrest.spring.it.controllers.ExceptionMappersController.TestExceptionMapper; +import io.agrest.spring.it.pojo.runtime.PojoFetchStage; +import io.agrest.spring.it.pojo.runtime.PojoSelectProcessorFactoryProvider; +import io.agrest.spring.it.pojo.runtime.PojoStore; + +import org.apache.cayenne.di.Module; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import static org.mockito.Mockito.mock; + +@SpringBootApplication +public class ItApp { + + @Bean + public AgRuntime agRuntime() { + return AgRuntime.builder() + .module(this::configureAg) + .module(exceptionsModule()) + .build(); + } + + private static Module exceptionsModule() { + return cb -> cb + .bindMap(AgExceptionMapper.class) + .put(AgException.class.getName(), TestAgExceptionMapper.class) + .put(TestException.class.getName(), TestExceptionMapper.class); + } + + @Bean + PojoStore pojoStore() { + return new PojoStore(); + } + + private void configureAg(org.apache.cayenne.di.Binder agBinder) { + agBinder.bind(SelectProcessorFactory.class).toProvider(PojoSelectProcessorFactoryProvider.class); + agBinder.bind(DeleteProcessorFactory.class).toInstance(mock(DeleteProcessorFactory.class)); + agBinder.bind(CreateProcessorFactory.class).toInstance(mock(CreateProcessorFactory.class)); + agBinder.bind(UpdateProcessorFactory.class).toInstance(mock(UpdateProcessorFactory.class)); + agBinder.bind(CreateOrUpdateProcessorFactory.class).toInstance(mock(CreateOrUpdateProcessorFactory.class)); + agBinder.bind(IdempotentCreateOrUpdateProcessorFactory.class).toInstance(mock(IdempotentCreateOrUpdateProcessorFactory.class)); + agBinder.bind(IdempotentFullSyncProcessorFactory.class).toInstance(mock(IdempotentFullSyncProcessorFactory.class)); + agBinder.bind(UnrelateProcessorFactory.class).toInstance(mock(UnrelateProcessorFactory.class)); + agBinder.bind(PojoFetchStage.class).to(PojoFetchStage.class); + agBinder.bind(PojoStore.class).toInstance(pojoStore()); + } + +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java new file mode 100644 index 000000000..701c61507 --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java @@ -0,0 +1,27 @@ +package io.agrest.spring.it; + +import io.agrest.AgException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(value = {AgException.class}) + protected ResponseEntity handleAgException(AgException ex, WebRequest request) { + Throwable cause = ex.getCause() != null && ex.getCause() != ex ? ex.getCause() : null; + if (cause instanceof ResponseStatusException) { + var rse = (ResponseStatusException) cause; + return new ResponseEntity<>(rse.getLocalizedMessage(), rse.getStatus()); + } + + var apiError = new ApiError(ex.getMessage()); + return new ResponseEntity<>(apiError, HttpStatus.resolve(ex.getStatus())); + } +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ExceptionMappersController.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ExceptionMappersController.java new file mode 100644 index 000000000..217f7b574 --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ExceptionMappersController.java @@ -0,0 +1,70 @@ +package io.agrest.spring.it.controllers; + +import io.agrest.AgException; +import io.agrest.DataResponse; +import io.agrest.SelectStage; +import io.agrest.runtime.AgRuntime; +import io.agrest.spi.AgExceptionMapper; +import io.agrest.spring.it.pojo.model.P1; +import io.agrest.spring.it.pojo.model.P2; + +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping( + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) +public class ExceptionMappersController { + + private final AgRuntime agRuntime; + + public ExceptionMappersController(AgRuntime agRuntime) { + this.agRuntime = agRuntime; + } + + @GetMapping(path = "agexception") + public DataResponse agException() { + return agRuntime.select(P1.class) + .stage(SelectStage.APPLY_SERVER_PARAMS, c -> { + throw AgException.forbidden("_ag_exception_"); + }) + .get(); + } + + @GetMapping(path = "testexception") + public DataResponse testException() { + return agRuntime.select(P2.class) + .stage(SelectStage.APPLY_SERVER_PARAMS, c -> { + throw new TestException("_test_exception_"); + }) + .get(); + } + + public static class TestAgExceptionMapper implements AgExceptionMapper { + + @Override + public AgException toAgException(AgException e) { + return AgException.internalServerError(e, "_ag_%s", e.getMessage()); + } + } + + public static class TestExceptionMapper implements AgExceptionMapper { + + @Override + public AgException toAgException(TestException e) { + return AgException.internalServerError(e, "_test_%s", e.getMessage()); + } + } + + public static class TestException extends RuntimeException { + public TestException(String message) { + super(message); + } + } +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetExceptionController.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetExceptionController.java new file mode 100644 index 000000000..b91729ffe --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetExceptionController.java @@ -0,0 +1,57 @@ +package io.agrest.spring.it.controllers; + +import io.agrest.AgException; +import io.agrest.DataResponse; +import io.agrest.SelectStage; +import io.agrest.runtime.AgRuntime; +import io.agrest.spring.it.pojo.model.P1; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +@RequestMapping( + path = "nodata", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) +public class GetExceptionController { + + private final AgRuntime agRuntime; + + public GetExceptionController(AgRuntime agRuntime) {this.agRuntime = agRuntime;} + + + @GetMapping + public ResponseEntity get() { + throw AgException.notFound("Request failed"); + } + + @GetMapping(path = "th") + public ResponseEntity getTh() { + try { + throw new Throwable("Dummy"); + } catch (Throwable th) { + throw AgException.internalServerError(th, "Request failed with th"); + } + } + + @GetMapping(path = "rse") + public ResponseEntity getRse() { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Was forbidden"); + } + + @GetMapping(path = "rse_inside_ag") + public DataResponse getWaeInsideAg() { + return agRuntime.select(P1.class) + .stage(SelectStage.START, c -> { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Was forbidden inside pipeline"); + }).get(); + } + + +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoController.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoController.java new file mode 100644 index 000000000..a93692b51 --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoController.java @@ -0,0 +1,74 @@ +package io.agrest.spring.it.controllers; + +import io.agrest.DataResponse; +import io.agrest.runtime.AgRuntime; +import io.agrest.spring.it.pojo.model.P1; +import io.agrest.spring.it.pojo.model.P10; +import io.agrest.spring.it.pojo.model.P4; +import io.agrest.spring.it.pojo.model.P6; +import io.agrest.spring.it.pojo.model.P8; +import io.agrest.spring.it.pojo.model.P9; + +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static io.agrest.spring.it.controllers.ParamUtils.convertParams; + +@RestController +@RequestMapping( + path = "pojo", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) +public class GetPojoController { + + private final AgRuntime agRuntime; + + public GetPojoController(AgRuntime agRuntime) {this.agRuntime = agRuntime;} + + @GetMapping(path = "p1_empty") + public DataResponse p1Empty() { + return agRuntime.select(P1.class).getEmpty(); + } + + @GetMapping(path = "p1") + public DataResponse p1All(@RequestParam Map allParams) { + return agRuntime.select(P1.class).clientParams(convertParams(allParams)).get(); + } + + @GetMapping(path = "p4") + public DataResponse p4All(@RequestParam Map allParams) { + return agRuntime.select(P4.class).clientParams(convertParams(allParams)).get(); + } + + @GetMapping(path = "p6") + public DataResponse p6All(@RequestParam Map allParams) { + return agRuntime.select(P6.class).clientParams(convertParams(allParams)).get(); + } + + @GetMapping(path = "p6/{id}") + public DataResponse p6ById(@PathVariable("id") String id, @RequestParam Map allParams) { + return agRuntime.select(P6.class).clientParams(convertParams(allParams)).byId(id).get(); + } + + @GetMapping(path = "p8/{id}") + public DataResponse p8ById(@PathVariable("id") int id, @RequestParam Map allParams) { + return agRuntime.select(P8.class).clientParams(convertParams(allParams)).byId(id).get(); + } + + @GetMapping(path = "p9") + public DataResponse p9All(@RequestParam Map allParams) { + return agRuntime.select(P9.class).clientParams(convertParams(allParams)).get(); + } + + @GetMapping(path = "p10/{id1}/{id2}") + public DataResponse p10ById(@PathVariable("id1") int id1, @PathVariable("id2") String id2, @RequestParam Map allParams) { + return agRuntime.select(P10.class).clientParams(convertParams(allParams)).byId(P10.id(id1, id2)).get(); + } +} + diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoDataResponseController.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoDataResponseController.java new file mode 100644 index 000000000..1911473f2 --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetPojoDataResponseController.java @@ -0,0 +1,70 @@ +package io.agrest.spring.it.controllers; + +import io.agrest.DataResponse; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping( + path = "data-response", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) +public class GetPojoDataResponseController { + + @GetMapping + public DataResponse xs() { + // generate response bypassing Agrest stack + return DataResponse.of(List.of( + new X(1, "two", new Y(5), List.of(new Y(6))), + new X(100, "two hundred", new Y(50), List.of(new Y(60))))).build(); + } + + + public static class X { + private final int a; + private final String b; + private final Y c; + private final List d; + + public X(int a, String b, Y c, List d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + public int getA() { + return a; + } + + public String getB() { + return b; + } + + public Y getC() { + return c; + } + + public List getD() { + return d; + } + } + + public static class Y { + private final int a; + + public Y(int a) { + this.a = a; + } + + public int getA() { + return a; + } + } + +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetSimpleResponseController.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetSimpleResponseController.java new file mode 100644 index 000000000..3771fc07d --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/GetSimpleResponseController.java @@ -0,0 +1,26 @@ +package io.agrest.spring.it.controllers; + +import io.agrest.SimpleResponse; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping( + path = "simple", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) +public class GetSimpleResponseController { + + @GetMapping + public SimpleResponse get() { + return SimpleResponse.of(200, "Hi!"); + } + + @GetMapping(path = "2") + public SimpleResponse get2() { + return SimpleResponse.of(200, "Hi2!"); + } +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ParamUtils.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ParamUtils.java new file mode 100644 index 000000000..42250ccb3 --- /dev/null +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/controllers/ParamUtils.java @@ -0,0 +1,14 @@ +package io.agrest.spring.it.controllers; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public class ParamUtils { + public static Map> convertParams(Map allParams) { + return allParams.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> List.of(e.getValue()))); + } +} diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P1.java similarity index 81% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P1.java index 1072aaff5..bb3cdb564 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P1.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P1.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P10.java similarity index 94% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P10.java index 8c4fd8e0a..f9c53b015 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P10.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P10.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; import io.agrest.annotation.AgId; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P2.java similarity index 88% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P2.java index 471475790..e648bd652 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P2.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P2.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; import io.agrest.annotation.AgRelationship; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P3.java similarity index 81% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P3.java index 8205ae542..eca012758 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P3.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P3.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P4.java similarity index 79% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P4.java index 873e458c9..e816ffcef 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P4.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P4.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgRelationship; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P6.java similarity index 89% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P6.java index bdc91ea0a..55bc70b74 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P6.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P6.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; import io.agrest.annotation.AgId; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P7.java similarity index 89% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P7.java index d495eabb8..2fd172f67 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P7.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P7.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; import io.agrest.annotation.AgId; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P8.java similarity index 98% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P8.java index 6265a48b2..66a5447f4 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P8.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P8.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; import io.agrest.annotation.AgId; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P9.java similarity index 93% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P9.java index 7b9e672e6..349b26377 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/model/P9.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/model/P9.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.model; +package io.agrest.spring.it.pojo.model; import io.agrest.annotation.AgAttribute; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoFetchStage.java similarity index 92% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoFetchStage.java index f0471e128..3679917ce 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoFetchStage.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoFetchStage.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.runtime; +package io.agrest.spring.it.pojo.runtime; import io.agrest.PathConstants; import io.agrest.meta.AgAttribute; @@ -17,8 +17,6 @@ import org.apache.cayenne.di.Inject; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class PojoFetchStage implements Processor> { private final PojoStore db; @@ -77,7 +75,6 @@ private Comparable readId(Object object, DataReader idReader) { } Map id = (Map) idReader.read(object); - assertEquals(1, id.size(), () -> "Unexpected id size " + id.size() + " for object " + object.getClass()); return (Comparable) id.values().iterator().next(); } } diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java similarity index 97% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java index df919b393..c956e4134 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoSelectProcessorFactoryProvider.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.runtime; +package io.agrest.spring.it.pojo.runtime; import io.agrest.SelectStage; import io.agrest.processor.Processor; diff --git a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoStore.java similarity index 82% rename from agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java rename to agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoStore.java index 832149080..ab66a72ff 100644 --- a/agrest-spring-boot-starter/src/test/java/io/agrest/spring/starter/it/pojo/runtime/PojoStore.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/pojo/runtime/PojoStore.java @@ -1,4 +1,4 @@ -package io.agrest.spring.starter.it.pojo.runtime; +package io.agrest.spring.it.pojo.runtime; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -9,7 +9,7 @@ */ public class PojoStore { - private ConcurrentMap, Map> map; + private final ConcurrentMap, Map> map; public PojoStore() { this.map = new ConcurrentHashMap<>(); diff --git a/agrest-spring-it/src/main/resources/application.properties b/agrest-spring-it/src/main/resources/application.properties new file mode 100644 index 000000000..5899b9296 --- /dev/null +++ b/agrest-spring-it/src/main/resources/application.properties @@ -0,0 +1 @@ +agrest.enabled = true \ No newline at end of file diff --git a/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/ExceptionMappersControllerTest.java b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/ExceptionMappersControllerTest.java new file mode 100644 index 000000000..614bc9011 --- /dev/null +++ b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/ExceptionMappersControllerTest.java @@ -0,0 +1,39 @@ +package io.agrest.spring.it.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@AutoConfigureMockMvc +class ExceptionMappersControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + public void testExceptionMapper() throws Exception { + + mvc.perform(get("/agexception") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isInternalServerError()) + .andExpect(content().json("{\"message\":\"_ag__ag_exception_\"}", true)); + + + mvc.perform(get("/testexception") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isInternalServerError()) + .andExpect(content().json("{\"message\":\"_test__test_exception_\"}", true)); + } +} \ No newline at end of file diff --git a/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetExceptionControllerTest.java b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetExceptionControllerTest.java new file mode 100644 index 000000000..919e3f2e3 --- /dev/null +++ b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetExceptionControllerTest.java @@ -0,0 +1,55 @@ +package io.agrest.spring.it.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class GetExceptionControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + public void testNoData() throws Exception { + mvc.perform(get("/nodata") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(content().json("{\"message\":\"Request failed\"}")); + } + + @Test + public void testNoData_WithThrowable() throws Exception { + mvc.perform(get("/nodata/th") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isInternalServerError()) + .andExpect(content().json("{\"message\":\"Request failed with th\"}")); + } + + @Test + public void testNoData_WithResponseStatusException() throws Exception { + mvc.perform(get("/nodata/rse") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + @Test + public void testNoData_WithResponseStatusException_InsidePipeline() throws Exception { + mvc.perform(get("/nodata/rse_inside_ag") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isForbidden()); + } +} \ No newline at end of file diff --git a/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoControllerTest.java b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoControllerTest.java new file mode 100644 index 000000000..e1c5c4666 --- /dev/null +++ b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoControllerTest.java @@ -0,0 +1,267 @@ +package io.agrest.spring.it.controllers; + +import io.agrest.spring.it.pojo.model.P1; +import io.agrest.spring.it.pojo.model.P10; +import io.agrest.spring.it.pojo.model.P3; +import io.agrest.spring.it.pojo.model.P4; +import io.agrest.spring.it.pojo.model.P6; +import io.agrest.spring.it.pojo.model.P8; +import io.agrest.spring.it.pojo.model.P9; +import io.agrest.spring.it.pojo.runtime.PojoStore; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class GetPojoControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private PojoStore pojoStore; + + @BeforeEach + void setUp() { + pojoStore.clear(); + } + + @Test + public void testById() throws Exception { + + P6 o1 = new P6(); + o1.setIntProp(15); + o1.setStringId("o1id"); + P6 o2 = new P6(); + o2.setIntProp(16); + o2.setStringId("o2id"); + + pojoStore.bucket(P6.class).put("o1id", o1); + pojoStore.bucket(P6.class).put("o2id", o2); + + mvc.perform(get("/pojo/p6/o2id") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"id\":\"o2id\",\"intProp\":16}],\"total\":1}")); + } + + @Test + public void testById_MultiKey() throws Exception { + + P10 o1 = new P10(); + o1.setId1(5); + o1.setId2("six"); + o1.setA1("seven"); + + P10 o2 = new P10(); + o2.setId1(8); + o2.setId2("nine"); + o2.setA1("ten"); + + pojoStore.bucket(P10.class).put(o1.id(), o1); + pojoStore.bucket(P10.class).put(o2.id(), o2); + + mvc.perform(get("/pojo/p10/5/six") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"id\":{\"id1\":5,\"id2\":\"six\"},\"a1\":\"seven\"}],\"total\":1}")); + } + + + @Test + public void testByQueryParam() throws Exception { + + P6 o1 = new P6(); + o1.setIntProp(15); + o1.setStringId("o1id"); + P6 o2 = new P6(); + o2.setIntProp(16); + o2.setStringId("o2id"); + + pojoStore.bucket(P6.class).put("o1id", o1); + pojoStore.bucket(P6.class).put("o2id", o2); + + mvc.perform(get("/pojo/p6") + .contentType(MediaType.APPLICATION_JSON) + .param("sort", "id")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"id\":\"o1id\",\"intProp\":15},{\"id\":\"o2id\",\"intProp\":16}],\"total\":2}")); + } + + @Test + public void testIncludeToOne() throws Exception { + + P3 o0 = new P3(); + o0.setName("xx3"); + + P4 o1 = new P4(); + o1.setP3(o0); + + pojoStore.bucket(P4.class).put("o1id", o1); + + mvc.perform(get("/pojo/p4") + .contentType(MediaType.APPLICATION_JSON) + .param("include", "p3")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"p3\":{\"name\":\"xx3\"}}],\"total\":1}")); + } + + @Test + public void testNoId() throws Exception { + + P1 o1 = new P1(); + o1.setName("n2"); + P1 o2 = new P1(); + o2.setName("n1"); + + pojoStore.bucket(P1.class).put("o1id", o1); + pojoStore.bucket(P1.class).put("o2id", o2); + + mvc.perform(get("/pojo/p1") + .contentType(MediaType.APPLICATION_JSON) + .param("sort", "name")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"name\":\"n1\"},{\"name\":\"n2\"}],\"total\":2}")); + + } + + @Test + public void testWithTime() throws Exception { + + P9 o9 = new P9(); + o9.setName("p9name1"); + LocalDateTime ldt = LocalDateTime.of(1999, 10, 2, 12, 54, 31); + o9.setCreated(OffsetDateTime.of(ldt, ZoneOffset.ofHours(3))); + o9.setCreatedLocal(ldt); + + pojoStore.bucket(P9.class).put("o9id", o9); + + mvc.perform(get("/pojo/p9") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"created\":\"1999-10-02T12:54:31+03:00\",\"createdLocal\":\"1999-10-02T12:54:31\",\"name\":\"p9name1\"}],\"total\":1}")); + } + + @Test + public void testMapBy() throws Exception { + + P1 o1 = new P1(); + o1.setName("n2"); + P1 o2 = new P1(); + o2.setName("n1"); + + pojoStore.bucket(P1.class).put("o1id", o1); + pojoStore.bucket(P1.class).put("o2id", o2); + + mvc.perform(get("/pojo/p1") + .contentType(MediaType.APPLICATION_JSON) + .param("mapBy", "name")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":{\"n1\":[{\"name\":\"n1\"}],\"n2\":[{\"name\":\"n2\"}]},\"total\":2}")); + } + + @Test + public void testCollectionAttributes() throws Exception { + + P8 o1 = new P8(); + o1.setId(1); + o1.setBooleans(List.of(true, false)); + o1.setCharacters(List.of('a', 'b', 'c')); + o1.setDoubles(List.of(1., 2.5, 3.5)); + o1.setStringSet(Set.of("abc")); + + List numbers = Arrays.asList((byte) 0, (short) 1, 2, 3L, 4.f, 5.); + o1.setNumberList(numbers); + o1.setWildcardCollection(numbers); + + pojoStore.bucket(P8.class).put(1, o1); + + mvc.perform(get("/pojo/p8/1") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\n" + + " \"data\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"booleans\": [\n" + + " true,\n" + + " false\n" + + " ],\n" + + " \"characters\": [\n" + + " \"a\",\n" + + " \"b\",\n" + + " \"c\"\n" + + " ],\n" + + " \"doubles\": [\n" + + " 1.0,\n" + + " 2.5,\n" + + " 3.5\n" + + " ],\n" + + " \"genericCollection\": [],\n" + + " \"numberList\": [\n" + + " 0,\n" + + " 1,\n" + + " 2,\n" + + " 3,\n" + + " 4.0,\n" + + " 5.0\n" + + " ],\n" + + " \"stringSet\": [\n" + + " \"abc\"\n" + + " ],\n" + + " \"wildcardCollection\": [\n" + + " 0,\n" + + " 1,\n" + + " 2,\n" + + " 3,\n" + + " 4.0,\n" + + " 5.0\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"total\": 1\n" + + "}")); + } + + @Test + public void testGetEmpty() throws Exception { + + P1 o1 = new P1(); + o1.setName("n2"); + + + pojoStore.bucket(P1.class).put("o1id", o1); + + mvc.perform(get("/pojo/p1_empty") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[],\"total\":0}")); + } +} \ No newline at end of file diff --git a/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoDataResponseControllerTest.java b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoDataResponseControllerTest.java new file mode 100644 index 000000000..18c817a63 --- /dev/null +++ b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetPojoDataResponseControllerTest.java @@ -0,0 +1,30 @@ +package io.agrest.spring.it.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class GetPojoDataResponseControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + public void test() throws Exception { + mvc.perform(get("/data-response") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"data\":[{\"a\":1,\"b\":\"two\",\"c\":{\"a\":5},\"d\":[{\"a\":6}]},{\"a\":100,\"b\":\"two hundred\",\"c\":{\"a\":50},\"d\":[{\"a\":60}]}],\"total\":2}")); + } +} \ No newline at end of file diff --git a/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetSimpleResponseControllerTest.java b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetSimpleResponseControllerTest.java new file mode 100644 index 000000000..7c2554907 --- /dev/null +++ b/agrest-spring-it/src/test/java/io/agrest/spring/it/controllers/GetSimpleResponseControllerTest.java @@ -0,0 +1,38 @@ +package io.agrest.spring.it.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class GetSimpleResponseControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + public void testWrite() throws Exception { + + mvc.perform(get("/simple") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"message\":\"Hi!\"}")); + + mvc.perform(get("/simple/2") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json("{\"message\":\"Hi2!\"}")); + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4e8371754..1236eddb6 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ agrest-cayenne agrest-bom agrest-spring-boot-starter + agrest-spring-it @@ -156,11 +157,6 @@ Importing individual Bootique modules instead of a BOM, as BOM would be treated as a dependency by the downstream projects. --> - - io.bootique.jdbc - bootique-jdbc-junit5-derby - ${bootique.version} - io.bootique.cayenne bootique-cayenne42-junit5 From 22a96677bb676388b8c43be38d0077a61104e662 Mon Sep 17 00:00:00 2001 From: Max Novik Date: Fri, 4 Nov 2022 15:08:05 +0300 Subject: [PATCH 3/4] Move exception handler to starter module. --- agrest-spring-boot-starter/pom.xml | 5 +++++ .../starter/AgrestAutoConfiguration.java | 7 +++++++ .../spring/starter/AgrestProperties.java | 18 ++++++++++++++++++ .../spring/starter/exceptions}/ApiError.java | 2 +- .../RestResponseEntityExceptionHandler.java | 5 ++++- .../main/java/io/agrest/spring/it/ItApp.java | 1 - .../src/main/resources/application.properties | 2 +- 7 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestProperties.java rename {agrest-spring-it/src/main/java/io/agrest/spring/it => agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions}/ApiError.java (84%) rename {agrest-spring-it/src/main/java/io/agrest/spring/it => agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions}/RestResponseEntityExceptionHandler.java (87%) diff --git a/agrest-spring-boot-starter/pom.xml b/agrest-spring-boot-starter/pom.xml index e822f09b4..93b95e423 100644 --- a/agrest-spring-boot-starter/pom.xml +++ b/agrest-spring-boot-starter/pom.xml @@ -41,6 +41,11 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-configuration-processor + true + org.springframework.boot spring-boot-starter-test diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java index a4e890e5a..95a23f499 100644 --- a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java @@ -4,6 +4,7 @@ import io.agrest.runtime.AgRuntime; import io.agrest.runtime.jackson.IJacksonService; import io.agrest.runtime.protocol.IEntityUpdateParser; +import io.agrest.spring.starter.exceptions.RestResponseEntityExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -11,6 +12,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.ControllerAdvice; @Configuration @ConditionalOnWebApplication @@ -60,4 +62,9 @@ public EntityUpdateCollectionReader entityUpdateCollectionReader(EntityUpdateRea public SimpleResponseWriter simpleResponseWriter(IJacksonService jacksonService) { return new SimpleResponseWriter(jacksonService); } + + @Bean + public RestResponseEntityExceptionHandler exceptionHandler() { + return new RestResponseEntityExceptionHandler(); + } } diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestProperties.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestProperties.java new file mode 100644 index 000000000..82ada3579 --- /dev/null +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestProperties.java @@ -0,0 +1,18 @@ +package io.agrest.spring.starter; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "agrest") +public class AgrestProperties { + + private boolean enabled = false; + + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java similarity index 84% rename from agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java rename to agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java index 7299df04c..096aa33e1 100644 --- a/agrest-spring-it/src/main/java/io/agrest/spring/it/ApiError.java +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java @@ -1,4 +1,4 @@ -package io.agrest.spring.it; +package io.agrest.spring.starter.exceptions; import org.springframework.http.HttpStatus; diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java similarity index 87% rename from agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java rename to agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java index 701c61507..e900f0767 100644 --- a/agrest-spring-it/src/main/java/io/agrest/spring/it/RestResponseEntityExceptionHandler.java +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java @@ -1,7 +1,8 @@ -package io.agrest.spring.it; +package io.agrest.spring.starter.exceptions; import io.agrest.AgException; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -10,6 +11,8 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import static org.springframework.core.Ordered.LOWEST_PRECEDENCE; + @ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { diff --git a/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java b/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java index d8edde2e2..ab67fb3ce 100644 --- a/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java +++ b/agrest-spring-it/src/main/java/io/agrest/spring/it/ItApp.java @@ -2,7 +2,6 @@ import io.agrest.AgException; import io.agrest.runtime.AgRuntime; -import io.agrest.runtime.AgRuntimeBuilder; import io.agrest.runtime.processor.delete.DeleteProcessorFactory; import io.agrest.runtime.processor.select.SelectProcessorFactory; import io.agrest.runtime.processor.unrelate.UnrelateProcessorFactory; diff --git a/agrest-spring-it/src/main/resources/application.properties b/agrest-spring-it/src/main/resources/application.properties index 5899b9296..8b7b4d330 100644 --- a/agrest-spring-it/src/main/resources/application.properties +++ b/agrest-spring-it/src/main/resources/application.properties @@ -1 +1 @@ -agrest.enabled = true \ No newline at end of file +agrest.enabled=true \ No newline at end of file From 8f256085465a5151ecd5eeb74990220ef8bba54a Mon Sep 17 00:00:00 2001 From: Max Novik Date: Thu, 10 Nov 2022 18:00:49 +0300 Subject: [PATCH 4/4] Polishing. --- agrest-spring-boot-starter/pom.xml | 8 ++------ .../io/agrest/spring/starter/AgrestAutoConfiguration.java | 1 - .../io/agrest/spring/starter/exceptions/ApiError.java | 2 -- .../exceptions/RestResponseEntityExceptionHandler.java | 3 --- .../src/main/resources/META-INF/spring.factories | 2 +- pom.xml | 1 + 6 files changed, 4 insertions(+), 13 deletions(-) diff --git a/agrest-spring-boot-starter/pom.xml b/agrest-spring-boot-starter/pom.xml index 93b95e423..6c9ee06a2 100644 --- a/agrest-spring-boot-starter/pom.xml +++ b/agrest-spring-boot-starter/pom.xml @@ -13,10 +13,6 @@ agrest-spring-boot-starter: Spring Boot starter for Agrest Spring Boot starter for Agrest - - 11 - 11 - io.agrest @@ -54,11 +50,11 @@ - + org.springframework.boot spring-boot-dependencies - 2.7.5 + ${spring.boot.version} pom import diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java index 95a23f499..3b1270681 100644 --- a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/AgrestAutoConfiguration.java @@ -12,7 +12,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.bind.annotation.ControllerAdvice; @Configuration @ConditionalOnWebApplication diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java index 096aa33e1..1fbbea8f7 100644 --- a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/ApiError.java @@ -1,7 +1,5 @@ package io.agrest.spring.starter.exceptions; -import org.springframework.http.HttpStatus; - public class ApiError { private final String message; diff --git a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java index e900f0767..9503dffbb 100644 --- a/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java +++ b/agrest-spring-boot-starter/src/main/java/io/agrest/spring/starter/exceptions/RestResponseEntityExceptionHandler.java @@ -2,7 +2,6 @@ import io.agrest.AgException; -import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -11,8 +10,6 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import static org.springframework.core.Ordered.LOWEST_PRECEDENCE; - @ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { diff --git a/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories b/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories index 54c63a597..2df42551c 100644 --- a/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/agrest-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=io/agrest.spring.starter.AgrestAutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.agrest.spring.starter.AgrestAutoConfiguration diff --git a/pom.xml b/pom.xml index 1236eddb6..639e7ea21 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ 2.1.6 3.0.0 2.13.4.2 + 2.7.5 3.0.M1-SNAPSHOT