├── 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 | [![Build Status](https://travis-ci.org/Codearte/resteeth.svg)](https://travis-ci.org/Codearte/resteeth) [![Coverage Status](https://img.shields.io/coveralls/Codearte/resteeth.svg)](https://coveralls.io/r/Codearte/resteeth?branch=master) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/eu.codearte.resteeth/resteeth/badge.svg)](https://maven-badges.herokuapp.com/maven-central/eu.codearte.resteeth/resteeth) [![Apache 2](http://img.shields.io/badge/license-Apache%202-red.svg)](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 | --------------------------------------------------------------------------------