├── src
├── main
│ ├── resources
│ │ └── META-INF
│ │ │ └── spring.factories
│ └── java
│ │ └── eu
│ │ └── codearte
│ │ └── resteeth
│ │ ├── annotation
│ │ ├── LogScope.java
│ │ ├── StaticHeaders.java
│ │ ├── StaticHeader.java
│ │ └── RestClient.java
│ │ ├── endpoint
│ │ ├── EndpointProvider.java
│ │ ├── FixedEndpoint.java
│ │ ├── RoundRobinEndpoint.java
│ │ └── Endpoints.java
│ │ ├── core
│ │ ├── IncorrectRequestMapping.java
│ │ ├── MethodAnnotationMetadata.java
│ │ ├── ParameterMetadata.java
│ │ ├── RestInvocationInterceptor.java
│ │ ├── MethodMetadata.java
│ │ ├── RestInvocation.java
│ │ ├── ResteethAnnotationMetadata.java
│ │ ├── BeanProxyCreator.java
│ │ ├── MetadataExtractor.java
│ │ └── RestTemplateInvoker.java
│ │ ├── config
│ │ ├── ResteethConfiguration.java
│ │ ├── EnableResteeth.java
│ │ ├── ResteethDefinitionRegistrar.java
│ │ ├── ResteethBeanFactoryPostProcessor.java
│ │ ├── BeanResolver.java
│ │ └── ResteethAutowireCandidateResolverDelegate.java
│ │ ├── handlers
│ │ ├── RestInvocationHandler.java
│ │ ├── UserAgentHandler.java
│ │ ├── ProfilingHandler.java
│ │ ├── LoggingHandler.java
│ │ └── HeadersHandler.java
│ │ ├── autoconfigure
│ │ └── ResteethAutoConfiguration.java
│ │ └── util
│ │ └── SpringUtils.java
└── test
│ ├── groovy
│ └── eu
│ │ └── codearte
│ │ └── resteeth
│ │ ├── sample
│ │ └── RestClientInterface.groovy
│ │ ├── core
│ │ ├── sample
│ │ │ ├── AbstractUser.groovy
│ │ │ ├── User.groovy
│ │ │ ├── RestMethodsConfig.groovy
│ │ │ ├── RestClientHeaders.groovy
│ │ │ └── RestClientWithMethods.groovy
│ │ ├── RestInvocationTest.groovy
│ │ ├── BeanProxyCreatorTest.groovy
│ │ ├── MetadataExtractorTest.groovy
│ │ └── RestClientMethodInterceptorTest.groovy
│ │ ├── config
│ │ ├── attributes
│ │ │ ├── RestClientWithEndpoint.groovy
│ │ │ └── RestClientWithEndpoints.groovy
│ │ ├── sample
│ │ │ ├── RestInterfaceWithCustomQualifier.groovy
│ │ │ └── SampleEndpoint.groovy
│ │ ├── qualifier
│ │ │ └── RestInterfaceWithQualifier.groovy
│ │ ├── boot
│ │ │ ├── EchoServer.groovy
│ │ │ └── ResteethAutoConfigurationTest.groovy
│ │ ├── constructor
│ │ ├── ResteethBeanFactoryPostProcessorTest.groovy
│ │ └── EndpointProviderResolverTest.groovy
│ │ ├── TestObjectWrapper.groovy
│ │ ├── endpoint
│ │ ├── StubEndpointProvider.groovy
│ │ ├── FixedEndpointTest.groovy
│ │ └── RoundRobinEndpointTest.groovy
│ │ └── handlers
│ │ ├── UserAgentHandlerTest.groovy
│ │ └── HeadersHandlerTest.groovy
│ └── resources
│ └── logback-test.xml
├── RELEASING.md
├── .travis.yml
├── .gitignore
├── README.md
├── pom.xml
└── LICENSE
/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 | eu.codearte.resteeth.autoconfigure.ResteethAutoConfiguration
3 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | Releasing Resteeth
2 | ==================
3 |
4 | To perform the release you need to invoke:
5 | ```
6 | mvn release:clean release:prepare
7 | mvn release:perform
8 | ```
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | jdk:
4 | - oraclejdk7
5 | - oraclejdk8
6 |
7 | script: mvn verify
8 |
9 | after_success:
10 | - mvn clean test jacoco:report coveralls:report
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/sample/RestClientInterface.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.sample
2 | /**
3 | * @author Jakub Kubrynski
4 | */
5 | interface RestClientInterface {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/sample/AbstractUser.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core.sample
2 |
3 | /**
4 | * @author Marek Kaluzny
5 | */
6 | class AbstractUser {
7 | Integer id
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Package Files #
2 | *.jar
3 | *.war
4 | *.ear
5 |
6 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
7 | hs_err_pid*
8 |
9 | *.iml
10 | target
11 | .idea
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/sample/User.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core.sample
2 |
3 | /**
4 | * @author Jakub Kubrynski
5 | */
6 | class User extends AbstractUser {
7 | String name
8 | }
9 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/attributes/RestClientWithEndpoint.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.attributes
2 | /**
3 | * @author Jakub Kubrynski
4 | */
5 | interface RestClientWithEndpoint {
6 | }
7 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/attributes/RestClientWithEndpoints.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.attributes
2 | /**
3 | * @author Jakub Kubrynski
4 | */
5 | interface RestClientWithEndpoints {
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/annotation/LogScope.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.annotation;
2 |
3 | /**
4 | * @author Jakub Kubrynski
5 | */
6 | public enum LogScope {
7 |
8 | NONE,
9 | INVOCATION_ONLY,
10 | FULL
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/sample/RestInterfaceWithCustomQualifier.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.sample
2 | /**
3 | * @author Jakub Kubrynski
4 | */
5 | @SampleEndpoint
6 | interface RestInterfaceWithCustomQualifier {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/endpoint/EndpointProvider.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint;
2 |
3 | import java.net.URL;
4 |
5 | /**
6 | * @author Jakub Kubrynski
7 | */
8 | public interface EndpointProvider {
9 |
10 | URL getEndpoint();
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/TestObjectWrapper.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth
2 |
3 | /**
4 | * @author Jakub Kubrynski
5 | */
6 | class TestObjectWrapper {
7 | Object target
8 |
9 | TestObjectWrapper(Object target) {
10 | this.target = target
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/IncorrectRequestMapping.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | /**
4 | * @author Jakub Kubrynski
5 | */
6 | class IncorrectRequestMapping extends RuntimeException {
7 |
8 | public IncorrectRequestMapping(String message) {
9 | super(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/endpoint/StubEndpointProvider.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint
2 |
3 | /**
4 | * @author Jakub Kubrynski
5 | */
6 | class StubEndpointProvider implements EndpointProvider {
7 |
8 | @Override
9 | URL getEndpoint() {
10 | new URL("http://localhost")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/qualifier/RestInterfaceWithQualifier.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.qualifier
2 |
3 | import org.springframework.beans.factory.annotation.Qualifier
4 |
5 | /**
6 | * @author Jakub Kubrynski
7 | */
8 | @Qualifier("test")
9 | interface RestInterfaceWithQualifier {
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %d{HH:mm:ss.SSS} | %-5level | %thread | %logger{1} | %m%n%rEx
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/endpoint/FixedEndpoint.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint;
2 |
3 | import java.net.URL;
4 |
5 | /**
6 | * @author Jakub Kubrynski
7 | */
8 | class FixedEndpoint implements EndpointProvider {
9 |
10 | private final URL endpointUrl;
11 |
12 | FixedEndpoint(URL endpointUrl) {
13 | this.endpointUrl = endpointUrl;
14 | }
15 |
16 | @Override
17 | public URL getEndpoint() {
18 | return endpointUrl;
19 | }
20 |
21 | @Override
22 | public String toString() {
23 | return endpointUrl.toString();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/annotation/StaticHeaders.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.annotation;
2 |
3 | import java.lang.annotation.Documented;
4 | import java.lang.annotation.ElementType;
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.RetentionPolicy;
7 | import java.lang.annotation.Target;
8 |
9 | /**
10 | * @author Jakub Kubrynski
11 | */
12 | @Retention(RetentionPolicy.RUNTIME)
13 | @Target({ElementType.TYPE, ElementType.METHOD})
14 | @Documented
15 | public @interface StaticHeaders {
16 |
17 | StaticHeader[] value();
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/MethodAnnotationMetadata.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | /**
4 | * @author Jakub Kubrynski
5 | */
6 | public class MethodAnnotationMetadata {
7 |
8 | private final ResteethAnnotationMetadata resteethAnnotationMetadata;
9 |
10 | MethodAnnotationMetadata(ResteethAnnotationMetadata resteethAnnotationMetadata) {
11 | this.resteethAnnotationMetadata = resteethAnnotationMetadata;
12 | }
13 |
14 | public ResteethAnnotationMetadata getResteethAnnotationMetadata() {
15 | return resteethAnnotationMetadata;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/annotation/StaticHeader.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.annotation;
2 |
3 | import java.lang.annotation.Documented;
4 | import java.lang.annotation.ElementType;
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.RetentionPolicy;
7 | import java.lang.annotation.Target;
8 |
9 | /**
10 | * @author Jakub Kubrynski
11 | */
12 | @Retention(RetentionPolicy.RUNTIME)
13 | @Target({ElementType.TYPE, ElementType.METHOD})
14 | @Documented
15 | public @interface StaticHeader {
16 |
17 | String name();
18 |
19 | String value();
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/boot/EchoServer.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.boot
2 |
3 | import org.springframework.web.bind.annotation.PathVariable
4 | import org.springframework.web.bind.annotation.RequestMapping
5 | import org.springframework.web.bind.annotation.RequestMethod
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | /**
9 | * @author Mariusz Smykula
10 | */
11 | @RestController
12 | class EchoServer {
13 |
14 | @RequestMapping(value = "/echo/{message}", method = RequestMethod.GET)
15 | String echo(@PathVariable("message") String message) {
16 | message
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/sample/SampleEndpoint.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.sample
2 |
3 | import org.springframework.beans.factory.annotation.Qualifier
4 |
5 | import java.lang.annotation.ElementType
6 | import java.lang.annotation.Retention
7 | import java.lang.annotation.RetentionPolicy
8 | import java.lang.annotation.Target
9 |
10 | /**
11 | * @author Jakub Kubrynski
12 | */
13 | @Qualifier
14 | @Retention(RetentionPolicy.RUNTIME)
15 | @Target([ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE])
16 | public @interface SampleEndpoint {
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/endpoint/FixedEndpointTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint
2 |
3 | import spock.lang.Specification
4 |
5 | /**
6 | * @author Jakub Kubrynski
7 | */
8 | class FixedEndpointTest extends Specification {
9 |
10 | private static final URL ENDPOINT_URL = "http://localhost".toURL()
11 |
12 | def "should return fixed url"() {
13 | given:
14 | EndpointProvider sut = Endpoints.fixedEndpoint(ENDPOINT_URL)
15 | when:
16 | def endpoint1 = sut.getEndpoint()
17 | def endpoint2 = sut.getEndpoint()
18 | then:
19 | endpoint1 == ENDPOINT_URL
20 | endpoint2 == ENDPOINT_URL
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/sample/RestMethodsConfig.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core.sample
2 |
3 | import eu.codearte.resteeth.config.EnableResteeth
4 | import eu.codearte.resteeth.endpoint.EndpointProvider
5 | import eu.codearte.resteeth.endpoint.StubEndpointProvider
6 | import org.springframework.context.annotation.Bean
7 | import org.springframework.context.annotation.Configuration
8 |
9 | /**
10 | * @author Jakub Kubrynski
11 | */
12 | @EnableResteeth
13 | @Configuration
14 | class RestMethodsConfig {
15 |
16 | @Bean
17 | public EndpointProvider endpointProvider() {
18 | new StubEndpointProvider()
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/constructor/TestBean.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.constructor
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import eu.codearte.resteeth.sample.RestClientInterface
5 | import org.springframework.beans.factory.annotation.Autowired
6 | import org.springframework.stereotype.Component
7 |
8 | /**
9 | * @author Jakub Kubrynski
10 | */
11 | @Component
12 | class TestBean {
13 |
14 | final RestClientInterface restClientInterface
15 |
16 | @Autowired
17 | TestBean(@RestClient RestClientInterface restClientInterface) {
18 | this.restClientInterface = restClientInterface
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/config/ResteethConfiguration.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config;
2 |
3 | import eu.codearte.resteeth.handlers.HeadersHandler;
4 | import eu.codearte.resteeth.handlers.LoggingHandler;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 |
8 | /**
9 | * @author Jakub Kubrynski
10 | */
11 | @Configuration
12 | public class ResteethConfiguration {
13 |
14 | @Bean
15 | public HeadersHandler headersHandler() {
16 | return new HeadersHandler();
17 | }
18 |
19 | @Bean
20 | public LoggingHandler loggingHandler() {
21 | return new LoggingHandler();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/annotation/RestClient.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.annotation;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 |
5 | import java.lang.annotation.Documented;
6 | import java.lang.annotation.ElementType;
7 | import java.lang.annotation.Retention;
8 | import java.lang.annotation.RetentionPolicy;
9 | import java.lang.annotation.Target;
10 |
11 | /**
12 | * @author Jakub Kubrynski
13 | */
14 | @Retention(RetentionPolicy.RUNTIME)
15 | @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE, ElementType.METHOD})
16 | @Documented
17 | @Autowired
18 | public @interface RestClient {
19 |
20 | String[] endpoints() default {};
21 |
22 | LogScope[] loggingScope() default {};
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/config/EnableResteeth.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config;
2 |
3 | import eu.codearte.resteeth.annotation.LogScope;
4 | import org.springframework.context.annotation.Import;
5 |
6 | import java.lang.annotation.Documented;
7 | import java.lang.annotation.ElementType;
8 | import java.lang.annotation.Retention;
9 | import java.lang.annotation.RetentionPolicy;
10 | import java.lang.annotation.Target;
11 |
12 | /**
13 | * @author Jakub Kubrynski
14 | */
15 |
16 | @Retention(RetentionPolicy.RUNTIME)
17 | @Target(ElementType.TYPE)
18 | @Documented
19 | @Import({ResteethDefinitionRegistrar.class, ResteethConfiguration.class})
20 | public @interface EnableResteeth {
21 |
22 | LogScope loggingScope() default LogScope.INVOCATION_ONLY;
23 |
24 | }
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/endpoint/RoundRobinEndpoint.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint;
2 |
3 | import java.net.URL;
4 | import java.util.Arrays;
5 | import java.util.concurrent.atomic.AtomicInteger;
6 |
7 | /**
8 | * @author Jakub Kubrynski
9 | */
10 | class RoundRobinEndpoint implements EndpointProvider {
11 |
12 | private final URL[] endpointUrls;
13 | private final AtomicInteger counter = new AtomicInteger();
14 |
15 | RoundRobinEndpoint(URL[] endpointUrls) {
16 | this.endpointUrls = endpointUrls;
17 | }
18 |
19 | @Override
20 | public URL getEndpoint() {
21 | return endpointUrls[Math.abs(counter.getAndIncrement()) % endpointUrls.length];
22 | }
23 |
24 | @Override
25 | public String toString() {
26 | return Arrays.toString(endpointUrls);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/handlers/RestInvocationHandler.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers;
2 |
3 | import eu.codearte.resteeth.core.RestInvocation;
4 | import org.springframework.core.Ordered;
5 |
6 | /**
7 | * @author Tomasz Nurkiewicz
8 | */
9 | public interface RestInvocationHandler extends Ordered {
10 |
11 | Object proceed(RestInvocation invocation);
12 |
13 | /**
14 | * Higher priority (smaller value) will cause this handler to be executed earlier in the chain.
15 | * Low priority (bigger value) will push handler to the end.
16 | * Handler with lowest priority must handle request.
17 | *
18 | * @return Value controlling at what stage this handler will be called.
19 | * @see org.springframework.core.Ordered
20 | */
21 | @Override
22 | int getOrder();
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/endpoint/RoundRobinEndpointTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint
2 |
3 | import spock.lang.Specification
4 |
5 | /**
6 | * @author Jakub Kubrynski
7 | */
8 | class RoundRobinEndpointTest extends Specification {
9 |
10 | private static final URL ENDPOINT_1_URL = "http://localhost1".toURL()
11 | private static final URL ENDPOINT_2_URL = "http://localhost2".toURL()
12 |
13 | def "should return fixed url"() {
14 | given:
15 | EndpointProvider sut = Endpoints.roundRobinEndpoint(ENDPOINT_1_URL, ENDPOINT_2_URL)
16 | when:
17 | def endpoint1 = sut.getEndpoint()
18 | def endpoint2 = sut.getEndpoint()
19 | def endpoint3 = sut.getEndpoint()
20 | then:
21 | endpoint1 == ENDPOINT_1_URL
22 | endpoint2 == ENDPOINT_2_URL
23 | endpoint3 == ENDPOINT_1_URL
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/handlers/UserAgentHandler.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers;
2 |
3 | import eu.codearte.resteeth.core.RestInvocation;
4 | import org.springframework.core.Ordered;
5 | import org.springframework.http.HttpHeaders;
6 |
7 | /**
8 | * @author Tomasz Nurkiewicz
9 | */
10 | public class UserAgentHandler implements RestInvocationHandler {
11 |
12 | public final String userAgent;
13 |
14 | public UserAgentHandler() {
15 | this("Resteeth");
16 | }
17 |
18 | public UserAgentHandler(String userAgent) {
19 | this.userAgent = userAgent;
20 | }
21 |
22 | @Override
23 | public Object proceed(RestInvocation invocation) {
24 | invocation.getMetadata().getHttpHeaders().add(HttpHeaders.USER_AGENT, userAgent);
25 | return invocation.proceed();
26 | }
27 |
28 | @Override
29 | public int getOrder() {
30 | return Ordered.LOWEST_PRECEDENCE - 1000;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/handlers/ProfilingHandler.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers;
2 |
3 | import eu.codearte.resteeth.core.RestInvocation;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.core.Ordered;
7 |
8 | /**
9 | * @author Tomasz Nurkiewicz
10 | */
11 | public class ProfilingHandler implements RestInvocationHandler {
12 |
13 | private static final Logger log = LoggerFactory.getLogger(ProfilingHandler.class);
14 |
15 | @Override
16 | public Object proceed(RestInvocation invocation) {
17 | final long startTime = System.currentTimeMillis();
18 | final Object result = invocation.proceed();
19 | final long stopTime = System.currentTimeMillis();
20 | log.debug("Invocation of {} took {}ms", invocation.getMethod(), stopTime - startTime);
21 | return result;
22 | }
23 |
24 | @Override
25 | public int getOrder() {
26 | return Ordered.LOWEST_PRECEDENCE - 500;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/autoconfigure/ResteethAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.autoconfigure;
2 |
3 | import eu.codearte.resteeth.annotation.RestClient;
4 | import eu.codearte.resteeth.config.EnableResteeth;
5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
7 | import org.springframework.context.annotation.Configuration;
8 |
9 | /**
10 | * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} for Resteeth support.
11 | * Equivalent to enabling {@link eu.codearte.resteeth.config.EnableResteeth} in your configuration.
12 | * m
13 | * The configuration will not be activated if {@literal resteeth.enabled=false}.
14 | *
15 | * @author Mariusz Smykula
16 | */
17 | @Configuration
18 | @ConditionalOnClass(RestClient.class)
19 | @ConditionalOnExpression("${resteeth.enabled:true}")
20 | @EnableResteeth
21 | public class ResteethAutoConfiguration {
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/ParameterMetadata.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import java.util.Map;
4 |
5 | public class ParameterMetadata {
6 |
7 | private final Integer requestBodyIndex;
8 | private final Map urlVariables;
9 | private final Map queryParameters;
10 | private final Integer pojoQueryIndex;
11 |
12 | public ParameterMetadata(Integer requestBody, Map urlVariables,
13 | Map queryParameters, Integer pojoQueryParameter) {
14 | this.requestBodyIndex = requestBody;
15 | this.urlVariables = urlVariables;
16 | this.queryParameters = queryParameters;
17 | this.pojoQueryIndex = pojoQueryParameter;
18 | }
19 |
20 | public Integer getRequestBodyIndex() {
21 | return requestBodyIndex;
22 | }
23 |
24 | public Map getUrlVariables() {
25 | return urlVariables;
26 | }
27 |
28 | public Map getQueryParameters() {
29 | return queryParameters;
30 | }
31 |
32 | public Integer getPojoQueryIndex() {
33 | return pojoQueryIndex;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/sample/RestClientHeaders.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core.sample
2 |
3 | import eu.codearte.resteeth.annotation.StaticHeader
4 | import eu.codearte.resteeth.annotation.StaticHeaders
5 | import org.springframework.web.bind.annotation.PathVariable
6 | import org.springframework.web.bind.annotation.RequestHeader
7 | import org.springframework.web.bind.annotation.RequestMapping
8 | import org.springframework.web.bind.annotation.RequestMethod
9 |
10 | /**
11 | * @author Jakub Kubrynski
12 | */
13 | interface RestClientHeaders {
14 |
15 | @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
16 | User getUserDynamicHeader(@RequestHeader("testHeaderName") String headerValue, @PathVariable("id") Integer id);
17 |
18 | @StaticHeader(name = "testHeaderName", value = "testHeaderValue")
19 | @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
20 | User getUserStaticHeader(@PathVariable("id") Integer id);
21 |
22 | @StaticHeaders([
23 | @StaticHeader(name = "testHeaderName1", value = "testHeaderValue1"),
24 | @StaticHeader(name = "testHeaderName2", value = "testHeaderValue2")
25 | ])
26 | @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
27 | User getUserStaticHeaders(@PathVariable("id") Integer id);
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/handlers/LoggingHandler.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers;
2 |
3 | import eu.codearte.resteeth.annotation.LogScope;
4 | import eu.codearte.resteeth.core.RestInvocation;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | /**
9 | * @author Tomasz Nurkiewicz
10 | */
11 | public class LoggingHandler implements RestInvocationHandler {
12 |
13 | private static final Logger log = LoggerFactory.getLogger(LoggingHandler.class);
14 |
15 | @Override
16 | public Object proceed(RestInvocation invocation) {
17 | LogScope loggingScope = invocation.getMetadata().getMethodAnnotationMetadata().getResteethAnnotationMetadata().getLoggingScope();
18 | if (loggingScope.ordinal() >= LogScope.INVOCATION_ONLY.ordinal()) {
19 | log.debug("Invoked {}, calling {} {}, variables: {}",
20 | invocation.getMethod(),
21 | invocation.getMetadata().getRequestMethod(),
22 | invocation.getMetadata().getMethodUrl(),
23 | invocation.getMetadata().getParameterMetadata().getUrlVariables());
24 | }
25 | final Object result = invocation.proceed();
26 | if (loggingScope == LogScope.FULL) {
27 | log.trace("Response: {}", result);
28 | }
29 | return result;
30 | }
31 |
32 | @Override
33 | public int getOrder() {
34 | return 100;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/endpoint/Endpoints.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.endpoint;
2 |
3 | import java.net.MalformedURLException;
4 | import java.net.URL;
5 |
6 | /**
7 | * @author Jakub Kubrynski
8 | */
9 | public class Endpoints {
10 |
11 | private Endpoints() {
12 | }
13 |
14 | public static EndpointProvider fixedEndpoint(URL endpointUrl) {
15 | return new FixedEndpoint(endpointUrl);
16 | }
17 |
18 | public static EndpointProvider fixedEndpoint(String endpointUrl) {
19 | return new FixedEndpoint(toUrl(endpointUrl));
20 | }
21 |
22 | public static EndpointProvider roundRobinEndpoint(URL... endpointUrls) {
23 | return new RoundRobinEndpoint(endpointUrls);
24 | }
25 |
26 | public static EndpointProvider roundRobinEndpoint(String... endpointUrls) {
27 | return new RoundRobinEndpoint(toUrls(endpointUrls));
28 | }
29 |
30 | private static URL[] toUrls(String[] endpoints) {
31 | URL[] urls = new URL[endpoints.length];
32 | for (int i = 0; i < endpoints.length; i++) {
33 | urls[i] = toUrl(endpoints[i]);
34 | }
35 | return urls;
36 | }
37 |
38 | private static URL toUrl(String endpoint) {
39 | try {
40 | return new URL(endpoint);
41 | } catch (MalformedURLException e) {
42 | throw new IllegalArgumentException(endpoint + " is not valid URL", e);
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/RestInvocationInterceptor.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import eu.codearte.resteeth.handlers.RestInvocationHandler;
4 | import org.aopalliance.intercept.MethodInterceptor;
5 | import org.aopalliance.intercept.MethodInvocation;
6 | import org.springframework.aop.support.AopUtils;
7 | import org.springframework.http.HttpHeaders;
8 |
9 | import java.lang.reflect.Method;
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 | /**
14 | * @author Tomasz Nurkiewicz
15 | */
16 | class RestInvocationInterceptor implements MethodInterceptor {
17 |
18 | private final Map methodMetadataMap;
19 | private final List handlers;
20 |
21 | public RestInvocationInterceptor(Map methodMetadataMap, List handlers) {
22 | this.methodMetadataMap = methodMetadataMap;
23 | this.handlers = handlers;
24 | }
25 |
26 | @Override
27 | public Object invoke(MethodInvocation invocation) throws Throwable {
28 | if (AopUtils.isToStringMethod(invocation.getMethod())) {
29 | return "Proxy to " + handlers;
30 | }
31 | final RestInvocation restInvocation = new RestInvocation(
32 | invocation.getMethod(), invocation.getArguments(), methodMetadataMap.get(invocation.getMethod()),
33 | handlers, new HttpHeaders());
34 | return restInvocation.proceed();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/config/ResteethDefinitionRegistrar.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config;
2 |
3 | import org.springframework.beans.MutablePropertyValues;
4 | import org.springframework.beans.factory.config.ConstructorArgumentValues;
5 | import org.springframework.beans.factory.support.BeanDefinitionRegistry;
6 | import org.springframework.beans.factory.support.RootBeanDefinition;
7 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
8 | import org.springframework.core.annotation.AnnotationAttributes;
9 | import org.springframework.core.type.AnnotationMetadata;
10 |
11 | /**
12 | * @author Jakub Kubrynski
13 | */
14 | class ResteethDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
15 |
16 | private static final String ANNOTATION_NAME = EnableResteeth.class.getCanonicalName();
17 | private static final String BEAN_FACTORY_POST_PROCESSOR_NAME = "resteethBeanFactoryPostProcessor";
18 |
19 | @Override
20 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
21 | AnnotationAttributes enableResteethAttributes = new AnnotationAttributes(importingClassMetadata.getAnnotationAttributes(ANNOTATION_NAME));
22 |
23 | ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
24 | constructorArgumentValues.addGenericArgumentValue(enableResteethAttributes);
25 |
26 | registry.registerBeanDefinition(BEAN_FACTORY_POST_PROCESSOR_NAME, new RootBeanDefinition(ResteethBeanFactoryPostProcessor.class,
27 | constructorArgumentValues, new MutablePropertyValues()));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/MethodMetadata.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import org.springframework.http.HttpHeaders;
4 | import org.springframework.http.HttpMethod;
5 |
6 | /**
7 | * @author Jakub Kubrynski
8 | */
9 | public class MethodMetadata {
10 |
11 | private final String methodUrl;
12 | private final HttpMethod requestMethod;
13 | private final Class> returnType;
14 | private final HttpHeaders httpHeaders;
15 | private final MethodAnnotationMetadata methodAnnotationMetadata;
16 | private final ParameterMetadata parameterMetadata;
17 |
18 | public MethodMetadata(String methodUrl, HttpMethod requestMethod, Class> returnType, HttpHeaders httpHeaders,
19 | MethodAnnotationMetadata methodAnnotationMetadata, ParameterMetadata parameterMetadata) {
20 | this.methodUrl = methodUrl;
21 | this.requestMethod = requestMethod;
22 | this.returnType = returnType;
23 | this.parameterMetadata = parameterMetadata;
24 | this.httpHeaders = httpHeaders;
25 | this.methodAnnotationMetadata = methodAnnotationMetadata;
26 | }
27 |
28 | public String getMethodUrl() {
29 | return methodUrl;
30 | }
31 |
32 | public HttpMethod getRequestMethod() {
33 | return requestMethod;
34 | }
35 |
36 | public Class> getReturnType() {
37 | return returnType;
38 | }
39 |
40 | public ParameterMetadata getParameterMetadata() {
41 | return parameterMetadata;
42 | }
43 |
44 | public HttpHeaders getHttpHeaders() {
45 | return httpHeaders;
46 | }
47 |
48 | public MethodAnnotationMetadata getMethodAnnotationMetadata() {
49 | return methodAnnotationMetadata;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/util/SpringUtils.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.util;
2 |
3 | import eu.codearte.resteeth.handlers.RestInvocationHandler;
4 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
5 | import org.springframework.beans.factory.support.RootBeanDefinition;
6 |
7 | import java.lang.reflect.ParameterizedType;
8 | import java.lang.reflect.Type;
9 | import java.util.Collection;
10 | import java.util.LinkedList;
11 |
12 | /**
13 | * @author Jakub Kubrynski
14 | */
15 | public final class SpringUtils {
16 |
17 | private SpringUtils() {
18 | }
19 |
20 | public static Collection getBeansOfType(Class restInvocationHandlerClass,
21 | ConfigurableListableBeanFactory beanFactory) {
22 | LinkedList restInvocationHandlers = new LinkedList<>();
23 | for (String beanName : beanFactory.getBeanDefinitionNames()) {
24 | RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName);
25 |
26 | if (restInvocationHandlerClass.isAssignableFrom(beanDefinition.getTargetType())) {
27 | restInvocationHandlers.add((RestInvocationHandler) beanFactory.getBean(beanName));
28 | }
29 | }
30 | return restInvocationHandlers;
31 | }
32 |
33 | public static Class getGenericType(Type genericReturnType) {
34 | if (genericReturnType instanceof ParameterizedType) {
35 | return (Class) ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
36 | } else {
37 | return (Class) genericReturnType;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/RestInvocation.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import eu.codearte.resteeth.handlers.RestInvocationHandler;
4 |
5 | import java.lang.reflect.Method;
6 | import java.util.List;
7 |
8 | import org.springframework.http.HttpHeaders;
9 |
10 | /**
11 | * @author Tomasz Nurkiewicz
12 | */
13 | public class RestInvocation {
14 |
15 | private final MethodMetadata metadata;
16 | private final List handlers;
17 | private final Object[] arguments;
18 | private final Method method;
19 | private final HttpHeaders dynamicHeaders;
20 |
21 | public RestInvocation(
22 | Method method, Object[] arguments, MethodMetadata metadata, List handlers,
23 | HttpHeaders dynamicHeaders) {
24 | this.method = method;
25 | this.arguments = arguments;
26 | this.metadata = metadata;
27 | this.handlers = handlers;
28 | this.dynamicHeaders = dynamicHeaders;
29 | }
30 |
31 | public Method getMethod() {
32 | return method;
33 | }
34 |
35 | public Object[] getArguments() {
36 | return arguments;
37 | }
38 |
39 | public MethodMetadata getMetadata() {
40 | return metadata;
41 | }
42 |
43 | public HttpHeaders getDynamicHeaders() {
44 | return dynamicHeaders;
45 | }
46 |
47 | public Object proceed() {
48 | return currentHandler().proceed(nextHandlers());
49 | }
50 |
51 | private RestInvocationHandler currentHandler() {
52 | return handlers.get(0);
53 | }
54 |
55 | private RestInvocation nextHandlers() {
56 | final List withoutCurrent = handlers.subList(1, handlers.size());
57 | return new RestInvocation(method, arguments, metadata, withoutCurrent, dynamicHeaders);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/config/ResteethBeanFactoryPostProcessor.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.BeansException;
6 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
7 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
8 | import org.springframework.beans.factory.support.DefaultListableBeanFactory;
9 | import org.springframework.core.annotation.AnnotationAttributes;
10 |
11 | import java.lang.invoke.MethodHandles;
12 |
13 | /**
14 | * @author Jakub Kubrynski
15 | */
16 | class ResteethBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
17 |
18 | private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
19 | private final AnnotationAttributes enableResteethAttributes;
20 |
21 | public ResteethBeanFactoryPostProcessor(AnnotationAttributes enableResteethAttributes) {
22 | this.enableResteethAttributes = enableResteethAttributes;
23 | }
24 |
25 | @Override
26 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
27 | LOG.info("Resteeth is being registered in Spring BeanFactory...");
28 | DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
29 | ResteethAutowireCandidateResolverDelegate resteethAutowireCandidateResolverDelegate = new ResteethAutowireCandidateResolverDelegate(
30 | defaultListableBeanFactory.getAutowireCandidateResolver(), enableResteethAttributes);
31 | defaultListableBeanFactory.setAutowireCandidateResolver(resteethAutowireCandidateResolverDelegate);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/boot/ResteethAutoConfigurationTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config.boot
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration
5 | import org.springframework.boot.test.IntegrationTest
6 | import org.springframework.boot.test.SpringApplicationContextLoader
7 | import org.springframework.context.annotation.ComponentScan
8 | import org.springframework.context.annotation.Configuration
9 | import org.springframework.test.context.ContextConfiguration
10 | import org.springframework.test.context.web.WebAppConfiguration
11 | import org.springframework.web.bind.annotation.PathVariable
12 | import org.springframework.web.bind.annotation.RequestMapping
13 | import org.springframework.web.bind.annotation.RequestMethod
14 | import spock.lang.Specification
15 |
16 | /**
17 | * @author Mariusz Smykula
18 | * @author Jakub Kubrynski
19 | */
20 | @ContextConfiguration(classes = Application.class, loader = SpringApplicationContextLoader.class)
21 | @WebAppConfiguration
22 | @IntegrationTest
23 | class ResteethAutoConfigurationTest extends Specification {
24 |
25 | @Configuration
26 | @EnableAutoConfiguration
27 | @ComponentScan("eu.codearte.resteeth.config.boot")
28 | static class Application {
29 | }
30 |
31 | interface EchoClient {
32 | @RequestMapping(value = "/echo/{message}", method = RequestMethod.GET)
33 | String echo(@PathVariable("message") String message);
34 | }
35 |
36 | @RestClient(endpoints = "http://localhost:8080")
37 | EchoClient echoClient
38 |
39 | def "should send and receive response from EchoServer"() {
40 | when:
41 | def echo = echoClient.echo("foo")
42 | then:
43 | echo == "foo"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/RestInvocationTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core
2 |
3 | import eu.codearte.resteeth.handlers.RestInvocationHandler
4 | import org.springframework.http.HttpHeaders
5 | import spock.lang.Specification
6 |
7 | /**
8 | * @author Tomasz Nurkiewicz
9 | */
10 | class RestInvocationTest extends Specification {
11 |
12 | static final int SOME_RESULT = 17
13 |
14 | def 'should call first and only handler in stack'() {
15 | given:
16 | def handlerMock = Mock(RestInvocationHandler)
17 | def invocation = new RestInvocation(null, null, Mock(MethodMetadata), [handlerMock], new HttpHeaders())
18 |
19 | when:
20 | invocation.proceed()
21 |
22 | then:
23 | 1 * handlerMock.proceed(_)
24 | }
25 |
26 | def 'should call all handlers in order'() {
27 | given:
28 | def firstHandler = [proceed: {
29 | return it.proceed()
30 | }] as RestInvocationHandler
31 | def secondHandler = Mock(RestInvocationHandler)
32 | def invocation = new RestInvocation(null, null, Mock(MethodMetadata), [firstHandler, secondHandler], new HttpHeaders())
33 |
34 | when:
35 | def result = invocation.proceed()
36 |
37 | then:
38 | 1 * secondHandler.proceed(_) >> SOME_RESULT
39 | result == SOME_RESULT
40 | }
41 |
42 | def 'should not call second handler if first handled request already'() {
43 | given:
44 | def firstHandler = [proceed: {
45 | return SOME_RESULT
46 | }] as RestInvocationHandler
47 | def secondHandler = Mock(RestInvocationHandler)
48 | def invocation = new RestInvocation(null, null, Mock(MethodMetadata), [firstHandler, secondHandler], new HttpHeaders())
49 |
50 | when:
51 | def result = invocation.proceed()
52 |
53 | then:
54 | 0 * secondHandler.proceed(_)
55 | result == SOME_RESULT
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/sample/RestClientWithMethods.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core.sample
2 |
3 | import org.springframework.http.ResponseEntity
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.bind.annotation.RequestBody
6 | import org.springframework.web.bind.annotation.RequestMapping
7 | import org.springframework.web.bind.annotation.RequestMethod
8 | import org.springframework.web.bind.annotation.RequestParam
9 |
10 | /**
11 | * @author Jakub Kubrynski
12 | */
13 | interface RestClientWithMethods {
14 |
15 | @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
16 | User getWithSingleParameter(@PathVariable("id") Integer id);
17 |
18 | @RequestMapping(value = "/users/{id}/staff/{orderId}", method = RequestMethod.GET)
19 | User getWithTwoParameters(@PathVariable("orderId") Integer orderId, @PathVariable("id") Integer id);
20 |
21 | @RequestMapping(value = "/users", method = RequestMethod.POST)
22 | void postToUsers(@RequestBody User user);
23 |
24 | @RequestMapping(value = "/users/{id}/staff", method = RequestMethod.POST)
25 | void postToUsersStaff(@PathVariable("id") Long userId, @RequestBody User user);
26 |
27 | @RequestMapping(value = "/users/{id}", method = RequestMethod.PUT)
28 | void putToUsers(@PathVariable("id") Long userId, @RequestBody User user);
29 |
30 | @RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
31 | void deleteUser(@PathVariable("id") Integer id);
32 |
33 | @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
34 | ResponseEntity getResponseEntity(@PathVariable("id") Integer id);
35 |
36 | @RequestMapping(value = "/users/queries", method = RequestMethod.GET)
37 | User getWithRequestParameter(@RequestParam("name") String name);
38 |
39 | @RequestMapping(value = "/users/queriesPojo", method = RequestMethod.GET)
40 | User getWithRequestParametersPojo(User user);
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/ResteethAnnotationMetadata.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import eu.codearte.resteeth.annotation.LogScope;
4 | import eu.codearte.resteeth.annotation.RestClient;
5 | import org.springframework.core.annotation.AnnotationAttributes;
6 |
7 | import java.lang.annotation.Annotation;
8 | import java.util.HashMap;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | /**
13 | * @author Jakub Kubrynski
14 | */
15 | public class ResteethAnnotationMetadata {
16 |
17 | private final LogScope logScope;
18 | private Map annotations;
19 |
20 | ResteethAnnotationMetadata(AnnotationAttributes enableResteethAttributes,
21 | List restClientAnnotations, List interfaceAnnotations) {
22 | RestClient restClientAnnotation = null;
23 | for (Annotation annotation : restClientAnnotations) {
24 | if (annotation.annotationType() == RestClient.class) {
25 | restClientAnnotation = (RestClient) annotation;
26 | break;
27 | }
28 | }
29 |
30 | if (restClientAnnotation == null) {
31 | throw new IllegalStateException("No RestClient annotation found");
32 | }
33 |
34 | LogScope logScopeVar = enableResteethAttributes.getEnum("loggingScope");
35 |
36 | LogScope[] logScopes = restClientAnnotation.loggingScope();
37 | if (logScopes.length > 0) {
38 | logScopeVar = logScopes[0];
39 | }
40 |
41 | this.logScope = logScopeVar;
42 |
43 | annotations = new HashMap<>();
44 | for (Annotation annotation : interfaceAnnotations) {
45 | annotations.put(annotation.annotationType(), annotation);
46 | }
47 | for (Annotation annotation : restClientAnnotations) {
48 | annotations.put(annotation.annotationType(), annotation);
49 | }
50 | }
51 |
52 | public LogScope getLoggingScope() {
53 | return logScope;
54 | }
55 |
56 | public Map getAnnotations() {
57 | return annotations;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/handlers/UserAgentHandlerTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import eu.codearte.resteeth.config.EnableResteeth
5 | import eu.codearte.resteeth.core.sample.RestClientWithMethods
6 | import org.springframework.beans.factory.annotation.Autowired
7 | import org.springframework.context.annotation.Bean
8 | import org.springframework.context.annotation.Configuration
9 | import org.springframework.http.HttpHeaders
10 | import org.springframework.test.context.ContextConfiguration
11 | import org.springframework.test.web.client.MockRestServiceServer
12 | import org.springframework.web.client.RestTemplate
13 | import spock.lang.Specification
14 |
15 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.header
16 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
17 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
18 |
19 | @ContextConfiguration(classes = LoggingConfiguration)
20 | class UserAgentHandlerTest extends Specification {
21 |
22 | public static final String USER_AGENT_NAME = "Test client"
23 |
24 | @RestClient(endpoints = "http://localhost")
25 | private RestClientWithMethods client
26 |
27 | @Autowired
28 | RestTemplate restTemplate
29 |
30 | MockRestServiceServer mockServer
31 |
32 | void setup() {
33 | mockServer = MockRestServiceServer.createServer(restTemplate)
34 | }
35 |
36 | def 'should add User-Agent header to request'() {
37 | given:
38 | mockServer.expect(
39 | requestTo("http://localhost/users/1"))
40 | .andExpect(header(HttpHeaders.USER_AGENT, USER_AGENT_NAME))
41 | .andRespond(withSuccess())
42 | when:
43 | client.deleteUser(1)
44 |
45 | then:
46 | mockServer.verify()
47 | }
48 |
49 | }
50 |
51 | @Configuration
52 | @EnableResteeth
53 | class LoggingConfiguration {
54 |
55 | @Bean
56 | UserAgentHandler userAgentHandler() {
57 | return new UserAgentHandler(UserAgentHandlerTest.USER_AGENT_NAME)
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/handlers/HeadersHandler.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers;
2 |
3 | import eu.codearte.resteeth.annotation.StaticHeader;
4 | import eu.codearte.resteeth.annotation.StaticHeaders;
5 | import eu.codearte.resteeth.core.RestInvocation;
6 | import org.springframework.web.bind.annotation.RequestHeader;
7 |
8 | import java.lang.annotation.Annotation;
9 |
10 | /**
11 | * @author Jakub Kubrynski
12 | */
13 | public class HeadersHandler implements RestInvocationHandler {
14 |
15 | @Override
16 | public Object proceed(RestInvocation invocation) {
17 | addDynamicHeaders(invocation);
18 | addStaticHeaders(invocation);
19 | return invocation.proceed();
20 | }
21 |
22 | private void addDynamicHeaders(RestInvocation invocation) {
23 | Annotation[][] parametersAnnotations = invocation.getMethod().getParameterAnnotations();
24 | for (int i = 0; i < parametersAnnotations.length; i++) {
25 | Annotation[] parameterAnnotation = parametersAnnotations[i];
26 | for (Annotation annotation : parameterAnnotation) {
27 | if (RequestHeader.class.isAssignableFrom(annotation.annotationType())) {
28 | RequestHeader requestHeader = (RequestHeader) annotation;
29 | invocation.getDynamicHeaders().add(requestHeader.value(), String.valueOf(invocation.getArguments()[i]));
30 | }
31 | }
32 | }
33 | }
34 |
35 | private void addStaticHeaders(RestInvocation invocation) {
36 | //FIXME should read annotation from metadata - then also interface could be annotated
37 | StaticHeader staticHeader = invocation.getMethod().getAnnotation(StaticHeader.class);
38 | if (staticHeader != null) {
39 | invocation.getMetadata().getHttpHeaders().add(staticHeader.name(), staticHeader.value());
40 | }
41 | StaticHeaders staticHeaders = invocation.getMethod().getAnnotation(StaticHeaders.class);
42 | if (staticHeaders != null) {
43 | for (StaticHeader header : staticHeaders.value()) {
44 | invocation.getMetadata().getHttpHeaders().add(header.name(), header.value());
45 | }
46 | }
47 | }
48 |
49 | @Override
50 | public int getOrder() {
51 | return 90;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/handlers/HeadersHandlerTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.handlers
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import eu.codearte.resteeth.core.sample.RestClientHeaders
5 | import eu.codearte.resteeth.core.sample.RestMethodsConfig
6 | import org.springframework.beans.factory.annotation.Autowired
7 | import org.springframework.http.MediaType
8 | import org.springframework.test.context.ContextConfiguration
9 | import org.springframework.test.web.client.MockRestServiceServer
10 | import org.springframework.web.client.RestTemplate
11 | import spock.lang.Specification
12 |
13 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.header
14 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
15 |
16 | /**
17 | * @author Jakub Kubrynski
18 | */
19 | @ContextConfiguration(classes = RestMethodsConfig)
20 | class HeadersHandlerTest extends Specification {
21 |
22 | @RestClient
23 | RestClientHeaders restClient
24 |
25 | @Autowired
26 | RestTemplate restTemplate
27 |
28 | MockRestServiceServer mockServer
29 |
30 | void setup() {
31 | mockServer = MockRestServiceServer.createServer(restTemplate)
32 | }
33 |
34 | def "should add dynamic header"() {
35 | given:
36 | mockServer.expect(header("testHeaderName", "testHeaderValue"))
37 | .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON))
38 |
39 | when:
40 | restClient.getUserDynamicHeader("testHeaderValue", 42)
41 |
42 | then:
43 | mockServer.verify()
44 | }
45 |
46 | def "should add static header"() {
47 | given:
48 | mockServer.expect(header("testHeaderName", "testHeaderValue"))
49 | .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON))
50 |
51 | when:
52 | restClient.getUserStaticHeader(42)
53 |
54 | then:
55 | mockServer.verify()
56 | }
57 |
58 | def "should add static headers"() {
59 | given:
60 | mockServer.expect(header("testHeaderName1", "testHeaderValue1"))
61 | .andExpect(header("testHeaderName2", "testHeaderValue2"))
62 | .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON))
63 |
64 | when:
65 | restClient.getUserStaticHeaders(42)
66 |
67 | then:
68 | mockServer.verify()
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Resteeth
2 | ========
3 |
4 | Resteeth dynamically creates rest clients based on plain java interface with Spring MVC annotations. Ready to use beans are available through standard Spring injections.
5 |
6 | [](https://travis-ci.org/Codearte/resteeth) [](https://coveralls.io/r/Codearte/resteeth?branch=master) [](https://maven-badges.herokuapp.com/maven-central/eu.codearte.resteeth/resteeth) [](http://www.apache.org/licenses/LICENSE-2.0)
7 |
8 | Usage
9 | -----
10 |
11 | 1) Add dependencies
12 |
13 | In Maven projects (pom.xml):
14 |
15 | ```xml
16 |
17 | ...
18 |
19 |
20 | eu.codearte.resteeth
21 | resteeth
22 | 0.2.0
23 |
24 |
25 | ...
26 |
27 | ```
28 |
29 | In Gradle projects (build.gradle):
30 |
31 | ```groovy
32 | repositories {
33 | mavenCentral()
34 | }
35 | ...
36 | testCompile 'eu.codearte.resteeth:resteeth:0.2.0'
37 | ```
38 |
39 | 2) Enable configuration
40 |
41 | In SpringBoot projects Resteeth will work out of the box without any configuration needed. For classical projects you have to annotate your configuration with `@EnableResteeth`
42 |
43 | ```java
44 | @Configuration
45 | @EnableResteeth
46 | public class FooSpringConfig {
47 |
48 | }
49 | ```
50 |
51 | 3) Prepare interface
52 |
53 | ```java
54 | interface FooRestInterface {
55 |
56 | @RequestMapping(value = "/foos/{id}", method = RequestMethod.GET)
57 | Foo getFoo(@PathVariable("id") Integer id);
58 |
59 | @RequestMapping(value = "/foos", method = RequestMethod.POST)
60 | void postFoo(@RequestBody Foo user);
61 |
62 | }
63 | ```
64 |
65 | 4) Use!
66 |
67 | with single URL
68 |
69 | ```java
70 | @RestClient(endpoints = {"http://api.mydomain.com"})
71 | private FooRestInterface fooRestInterface;
72 |
73 | Foo foo = fooRestInterface.getFoo(123);
74 | ```
75 |
76 | or with round robin load balancing
77 |
78 | ```java
79 | @RestClient(endpoints = {"http://api1.mydomain.com/", "http://api2.mydomain.com/"})
80 | private FooRestInterface fooRestInterface;
81 |
82 | Foo foo = fooRestInterface.getFoo(123);
83 | ```
84 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/ResteethBeanFactoryPostProcessorTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config
2 |
3 | import eu.codearte.resteeth.TestObjectWrapper
4 | import eu.codearte.resteeth.annotation.RestClient
5 | import eu.codearte.resteeth.config.constructor.TestBean
6 | import eu.codearte.resteeth.endpoint.EndpointProvider
7 | import eu.codearte.resteeth.endpoint.StubEndpointProvider
8 | import eu.codearte.resteeth.sample.RestClientInterface
9 | import org.springframework.context.annotation.AnnotationConfigApplicationContext
10 | import org.springframework.context.annotation.Bean
11 | import org.springframework.context.annotation.ComponentScan
12 | import org.springframework.context.annotation.Configuration
13 | import spock.lang.Specification
14 |
15 | /**
16 | * @author Jakub Kubrynski
17 | */
18 | class ResteethBeanFactoryPostProcessorTest extends Specification {
19 |
20 | @Configuration
21 | @EnableResteeth
22 | static class SampleConfigurationInject {
23 |
24 | @RestClient
25 | private RestClientInterface restClientInterface
26 |
27 | @Bean
28 | TestObjectWrapper objectWrapper() {
29 | new TestObjectWrapper(restClientInterface)
30 | }
31 |
32 | @Bean
33 | EndpointProvider endpointProvider() {
34 | new StubEndpointProvider()
35 | }
36 | }
37 |
38 | def "should inject RestClientInterface into field"() {
39 | given:
40 | def context = new AnnotationConfigApplicationContext(SampleConfigurationInject)
41 | when:
42 | def bean = context.getBean(TestObjectWrapper)
43 | then:
44 | bean != null
45 | bean.target instanceof RestClientInterface
46 | }
47 |
48 | @Configuration
49 | @EnableResteeth
50 | static class SampleMethodInject {
51 |
52 | @Bean
53 | TestObjectWrapper objectWrapper(@RestClient RestClientInterface restClientInterface) {
54 | new TestObjectWrapper(restClientInterface)
55 | }
56 |
57 | @Bean
58 | EndpointProvider endpointProvider() {
59 | new StubEndpointProvider()
60 | }
61 | }
62 |
63 | def "should inject RestClientInterface into method parameter"() {
64 | given:
65 | def context = new AnnotationConfigApplicationContext(SampleMethodInject)
66 | when:
67 | def bean = context.getBean(TestObjectWrapper)
68 | then:
69 | bean != null
70 | bean.target instanceof RestClientInterface
71 | }
72 |
73 | @Configuration
74 | @EnableResteeth
75 | @ComponentScan("eu.codearte.resteeth.config.constructor")
76 | static class ConstructorInjectionConfiguration {
77 |
78 | @Bean
79 | EndpointProvider endpointProvider() {
80 | new StubEndpointProvider()
81 | }
82 | }
83 |
84 | def "should inject RestClientInterface into constructor"() {
85 | given:
86 | def context = new AnnotationConfigApplicationContext(ConstructorInjectionConfiguration)
87 | when:
88 | def bean = context.getBean(TestBean)
89 | then:
90 | bean != null
91 | bean.restClientInterface instanceof RestClientInterface
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/BeanProxyCreatorTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import eu.codearte.resteeth.config.EnableResteeth
5 | import eu.codearte.resteeth.core.sample.RestClientWithMethods
6 | import eu.codearte.resteeth.core.sample.User
7 | import eu.codearte.resteeth.handlers.LoggingHandler
8 | import eu.codearte.resteeth.handlers.ProfilingHandler
9 | import eu.codearte.resteeth.handlers.RestInvocationHandler
10 | import eu.codearte.resteeth.handlers.UserAgentHandler
11 | import org.springframework.context.annotation.Bean
12 | import org.springframework.context.annotation.Configuration
13 | import org.springframework.test.context.ContextConfiguration
14 | import spock.lang.Specification
15 |
16 | /**
17 | * @author Tomasz Nurkiewicz
18 | */
19 | @ContextConfiguration(classes = CustomHandlersConfiguration)
20 | class BeanProxyCreatorTest extends Specification {
21 |
22 | public static final int CACHED_ID = 1
23 | public static final String FIXED_RESPONSE = "Fixed"
24 | public static final String CACHED_RESPONSE = "Cached"
25 |
26 | @RestClient(endpoints = "http://localhost")
27 | private RestClientWithMethods client
28 |
29 | def 'should stop at custom handler and do not invoke remaining handlers'() {
30 | given:
31 | def idThatIsCached = CACHED_ID
32 |
33 | when:
34 | User user = client.getWithSingleParameter(idThatIsCached)
35 |
36 | then:
37 | user.id == CACHED_ID
38 | user.name == CACHED_RESPONSE
39 | }
40 |
41 | def 'should proceed after first handler and call second in order'() {
42 | given:
43 | def idThatIsNotCached = 2
44 |
45 | when:
46 | User user = client.getWithSingleParameter(idThatIsNotCached)
47 |
48 | then:
49 | user.id == 2
50 | user.name == FIXED_RESPONSE
51 | }
52 |
53 | }
54 |
55 | @Configuration
56 | @EnableResteeth
57 | class CustomHandlersConfiguration {
58 |
59 | @Bean
60 | RestInvocationHandler cachingHandler() {
61 | return new RestInvocationHandler() {
62 | @Override
63 | Object proceed(RestInvocation invocation) {
64 | def id = invocation.arguments[0]
65 | if (id == BeanProxyCreatorTest.CACHED_ID) {
66 | return new User(id: id, name: BeanProxyCreatorTest.CACHED_RESPONSE)
67 | } else {
68 | return invocation.proceed()
69 | }
70 | }
71 |
72 | @Override
73 | int getOrder() {
74 | return LOWEST_PRECEDENCE - 10
75 | }
76 | }
77 | }
78 |
79 | @Bean
80 | RestInvocationHandler fixedHandler() {
81 | return new RestInvocationHandler() {
82 |
83 | @Override
84 | Object proceed(RestInvocation invocation) {
85 | return new User(id: invocation.arguments[0], name: BeanProxyCreatorTest.FIXED_RESPONSE)
86 | }
87 |
88 | @Override
89 | int getOrder() {
90 | return LOWEST_PRECEDENCE - 5
91 | }
92 | }
93 | }
94 |
95 | @Bean
96 | RestInvocationHandler loggingHandler() {
97 | return new LoggingHandler()
98 | }
99 |
100 | @Bean
101 | RestInvocationHandler profilingHandler() {
102 | return new ProfilingHandler()
103 | }
104 |
105 | @Bean
106 | RestInvocationHandler userAgentHandler() {
107 | return new UserAgentHandler()
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/config/BeanResolver.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config;
2 |
3 | import eu.codearte.resteeth.annotation.RestClient;
4 | import eu.codearte.resteeth.endpoint.EndpointProvider;
5 | import eu.codearte.resteeth.endpoint.Endpoints;
6 | import org.springframework.beans.factory.BeanFactoryUtils;
7 | import org.springframework.beans.factory.NoSuchBeanDefinitionException;
8 | import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
9 | import org.springframework.beans.factory.annotation.Qualifier;
10 | import org.springframework.beans.factory.config.BeanDefinition;
11 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
12 | import org.springframework.core.annotation.AnnotationUtils;
13 | import org.springframework.core.type.MethodMetadata;
14 |
15 | import java.lang.annotation.Annotation;
16 |
17 | /**
18 | * @author Jakub Kubrynski
19 | */
20 | class BeanResolver {
21 |
22 | boolean beanNotDefinedExplicitly(ConfigurableListableBeanFactory configurableListableBeanFactory, Class> beanClass) {
23 | String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(configurableListableBeanFactory, beanClass, true, true);
24 | return beanNames == null || beanNames.length == 0;
25 | }
26 |
27 | EndpointProvider findEndpointProvider(Class> beanClass, ConfigurableListableBeanFactory beanFactory, RestClient restClient) {
28 | if (restClient.endpoints().length == 1) {
29 | return Endpoints.fixedEndpoint(restClient.endpoints()[0]);
30 | } else if (restClient.endpoints().length > 1) {
31 | return Endpoints.roundRobinEndpoint(restClient.endpoints());
32 | }
33 |
34 | Qualifier qualifier = AnnotationUtils.findAnnotation(beanClass, Qualifier.class);
35 |
36 | if (qualifier == null) {
37 | // without qualifier
38 | return BeanFactoryUtils.beanOfTypeIncludingAncestors(beanFactory, EndpointProvider.class);
39 | }
40 |
41 | Annotation qualifierAnnotation = qualifier;
42 |
43 | for (Annotation annotation : beanClass.getAnnotations()) {
44 | if (qualifier != annotation && annotation.annotationType().isAnnotationPresent(Qualifier.class)) {
45 | qualifierAnnotation = annotation;
46 | }
47 | }
48 |
49 | String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, EndpointProvider.class, true, true);
50 |
51 | for (String beanName : beanNames) {
52 |
53 | if (checkQualifier(beanFactory.getBeanDefinition(beanName), qualifierAnnotation)) {
54 | return (EndpointProvider) beanFactory.getBean(beanName);
55 | }
56 | }
57 |
58 | throw new NoSuchBeanDefinitionException(EndpointProvider.class, "Cannot find proper for " + beanClass.getCanonicalName());
59 | }
60 |
61 | private boolean checkQualifier(BeanDefinition endpointBeanDefinition, Annotation qualifierAnnotation) {
62 | if (endpointBeanDefinition instanceof AnnotatedBeanDefinition) {
63 | AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) endpointBeanDefinition;
64 | String qualifierCanonicalName = qualifierAnnotation.annotationType().getCanonicalName();
65 |
66 | MethodMetadata factoryMethodMetadata = annotatedBeanDefinition.getFactoryMethodMetadata();
67 |
68 | if (factoryMethodMetadata.isAnnotated(qualifierCanonicalName)) {
69 | if (qualifierAnnotation instanceof Qualifier) {
70 | Object value1 = factoryMethodMetadata.getAnnotationAttributes(qualifierCanonicalName).get("value");
71 | Object value2 = ((Qualifier) qualifierAnnotation).value();
72 | if (value1 == null || value2 == null) {
73 | throw new IllegalArgumentException("No value found on Qualifier annotation");
74 | }
75 | if (value1.equals(value2)) {
76 | return true;
77 | }
78 | }
79 | return true;
80 | }
81 | }
82 | return false;
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/config/EndpointProviderResolverTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import eu.codearte.resteeth.config.attributes.RestClientWithEndpoints
5 | import eu.codearte.resteeth.config.qualifier.RestInterfaceWithQualifier
6 | import eu.codearte.resteeth.config.sample.RestInterfaceWithCustomQualifier
7 | import eu.codearte.resteeth.config.sample.SampleEndpoint
8 | import eu.codearte.resteeth.endpoint.EndpointProvider
9 | import eu.codearte.resteeth.endpoint.StubEndpointProvider
10 | import org.springframework.beans.factory.BeanCreationException
11 | import org.springframework.beans.factory.annotation.Qualifier
12 | import org.springframework.context.annotation.AnnotationConfigApplicationContext
13 | import org.springframework.context.annotation.Bean
14 | import org.springframework.context.annotation.Configuration
15 | import spock.lang.Specification
16 |
17 | /**
18 | * @author Jakub Kubrynski
19 | */
20 | class EndpointProviderResolverTest extends Specification {
21 |
22 | @Configuration
23 | @EnableResteeth
24 | static class SampleConfigurationWithoutProperEndpointProvider {
25 |
26 | @RestClient
27 | private RestInterfaceWithCustomQualifier customQualifier
28 |
29 | @Bean
30 | EndpointProvider endpointProvider() {
31 | new StubEndpointProvider()
32 | }
33 | }
34 |
35 | def "should throw exception when no proper EndpointProvider is found"() {
36 | when:
37 | def context = new AnnotationConfigApplicationContext(SampleConfigurationWithoutProperEndpointProvider)
38 | context.getBean(RestInterfaceWithCustomQualifier.class)
39 | then:
40 | def exception = thrown(BeanCreationException)
41 | exception.message.contains("Cannot find proper for eu.codearte.resteeth.config.sample.RestInterfaceWithCustomQualifier")
42 | }
43 |
44 | @Configuration
45 | @EnableResteeth
46 | static class SampleCustomQualifierConfiguration {
47 |
48 | @RestClient
49 | RestInterfaceWithCustomQualifier restInterfaceWithCustomQualifier
50 |
51 | @Bean
52 | @SampleEndpoint
53 | EndpointProvider endpointProvidera() {
54 | new StubEndpointProvider()
55 | }
56 |
57 | @Bean
58 | @Qualifier("test2")
59 | EndpointProvider endpointProvider2() {
60 | new StubEndpointProvider()
61 | }
62 | }
63 |
64 | def "should find proper EndpointProvided using @SampleEndpoint annotation"() {
65 | when:
66 | new AnnotationConfigApplicationContext(SampleCustomQualifierConfiguration)
67 | then:
68 | // check if proper endpoint is injected
69 | noExceptionThrown()
70 | }
71 |
72 | @Configuration
73 | @EnableResteeth
74 | static class SampleQualifierConfiguration {
75 |
76 | @RestClient
77 | RestInterfaceWithQualifier restInterfaceWithQualifier
78 |
79 | @Bean
80 | @Qualifier("test")
81 | EndpointProvider endpointProvider() {
82 | new StubEndpointProvider()
83 | }
84 |
85 | @Bean
86 | @Qualifier("test2")
87 | EndpointProvider endpointProvider2() {
88 | new StubEndpointProvider()
89 | }
90 | }
91 |
92 | def "should find proper EndpointProvided using @Qualifier annotation"() {
93 | when:
94 | new AnnotationConfigApplicationContext(SampleQualifierConfiguration)
95 | then:
96 | // check if proper endpoint is injected
97 | noExceptionThrown()
98 | }
99 |
100 | @Configuration
101 | @EnableResteeth
102 | static class SampleEndpointsAttributeConfiguration {
103 |
104 | @RestClient(endpoints = ["http://test"])
105 | RestClientWithEndpoints restClientWithFixedEndpoints
106 |
107 | @RestClient(endpoints = ["http://test", "http://test2"])
108 | RestClientWithEndpoints restClientWithRoundRobinEndpoint
109 | }
110 |
111 | def "should create fixed EndpointProvided from RestClient.endpoints() attribute"() {
112 | when:
113 | new AnnotationConfigApplicationContext(SampleEndpointsAttributeConfiguration)
114 | then:
115 | // check if proper endpoint is injected
116 | noExceptionThrown()
117 | }
118 |
119 | def "should create round robin EndpointProvided from RestClient.endpoints() attribute"() {
120 | when:
121 | new AnnotationConfigApplicationContext(SampleEndpointsAttributeConfiguration)
122 | then:
123 | // check if proper endpoint is injected
124 | noExceptionThrown()
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/BeanProxyCreator.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import eu.codearte.resteeth.annotation.LogScope;
4 | import eu.codearte.resteeth.annotation.RestClient;
5 | import eu.codearte.resteeth.endpoint.EndpointProvider;
6 | import eu.codearte.resteeth.handlers.RestInvocationHandler;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.aop.framework.ProxyFactory;
10 | import org.springframework.core.OrderComparator;
11 | import org.springframework.core.annotation.AnnotationAttributes;
12 | import org.springframework.web.bind.annotation.RequestMapping;
13 | import org.springframework.web.client.RestTemplate;
14 |
15 | import java.lang.annotation.Annotation;
16 | import java.lang.invoke.MethodHandles;
17 | import java.lang.reflect.Method;
18 | import java.util.ArrayList;
19 | import java.util.Arrays;
20 | import java.util.Collection;
21 | import java.util.HashMap;
22 | import java.util.List;
23 | import java.util.Map;
24 |
25 | /**
26 | * @author Jakub Kubrynski
27 | * @author Tomasz Nurkiewicz
28 | */
29 | public class BeanProxyCreator {
30 |
31 | private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
32 |
33 | private final RestTemplate restTemplate;
34 |
35 | private final MetadataExtractor metadataExtractor = new MetadataExtractor();
36 | private final List handlers;
37 |
38 | public BeanProxyCreator(RestTemplate restTemplate, Collection handlers) {
39 | this.restTemplate = restTemplate;
40 | this.handlers = new ArrayList<>(handlers);
41 | OrderComparator.sort(this.handlers);
42 | LOG.debug("Custom handlers: {}", this.handlers);
43 | }
44 |
45 | public Object createProxyBean(Class> beanClass, EndpointProvider endpointProvider,
46 | AnnotationAttributes enableResteethAttributes, List restClientAnnotations) {
47 | LOG.info("Creating Resteeth bean for interface {}", beanClass.getCanonicalName());
48 | final RestInvocationInterceptor interceptor = buildInvocationHandler(beanClass, endpointProvider, enableResteethAttributes, restClientAnnotations);
49 | return buildProxy(beanClass, interceptor);
50 | }
51 |
52 | private RestInvocationInterceptor buildInvocationHandler(Class> beanClass, EndpointProvider endpointProvider,
53 | AnnotationAttributes enableResteethAttributes, List restClientAnnotations) {
54 | final Map methodMetadataMap = extractInterfaceInformation(beanClass, enableResteethAttributes, restClientAnnotations);
55 | final List handlersList = prepareHandlersList(endpointProvider);
56 | return new RestInvocationInterceptor(methodMetadataMap, handlersList);
57 | }
58 |
59 | private List prepareHandlersList(EndpointProvider endpointProvider) {
60 | final List handlersList = new ArrayList<>(this.handlers);
61 | handlersList.add(new RestTemplateInvoker(restTemplate, endpointProvider));
62 | return handlersList;
63 | }
64 |
65 | private Object buildProxy(Class> beanClass, RestInvocationInterceptor invocation) {
66 | ProxyFactory proxyFactory = new ProxyFactory();
67 | proxyFactory.addInterface(beanClass);
68 | proxyFactory.addAdvice(invocation);
69 | return proxyFactory.getProxy();
70 | }
71 |
72 | private Map extractInterfaceInformation(Class> beanClass,
73 | AnnotationAttributes enableResteethAttributes, List restClientAnnotations) {
74 | Map methodMetadataMap = new HashMap<>();
75 | RequestMapping controllerRequestMapping = beanClass.getAnnotation(RequestMapping.class);
76 | ResteethAnnotationMetadata annotationMetadata = mergeAnnotations(enableResteethAttributes, restClientAnnotations, Arrays.asList(beanClass.getAnnotations()));
77 | for (Method method : beanClass.getMethods()) {
78 | methodMetadataMap.put(method, metadataExtractor.extractMethodMetadata(method, controllerRequestMapping, annotationMetadata));
79 | }
80 | return methodMetadataMap;
81 | }
82 |
83 | private ResteethAnnotationMetadata mergeAnnotations(AnnotationAttributes enableResteethAttributes,
84 | List restClientAnnotations,
85 | List interfaceAnnotations) {
86 | return new ResteethAnnotationMetadata(enableResteethAttributes, restClientAnnotations, interfaceAnnotations);
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/config/ResteethAutowireCandidateResolverDelegate.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.config;
2 |
3 | import eu.codearte.resteeth.annotation.RestClient;
4 | import eu.codearte.resteeth.core.BeanProxyCreator;
5 | import eu.codearte.resteeth.handlers.RestInvocationHandler;
6 | import eu.codearte.resteeth.util.SpringUtils;
7 | import org.springframework.beans.BeansException;
8 | import org.springframework.beans.factory.BeanFactory;
9 | import org.springframework.beans.factory.BeanFactoryAware;
10 | import org.springframework.beans.factory.config.BeanDefinitionHolder;
11 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
12 | import org.springframework.beans.factory.config.DependencyDescriptor;
13 | import org.springframework.beans.factory.support.AutowireCandidateResolver;
14 | import org.springframework.core.annotation.AnnotationAttributes;
15 | import org.springframework.http.converter.HttpMessageConverter;
16 | import org.springframework.http.converter.StringHttpMessageConverter;
17 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
18 | import org.springframework.web.client.RestTemplate;
19 |
20 | import java.lang.annotation.Annotation;
21 | import java.util.ArrayList;
22 | import java.util.Arrays;
23 | import java.util.Collection;
24 |
25 | /**
26 | * @author Jakub Kubrynski
27 | */
28 | class ResteethAutowireCandidateResolverDelegate implements AutowireCandidateResolver, BeanFactoryAware {
29 |
30 | private static final String RESTEETH_REST_TEMPLATE_BEAN_NAME = "resteethRestTemplate";
31 | private BeanResolver beanResolver = new BeanResolver();
32 | private ConfigurableListableBeanFactory beanFactory;
33 | private BeanProxyCreator beanProxyCreator;
34 |
35 | private final AutowireCandidateResolver autowireCandidateResolver;
36 | private final AnnotationAttributes enableResteethAttributes;
37 |
38 | private boolean initialized = false;
39 |
40 | public ResteethAutowireCandidateResolverDelegate(AutowireCandidateResolver autowireCandidateResolver,
41 | AnnotationAttributes enableResteethAttributes) {
42 | this.autowireCandidateResolver = autowireCandidateResolver;
43 | this.enableResteethAttributes = enableResteethAttributes;
44 | }
45 |
46 | @Override
47 | public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
48 | return autowireCandidateResolver.isAutowireCandidate(bdHolder, descriptor);
49 | }
50 |
51 | @Override
52 | public Object getSuggestedValue(DependencyDescriptor descriptor) {
53 | return autowireCandidateResolver.getSuggestedValue(descriptor);
54 | }
55 |
56 | @Override
57 | public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, String beanName) {
58 | RestClient restClientAnnotation = getRestClientAnnotation(descriptor.getAnnotations());
59 | if (restClientAnnotation != null) {
60 | ensueBeanProxyCreatorInitialized();
61 | return beanProxyCreator.createProxyBean(descriptor.getDependencyType(),
62 | beanResolver.findEndpointProvider(descriptor.getDependencyType(), beanFactory, restClientAnnotation),
63 | enableResteethAttributes, Arrays.asList(descriptor.getAnnotations()));
64 | }
65 | return autowireCandidateResolver.getLazyResolutionProxyIfNecessary(descriptor, beanName);
66 | }
67 |
68 | private RestClient getRestClientAnnotation(Annotation[] annotations) {
69 | for (Annotation annotation : annotations) {
70 | if (RestClient.class.equals(annotation.annotationType())) {
71 | return (RestClient) annotation;
72 | }
73 | }
74 | return null;
75 | }
76 |
77 | private synchronized void ensueBeanProxyCreatorInitialized() {
78 | if (!initialized) {
79 | initialized = true;
80 | RestTemplate restTemplate = provideRestTemplate(this.beanFactory);
81 | final Collection handlers = SpringUtils.getBeansOfType(RestInvocationHandler.class, this.beanFactory);
82 | beanProxyCreator = new BeanProxyCreator(restTemplate, handlers);
83 | }
84 | }
85 |
86 | private RestTemplate provideRestTemplate(ConfigurableListableBeanFactory configurableListableBeanFactory) {
87 | if (beanResolver.beanNotDefinedExplicitly(configurableListableBeanFactory, RestTemplate.class)) {
88 | ArrayList> messageConverters = new ArrayList<>();
89 | messageConverters.add(new StringHttpMessageConverter());
90 | messageConverters.add(new MappingJackson2HttpMessageConverter());
91 | configurableListableBeanFactory.registerSingleton(RESTEETH_REST_TEMPLATE_BEAN_NAME, new RestTemplate(messageConverters));
92 | }
93 | return configurableListableBeanFactory.getBean(RestTemplate.class);
94 | }
95 |
96 | @Override
97 | public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
98 | this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/MetadataExtractorTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core
2 |
3 | import org.springframework.http.HttpMethod
4 | import org.springframework.http.MediaType
5 | import org.springframework.web.bind.annotation.PathVariable
6 | import org.springframework.web.bind.annotation.RequestMapping
7 | import org.springframework.web.bind.annotation.RequestMethod
8 | import spock.lang.Specification
9 |
10 | /**
11 | * @author Jakub Kubrynski
12 | */
13 | class MetadataExtractorTest extends Specification {
14 |
15 | private MetadataExtractor extractor
16 |
17 | static interface SampleRestClient {
18 |
19 | @RequestMapping(value = "/somethings/{id}")
20 | void withoutRequestMethod(@PathVariable("id") Long id);
21 |
22 | @RequestMapping(method = RequestMethod.GET)
23 | void withoutRequestUrl();
24 |
25 | @RequestMapping(value = "/somethings/{id}", method = RequestMethod.GET)
26 | void withoutContentTypes();
27 |
28 | @RequestMapping(value = "/somethings", method = RequestMethod.GET,
29 | consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_XML_VALUE])
30 | void withAllData();
31 | }
32 |
33 | @RequestMapping(value = "/somethings", method = RequestMethod.GET,
34 | consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_XML_VALUE])
35 | static interface SampleRestClientWithMapping {
36 |
37 | @RequestMapping(value = "/{id}")
38 | void withoutRequestMethod(@PathVariable("id") Long id);
39 |
40 | @RequestMapping(method = RequestMethod.GET)
41 | void withoutRequestUrl();
42 |
43 | @RequestMapping(method = RequestMethod.GET)
44 | void withoutContentTypes();
45 | }
46 |
47 | void setup() {
48 | extractor = new MetadataExtractor()
49 | }
50 |
51 | def "should extract request information from method mapping"() {
52 | given:
53 | def method = SampleRestClient.class.getDeclaredMethod("withAllData")
54 | when:
55 | def metadata = extractor.extractMethodMetadata(method, null, null)
56 | then:
57 | metadata.methodUrl == "/somethings"
58 | metadata.requestMethod == HttpMethod.GET
59 | metadata.httpHeaders.getContentType() == MediaType.APPLICATION_JSON
60 | metadata.httpHeaders.getAccept() == [MediaType.APPLICATION_XML]
61 | }
62 |
63 | def "should check if request method is required"() {
64 | given:
65 | def method = SampleRestClient.class.getDeclaredMethod("withoutRequestMethod", Long.class)
66 | when:
67 | extractor.extractMethodMetadata(method, null, null)
68 | then:
69 | def e = thrown(IncorrectRequestMapping.class)
70 | e.message.contains("No requestMethods specified")
71 | }
72 |
73 | def "should inherit request method from controller"() {
74 | given:
75 | def method = SampleRestClientWithMapping.class.getDeclaredMethod("withoutRequestMethod", Long.class)
76 | def controllerRequestMapping = SampleRestClientWithMapping.getAnnotation(RequestMapping)
77 | when:
78 | def metadata = extractor.extractMethodMetadata(method, controllerRequestMapping, null)
79 | then:
80 | metadata.requestMethod == HttpMethod.GET
81 | }
82 |
83 | def "should check if request url is required"() {
84 | given:
85 | def method = SampleRestClient.class.getDeclaredMethod("withoutRequestUrl")
86 | when:
87 | extractor.extractMethodMetadata(method, null, null)
88 | then:
89 | def e = thrown(IncorrectRequestMapping.class)
90 | e.message.contains("No request url found")
91 | }
92 |
93 | def "should inherit request url from controller"() {
94 | given:
95 | def method = SampleRestClientWithMapping.class.getDeclaredMethod("withoutRequestUrl")
96 | def controllerRequestMapping = SampleRestClientWithMapping.getAnnotation(RequestMapping)
97 | when:
98 | def metadata = extractor.extractMethodMetadata(method, controllerRequestMapping, null)
99 | then:
100 | metadata.methodUrl == "/somethings"
101 | }
102 |
103 | def "should merge request url from controller"() {
104 | given:
105 | def method = SampleRestClientWithMapping.class.getDeclaredMethod("withoutRequestMethod", Long.class)
106 | def controllerRequestMapping = SampleRestClientWithMapping.getAnnotation(RequestMapping)
107 | when:
108 | def metadata = extractor.extractMethodMetadata(method, controllerRequestMapping, null)
109 | then:
110 | metadata.methodUrl == "/somethings/{id}"
111 | }
112 |
113 | def "should work without content types"() {
114 | given:
115 | def method = SampleRestClient.class.getDeclaredMethod("withoutContentTypes")
116 | when:
117 | def metadata = extractor.extractMethodMetadata(method, null, null)
118 | then:
119 | !metadata.httpHeaders.containsKey("ContentType")
120 | !metadata.httpHeaders.containsKey("Accept")
121 | }
122 |
123 | def "should inherit content types from controller"() {
124 | given:
125 | def method = SampleRestClientWithMapping.class.getDeclaredMethod("withoutContentTypes")
126 | def controllerRequestMapping = SampleRestClientWithMapping.getAnnotation(RequestMapping)
127 | when:
128 | def metadata = extractor.extractMethodMetadata(method, controllerRequestMapping, null)
129 | then:
130 | metadata.httpHeaders.getContentType() == MediaType.APPLICATION_JSON
131 | metadata.httpHeaders.getAccept() == [MediaType.APPLICATION_XML]
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/MetadataExtractor.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.http.HttpHeaders;
6 | import org.springframework.http.HttpMethod;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.web.bind.annotation.PathVariable;
9 | import org.springframework.web.bind.annotation.RequestBody;
10 | import org.springframework.web.bind.annotation.RequestMapping;
11 | import org.springframework.web.bind.annotation.RequestMethod;
12 | import org.springframework.web.bind.annotation.RequestParam;
13 |
14 | import java.lang.annotation.Annotation;
15 | import java.lang.invoke.MethodHandles;
16 | import java.lang.reflect.Method;
17 | import java.util.ArrayList;
18 | import java.util.HashMap;
19 |
20 | /**
21 | * @author Jakub Kubrynski
22 | */
23 | class MetadataExtractor {
24 |
25 | private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
26 |
27 | MethodMetadata extractMethodMetadata(Method method, RequestMapping controllerRequestMapping,
28 | ResteethAnnotationMetadata resteethAnnotationMetadata) {
29 | RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
30 |
31 | String methodUrl = extractUrl(requestMapping, controllerRequestMapping);
32 |
33 | Class>[] parameterTypes = method.getParameterTypes();
34 | Annotation[][] parameterAnnotations = method.getParameterAnnotations();
35 | Integer requestBody = null;
36 | Integer pojoQueryParameter = null;
37 | HashMap urlVariables = new HashMap<>();
38 | HashMap queryParameters = new HashMap<>();
39 | if (parameterTypes != null && parameterTypes.length > 0) {
40 | for (int i = 0; i < parameterTypes.length; i++) {
41 | if (parameterAnnotations[i].length > 0) {
42 | for (Annotation parameterAnnotation : parameterAnnotations[i]) {
43 | if (PathVariable.class.isAssignableFrom(parameterAnnotation.getClass())) {
44 | urlVariables.put(i, ((PathVariable) parameterAnnotation).value());
45 | } else if (RequestParam.class.isAssignableFrom(parameterAnnotation.getClass())) {
46 | queryParameters.put(i, ((RequestParam) parameterAnnotation).value());
47 | } else if (RequestBody.class.isAssignableFrom(parameterAnnotation.getClass())) {
48 | requestBody = i;
49 | }
50 | }
51 | } else {
52 | pojoQueryParameter = i;
53 | }
54 | }
55 | }
56 |
57 | return new MethodMetadata(methodUrl,
58 | extractRequestMethod(requestMapping, controllerRequestMapping),
59 | extractReturnType(method),
60 | extractHeaders(requestMapping, controllerRequestMapping),
61 | new MethodAnnotationMetadata(resteethAnnotationMetadata),
62 | new ParameterMetadata(requestBody, urlVariables, queryParameters, pojoQueryParameter));
63 | }
64 |
65 | private Class> extractReturnType(Method method) {
66 | return method.getReturnType() == void.class ? Void.class : method.getReturnType();
67 | }
68 |
69 | private HttpHeaders extractHeaders(RequestMapping requestMapping, RequestMapping controllerRequestMapping) {
70 | HttpHeaders headers = new HttpHeaders();
71 |
72 | String[] consumes = requestMapping.consumes();
73 | if (consumes.length == 0 && controllerRequestMapping != null) {
74 | consumes = controllerRequestMapping.consumes();
75 | }
76 |
77 | if (consumes.length > 0) {
78 | headers.setContentType(MediaType.valueOf(consumes[0]));
79 | }
80 |
81 | String[] produces = requestMapping.produces();
82 | if (produces.length == 0 && controllerRequestMapping != null) {
83 | produces = controllerRequestMapping.produces();
84 | }
85 |
86 | if (produces.length > 0) {
87 | ArrayList acceptableMediaTypes = new ArrayList<>();
88 | for (String acceptType : produces) {
89 | acceptableMediaTypes.add(MediaType.valueOf(acceptType));
90 | }
91 | headers.setAccept(acceptableMediaTypes);
92 | }
93 |
94 | return headers;
95 | }
96 |
97 | private String extractUrl(RequestMapping methodMapping, RequestMapping controllerMapping) {
98 | String foundUrl = "";
99 |
100 | String[] controllerValues = controllerMapping != null ? controllerMapping.value() : new String[0];
101 | String[] methodValues = methodMapping.value();
102 |
103 | if (methodValues.length == 0 && controllerValues.length == 0) {
104 | throw new IncorrectRequestMapping("No request url found!");
105 | }
106 |
107 | if (controllerValues.length > 0) {
108 | foundUrl += controllerValues[0];
109 | if (controllerValues.length > 1) {
110 | LOG.warn("Found more than one controller URL mapping. Using first specified: {}", foundUrl);
111 | }
112 | }
113 |
114 | if (methodValues.length > 0) {
115 | foundUrl += methodValues[0];
116 | if (methodValues.length > 1) {
117 | LOG.warn("Found more than one URL mapping. Using first specified: {}", foundUrl);
118 | }
119 | }
120 | return foundUrl;
121 | }
122 |
123 | private HttpMethod extractRequestMethod(RequestMapping requestMapping, RequestMapping controllerRequestMapping) {
124 | RequestMethod[] requestMethods = requestMapping.method();
125 | if (requestMethods == null || requestMethods.length == 0) {
126 | if (controllerRequestMapping == null ||
127 | controllerRequestMapping.method() == null ||
128 | controllerRequestMapping.method().length == 0) {
129 | LOG.warn("No request mapping requestMethods found");
130 | throw new IncorrectRequestMapping("No requestMethods specified!");
131 | } else {
132 | requestMethods = controllerRequestMapping.method();
133 | }
134 | } else if (requestMethods.length > 1) {
135 | LOG.warn("More than one request method found. Using first specified");
136 | }
137 | return HttpMethod.valueOf(requestMethods[0].name());
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/eu/codearte/resteeth/core/RestTemplateInvoker.java:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core;
2 |
3 | import eu.codearte.resteeth.endpoint.EndpointProvider;
4 | import eu.codearte.resteeth.handlers.RestInvocationHandler;
5 | import eu.codearte.resteeth.util.SpringUtils;
6 | import org.apache.commons.beanutils.BeanUtils;
7 | import org.springframework.core.Ordered;
8 | import org.springframework.http.HttpEntity;
9 | import org.springframework.http.HttpHeaders;
10 | import org.springframework.http.ResponseEntity;
11 | import org.springframework.web.client.RestTemplate;
12 |
13 | import java.lang.reflect.Field;
14 | import java.util.*;
15 |
16 | /**
17 | * @author Jakub Kubrynski
18 | */
19 | class RestTemplateInvoker implements RestInvocationHandler {
20 |
21 | private final RestTemplate restTemplate;
22 | private final EndpointProvider endpointProvider;
23 |
24 | RestTemplateInvoker(RestTemplate restTemplate, EndpointProvider endpointProvider) {
25 | this.restTemplate = restTemplate;
26 | this.endpointProvider = endpointProvider;
27 | }
28 |
29 | @Override
30 | public Object proceed(RestInvocation invocation) {
31 | MethodMetadata methodMetadata = invocation.getMetadata();
32 | Map urlVariablesValues = buildArgumentsMap(methodMetadata.getParameterMetadata().getUrlVariables(), invocation.getArguments());
33 |
34 | String requestUrl = endpointProvider.getEndpoint() + methodMetadata.getMethodUrl();
35 |
36 | requestUrl = appendAnnotatedQueryParameters(requestUrl, methodMetadata.getParameterMetadata().getQueryParameters(), invocation.getArguments());
37 | requestUrl = appendPojoQueryParameters(requestUrl, methodMetadata.getParameterMetadata().getPojoQueryIndex(), invocation.getArguments());
38 |
39 | HttpHeaders headers = new HttpHeaders();
40 |
41 | headers.putAll(methodMetadata.getHttpHeaders());
42 | headers.putAll(invocation.getDynamicHeaders());
43 |
44 | @SuppressWarnings("unchecked")
45 | HttpEntity entity = new HttpEntity(
46 | extractRequestBody(methodMetadata.getParameterMetadata().getRequestBodyIndex(), invocation.getArguments()),
47 | headers);
48 |
49 | Class responseType;
50 | boolean returnsResponseEntity = ResponseEntity.class.isAssignableFrom(methodMetadata.getReturnType());
51 | if (returnsResponseEntity) {
52 | responseType = SpringUtils.getGenericType(invocation.getMethod().getGenericReturnType());
53 | } else {
54 | responseType = methodMetadata.getReturnType();
55 | }
56 |
57 | @SuppressWarnings("unchecked")
58 | ResponseEntity> exchange = restTemplate.exchange(requestUrl, methodMetadata.getRequestMethod(), entity,
59 | responseType, urlVariablesValues);
60 |
61 | return returnsResponseEntity ? exchange : exchange.getBody();
62 | }
63 |
64 | private Object extractRequestBody(Integer requestBody, Object[] arguments) {
65 | if (requestBody != null && arguments != null && arguments.length >= requestBody) {
66 | return arguments[requestBody];
67 | }
68 | return null;
69 | }
70 |
71 | private Map buildArgumentsMap(Map urlVariables, Object[] arguments) {
72 | Map stringHashMap = new HashMap<>();
73 |
74 | for (Integer paramIndex : urlVariables.keySet()) {
75 | stringHashMap.put(urlVariables.get(paramIndex), arguments[paramIndex]);
76 | }
77 |
78 | return stringHashMap;
79 | }
80 |
81 | private String appendPojoQueryParameters(String requestUrl, Integer pojoQueryParameter, Object[] arguments) {
82 | if (pojoQueryParameter == null) {
83 | return requestUrl;
84 | }
85 | Map queryParamsMap = new HashMap<>();
86 | Object pojoParameterValue = arguments[pojoQueryParameter];
87 |
88 | List fields = getAllFields(pojoParameterValue.getClass());
89 | for (Field field : fields) {
90 | if (!field.isSynthetic()) {
91 | try {
92 | String property = BeanUtils.getProperty(pojoParameterValue, field.getName());
93 | if (property != null) {
94 | queryParamsMap.put(field.getName(), property);
95 | }
96 | } catch (ReflectiveOperationException e) {
97 | throw new IllegalStateException("Cannot do reflection magic on " + pojoParameterValue.getClass(), e);
98 | }
99 | }
100 | }
101 | return appendQueryParameters(requestUrl, queryParamsMap);
102 | }
103 |
104 | private static List getAllFields(Class> type) {
105 | List fields = new ArrayList();
106 | for (Class> c = type; c != null; c = c.getSuperclass()) {
107 | fields.addAll(Arrays.asList(c.getDeclaredFields()));
108 | }
109 | return fields;
110 | }
111 |
112 | private String appendAnnotatedQueryParameters(String requestUrl, Map queryParameters, Object[] arguments) {
113 | return appendQueryParameters(requestUrl, buildArgumentsMap(queryParameters, arguments));
114 | }
115 |
116 | private String appendQueryParameters(String requestUrl, Map queryParamsMap) {
117 | if (queryParamsMap.isEmpty()) {
118 | return requestUrl;
119 | }
120 |
121 | StringBuilder urlBuilder = new StringBuilder(requestUrl);
122 |
123 | if (urlBuilder.indexOf("?") == -1) {
124 | urlBuilder.append("?");
125 | }
126 |
127 | boolean isFirst = true;
128 |
129 | String[] keys = queryParamsMap.keySet().toArray(new String[queryParamsMap.size()]);
130 | Arrays.sort(keys);
131 | for (String paramName : keys) {
132 | if (isFirst) {
133 | isFirst = false;
134 | } else {
135 | urlBuilder.append("&");
136 | }
137 | urlBuilder.append(paramName).append("=").append(queryParamsMap.get(paramName));
138 | }
139 |
140 | return urlBuilder.toString();
141 | }
142 |
143 | @Override
144 | public String toString() {
145 | return "RestTemplateInvoker(" + "restTemplate=" + restTemplate +
146 | ", endpointProvider=" + endpointProvider + ')';
147 | }
148 |
149 | public int getOrder() {
150 | return Ordered.LOWEST_PRECEDENCE;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/test/groovy/eu/codearte/resteeth/core/RestClientMethodInterceptorTest.groovy:
--------------------------------------------------------------------------------
1 | package eu.codearte.resteeth.core
2 |
3 | import eu.codearte.resteeth.annotation.RestClient
4 | import eu.codearte.resteeth.core.sample.RestClientWithMethods
5 | import eu.codearte.resteeth.core.sample.RestMethodsConfig
6 | import eu.codearte.resteeth.core.sample.User
7 | import org.springframework.beans.factory.annotation.Autowired
8 | import org.springframework.http.HttpMethod
9 | import org.springframework.http.HttpStatus
10 | import org.springframework.http.MediaType
11 | import org.springframework.test.context.ContextConfiguration
12 | import org.springframework.test.web.client.MockRestServiceServer
13 | import org.springframework.test.web.client.match.MockRestRequestMatchers
14 | import org.springframework.web.client.RestTemplate
15 | import spock.lang.Specification
16 |
17 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.method
18 | import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
19 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withCreatedEntity
20 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
21 |
22 | /**
23 | * @author Jakub Kubrynski
24 | */
25 | @ContextConfiguration(classes = RestMethodsConfig)
26 | class RestClientMethodInterceptorTest extends Specification {
27 |
28 | @RestClient
29 | RestClientWithMethods restClient
30 |
31 | @Autowired
32 | RestTemplate restTemplate
33 |
34 | MockRestServiceServer mockServer
35 |
36 | void setup() {
37 | mockServer = MockRestServiceServer.createServer(restTemplate)
38 | }
39 |
40 | def "should invoke get method"() {
41 | given:
42 | mockServer.expect(requestTo("http://localhost/users/42")).andExpect(method(HttpMethod.GET))
43 | .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"John\"}", MediaType.APPLICATION_JSON))
44 |
45 | when:
46 | User user = restClient.getWithSingleParameter(42)
47 |
48 | then:
49 | mockServer.verify()
50 | user.id == 42
51 | user.name == "John"
52 | }
53 |
54 | def "should invoke get method with two parameters"() {
55 | given:
56 | mockServer.expect(requestTo("http://localhost/users/42/staff/123")).andExpect(method(HttpMethod.GET))
57 | .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"John\"}", MediaType.APPLICATION_JSON))
58 |
59 | when:
60 | User user = restClient.getWithTwoParameters(123, 42)
61 |
62 | then:
63 | mockServer.verify()
64 | user.id == 42
65 | user.name == "John"
66 | }
67 |
68 | def "should invoke post method"() {
69 | given:
70 | mockServer.expect(requestTo("http://localhost/users")).andExpect(method(HttpMethod.POST))
71 | .andExpect(MockRestRequestMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
72 | .andRespond(withSuccess())
73 |
74 | when:
75 | restClient.postToUsers(new User(name: "test"))
76 |
77 | then:
78 | mockServer.verify()
79 | }
80 |
81 | def "should invoke post method with path parameter"() {
82 | given:
83 | mockServer.expect(requestTo("http://localhost/users/135/staff")).andExpect(method(HttpMethod.POST))
84 | .andExpect(MockRestRequestMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
85 | .andRespond(withCreatedEntity(new URI("http://localhost/users/42")))
86 |
87 | when:
88 | restClient.postToUsersStaff(135, new User(name: "test"))
89 |
90 | then:
91 | mockServer.verify()
92 | }
93 |
94 | def "should invoke put method"() {
95 | given:
96 | mockServer.expect(requestTo("http://localhost/users/44")).andExpect(method(HttpMethod.PUT))
97 | .andExpect(MockRestRequestMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
98 | .andRespond(withSuccess())
99 |
100 | when:
101 | restClient.putToUsers(44, new User(name: "test"))
102 |
103 | then:
104 | mockServer.verify()
105 | }
106 |
107 | def "should invoke delete method"() {
108 | given:
109 | mockServer.expect(requestTo("http://localhost/users/42")).andExpect(method(HttpMethod.DELETE))
110 | .andRespond(withSuccess())
111 |
112 | when:
113 | restClient.deleteUser(42)
114 |
115 | then:
116 | mockServer.verify()
117 | }
118 |
119 | def "should not throw when calling toString()"() {
120 | when:
121 | def str = restClient.toString()
122 |
123 | then:
124 | str.startsWith("Proxy to ")
125 | }
126 |
127 | def "should get ResponseEntity"() {
128 | given:
129 | mockServer.expect(requestTo("http://localhost/users/42")).andExpect(method(HttpMethod.GET))
130 | .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"John\"}", MediaType.APPLICATION_JSON))
131 |
132 | when:
133 | def entity = restClient.getResponseEntity(42)
134 |
135 | then:
136 | mockServer.verify()
137 | entity.statusCode == HttpStatus.OK
138 | def user = entity.getBody()
139 | user.id == 42
140 | user.name == "John"
141 | }
142 |
143 | def "should invoke get method with request parameter"() {
144 | given:
145 | mockServer.expect(requestTo("http://localhost/users/queries?name=John")).andExpect(method(HttpMethod.GET))
146 | .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"John\"}", MediaType.APPLICATION_JSON))
147 |
148 | when:
149 | User user = restClient.getWithRequestParameter("John")
150 |
151 | then:
152 | mockServer.verify()
153 | user.id == 42
154 | user.name == "John"
155 | }
156 |
157 | def "should invoke get method with request parameters pojo"() {
158 | given:
159 | mockServer.expect(requestTo("http://localhost/users/queriesPojo?id=42&name=John")).andExpect(method(HttpMethod.GET))
160 | .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"John\"}", MediaType.APPLICATION_JSON))
161 |
162 | when:
163 | User user = restClient.getWithRequestParametersPojo(new User(name: "John", id: 42))
164 |
165 | then:
166 | mockServer.verify()
167 | user.id == 42
168 | user.name == "John"
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 |
4 | eu.codearte.resteeth
5 | resteeth
6 | ResTeeth
7 | 0.3.5-SNAPSHOT
8 |
9 | ResTeeth - dynamic REST client for Spring
10 | https://github.com/Codearte/resteeth/
11 |
12 |
13 | 1.7
14 | 1.7
15 | UTF-8
16 |
17 | [3.2.0.RELEASE,5.0.0.RELEASE)
18 | [1.1.0.RELEASE,1.3.99.RELEASE)
19 | [2.1.0,3.0.0)
20 | [1.2.0,2.0.0)
21 | 2.3.7
22 | 0.7-groovy-2.0
23 |
24 |
25 |
26 |
27 |
28 | org.springframework
29 | spring-context
30 | ${spring.version}
31 |
32 |
33 | org.springframework
34 | spring-webmvc
35 | ${spring.version}
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-autoconfigure
40 | ${spring-boot.version}
41 | true
42 | provided
43 |
44 |
45 | org.springframework
46 | spring-test
47 | ${spring.version}
48 | test
49 |
50 |
51 | org.springframework.boot
52 | spring-boot
53 | ${spring-boot.version}
54 | test
55 |
56 |
57 | org.springframework.boot
58 | spring-boot-starter-web
59 | ${spring-boot.version}
60 | test
61 |
62 |
63 |
64 |
65 | com.fasterxml.jackson.core
66 | jackson-core
67 | ${jackson.version}
68 |
69 |
70 | com.fasterxml.jackson.core
71 | jackson-databind
72 | ${jackson.version}
73 |
74 |
75 |
76 | commons-beanutils
77 | commons-beanutils
78 | 1.9.2
79 |
80 |
81 |
82 |
83 | org.codehaus.groovy
84 | groovy-all
85 | ${groovy.version}
86 | test
87 |
88 |
89 | org.spockframework
90 | spock-core
91 | ${spock.version}
92 | test
93 |
94 |
95 | org.spockframework
96 | spock-spring
97 | ${spock.version}
98 | test
99 |
100 |
101 |
102 | cglib
103 | cglib
104 | 3.1
105 | test
106 |
107 |
108 | org.objenesis
109 | objenesis
110 | 2.1
111 | test
112 |
113 |
114 |
115 |
116 | org.slf4j
117 | slf4j-api
118 | ${slf4j.version}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | org.apache.maven.plugins
127 | maven-compiler-plugin
128 | 3.2
129 |
130 |
131 |
132 | org.codehaus.gmavenplus
133 | gmavenplus-plugin
134 | 1.2
135 |
136 |
137 |
138 | testCompile
139 |
140 |
141 |
142 |
143 |
144 |
145 | org.eluder.coveralls
146 | coveralls-maven-plugin
147 | 3.0.1
148 |
149 |
150 | org.jacoco
151 | jacoco-maven-plugin
152 | 0.7.2.201409121644
153 |
154 |
155 | prepare-agent
156 |
157 | prepare-agent
158 |
159 |
160 |
161 |
162 |
163 |
164 | org.apache.maven.plugins
165 | maven-release-plugin
166 | 2.5.1
167 |
168 | true
169 | false
170 | release
171 | deploy
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | jkubrynski
180 | Jakub Kubrynski
181 | jk ATT codearte DOTT eu
182 |
183 |
184 | mariuszs
185 | Mariusz Smykula
186 | ms ATT codearte DOTT eu
187 |
188 |
189 |
190 |
191 | scm:git:https://github.com/Codearte/resteeth.git
192 | scm:git:git@github.com:Codearte/resteeth.git
193 | https://github.com/Codearte/resteeth/
194 | HEAD
195 |
196 |
197 |
198 |
199 | Apache 2
200 | http://www.apache.org/licenses/LICENSE-2.0.txt
201 | repo
202 | A business-friendly OSS license
203 |
204 |
205 |
206 |
207 |
208 | sonatype-nexus-staging
209 | Nexus Release Repository
210 | http://oss.sonatype.org/service/local/staging/deploy/maven2/
211 |
212 |
213 |
214 |
215 |
216 | release
217 |
218 |
219 |
220 | org.apache.maven.plugins
221 | maven-gpg-plugin
222 | 1.5
223 |
224 |
225 | sign-artifacts
226 | verify
227 |
228 | sign
229 |
230 |
231 |
232 |
233 |
234 | org.apache.maven.plugins
235 | maven-source-plugin
236 | 2.2.1
237 |
238 |
239 | attach-sources
240 |
241 | jar-no-fork
242 |
243 |
244 |
245 |
246 |
247 | org.apache.maven.plugins
248 | maven-javadoc-plugin
249 | 2.9.1
250 |
251 |
252 | attach-javadocs
253 |
254 | jar
255 |
256 |
257 |
258 |
259 |
260 | org.sonatype.plugins
261 | nexus-staging-maven-plugin
262 | 1.6.5
263 | true
264 |
265 | sonatype-nexus-staging
266 | https://oss.sonatype.org/
267 | true
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------