Skip to content

Commit

Permalink
#24 action support for JAX-RS (#211)
Browse files Browse the repository at this point in the history
added support for custom actions by allowing katharsis JAX-RS methods to be added to katharsis repositories. Other implementations like Vert.x can follow the same pattern. Methods have to obey some rule to be recognized as either repository or resource actions.

katharsis client extended by a getResourceRepository(...) method taking a QuerySpecResourceRepository subclass and returning a proxy that either invokes katharsis or jax-rs. 

Example available in ActionTest with the SchedulerRepository.
  • Loading branch information
Remo authored Nov 14, 2016
2 parents 0f27485 + 6a46657 commit e689445
Show file tree
Hide file tree
Showing 45 changed files with 1,389 additions and 325 deletions.
44 changes: 37 additions & 7 deletions katharsis-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
<relativePath>../katharsis-parent</relativePath>
</parent>

<groupId>io.katharsis</groupId>
<artifactId>katharsis-client</artifactId>
<version>2.8.2-SNAPSHOT</version>
<packaging>bundle</packaging>

<name>katharsis-client</name>
Expand Down Expand Up @@ -91,10 +89,26 @@
</dependency>

<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<optional>true</optional>
</dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-proxy-client</artifactId>
<version>${jersey.version}</version>
<optional>true</optional>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
<optional>true</optional>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
<optional>true</optional>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
Expand All @@ -108,7 +122,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>


<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.glassfish.jersey.core</groupId>
Expand Down Expand Up @@ -138,7 +157,18 @@
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.katharsis.client;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
Expand All @@ -9,9 +11,12 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;

import io.katharsis.client.action.ActionStubFactory;
import io.katharsis.client.action.ActionStubFactoryContext;
import io.katharsis.client.http.HttpAdapter;
import io.katharsis.client.http.okhttp.OkHttpAdapter;
import io.katharsis.client.internal.BaseResponseDeserializer;
import io.katharsis.client.internal.ClientStubInvocationHandler;
import io.katharsis.client.internal.ErrorResponseDeserializer;
import io.katharsis.client.internal.RelationshipRepositoryStubImpl;
import io.katharsis.client.internal.ResourceRepositoryStubImpl;
Expand All @@ -25,11 +30,18 @@
import io.katharsis.module.CoreModule;
import io.katharsis.module.Module;
import io.katharsis.module.ModuleRegistry;
import io.katharsis.queryspec.QuerySpecRelationshipRepository;
import io.katharsis.queryspec.QuerySpecResourceRepository;
import io.katharsis.repository.RelationshipRepository;
import io.katharsis.repository.RepositoryInstanceBuilder;
import io.katharsis.repository.information.RepositoryInformationBuilder;
import io.katharsis.repository.information.RepositoryInformationBuilderContext;
import io.katharsis.repository.information.ResourceRepositoryInformation;
import io.katharsis.repository.information.internal.ResourceRepositoryInformationImpl;
import io.katharsis.resource.field.ResourceField;
import io.katharsis.resource.field.ResourceFieldNameTransformer;
import io.katharsis.resource.information.ResourceInformation;
import io.katharsis.resource.information.ResourceInformationBuilder;
import io.katharsis.resource.registry.ConstantServiceUrlProvider;
import io.katharsis.resource.registry.RegistryEntry;
import io.katharsis.resource.registry.ResourceLookup;
Expand All @@ -43,6 +55,7 @@
import io.katharsis.resource.registry.repository.adapter.ResourceRepositoryAdapter;
import io.katharsis.response.BaseResponseContext;
import io.katharsis.utils.JsonApiUrlBuilder;
import io.katharsis.utils.PreconditionUtil;
import okhttp3.OkHttpClient;

/**
Expand All @@ -66,9 +79,12 @@ public class KatharsisClient {

private boolean pushAlways = true;

private ActionStubFactory actionStubFactory;

public KatharsisClient(String serviceUrl) {
this(new ConstantServiceUrlProvider(normalize(serviceUrl)));
}

public KatharsisClient(ServiceUrlProvider serviceUrlProvider) {
httpAdapter = new OkHttpAdapter();

Expand Down Expand Up @@ -97,8 +113,8 @@ public ClientResourceRegistry(ModuleRegistry moduleRegistry, ServiceUrlProvider
}

@Override
protected synchronized RegistryEntry<?> getEntry(Class<?> clazz, boolean allowNull) {
RegistryEntry<?> entry = super.getEntry(clazz, true);
protected synchronized <T> RegistryEntry<T> getEntry(Class<T> clazz, boolean allowNull) {
RegistryEntry<T> entry = super.getEntry(clazz, true);
if (entry == null) {
entry = allocateRepository(clazz, true);
}
Expand Down Expand Up @@ -193,7 +209,6 @@ private void initExceptionMapperRegistry() {

@SuppressWarnings({ "rawtypes", "unchecked" })
private <T, I extends Serializable> RegistryEntry<T> allocateRepository(Class<T> resourceClass, boolean allocateRelated) {

ResourceInformation resourceInformation = moduleRegistry.getResourceInformationBuilder().build(resourceClass);
final ResourceRepositoryStub<T, I> repositoryStub = new ResourceRepositoryStubImpl<>(this, resourceClass,
resourceInformation, urlBuilder);
Expand All @@ -206,9 +221,11 @@ public Object buildRepository() {
return repositoryStub;
}
};
ResourceRepositoryInformation repositoryInformation = new ResourceRepositoryInformationImpl(repositoryStub.getClass(),
resourceInformation.getResourceType(), resourceInformation);
ResourceEntry<T, I> resourceEntry = new DirectResponseResourceEntry<>(repositoryInstanceBuilder);
List<ResponseRelationshipEntry<T, ?>> relationshipEntries = new ArrayList<>();
RegistryEntry<T> registryEntry = new RegistryEntry<>(resourceInformation, resourceEntry, relationshipEntries);
RegistryEntry<T> registryEntry = new RegistryEntry<>(repositoryInformation, resourceEntry, relationshipEntries);
resourceRegistry.addEntry(resourceClass, registryEntry);

allocateRepositoryRelations(registryEntry, allocateRelated, relationshipEntries);
Expand Down Expand Up @@ -255,6 +272,38 @@ public Class<?> getTargetAffiliation() {
}
}

@SuppressWarnings("unchecked")
public <R extends QuerySpecResourceRepository<?, ?>> R getResourceRepository(Class<R> repositoryInterfaceClass) {
RepositoryInformationBuilder informationBuilder = moduleRegistry.getRepositoryInformationBuilder();
PreconditionUtil.assertTrue("no a valid repository interface", informationBuilder.accept(repositoryInterfaceClass));
ResourceRepositoryInformation repositoryInformation = (ResourceRepositoryInformation) informationBuilder
.build(repositoryInterfaceClass, newRepositoryInformationBuilderContext());
Class<?> resourceClass = repositoryInformation.getResourceInformation().getResourceClass();

Object actionStub = actionStubFactory != null ? actionStubFactory.createStub(repositoryInterfaceClass) : null;
QuerySpecResourceRepositoryStub<?, Serializable> repositoryStub = getQuerySpecRepository(resourceClass);

ClassLoader classLoader = repositoryInterfaceClass.getClassLoader();
InvocationHandler invocationHandler = new ClientStubInvocationHandler(repositoryStub, actionStub);
return (R) Proxy.newProxyInstance(classLoader,
new Class[] { repositoryInterfaceClass, QuerySpecResourceRepositoryStub.class }, invocationHandler);
}

private RepositoryInformationBuilderContext newRepositoryInformationBuilderContext() {
return new RepositoryInformationBuilderContext() {

@Override
public ResourceInformationBuilder getResourceInformationBuilder() {
return moduleRegistry.getResourceInformationBuilder();
}
};
}

public <R extends QuerySpecRelationshipRepository<?, ?, ?, ?>> R getRelationshipRepository(
Class<R> repositoryInterfaceClass) {
return null;
}

/**
* @param resourceClass resource class
* @return stub for the given resourceClass
Expand Down Expand Up @@ -370,4 +419,31 @@ public HttpAdapter getHttpAdapter() {
public ExceptionMapperRegistry getExceptionMapperRegistry() {
return exceptionMapperRegistry;
}

public ActionStubFactory getActionStubFactory() {
return actionStubFactory;
}

/**
* Sets the factory to use to create action stubs (like JAX-RS annotated repository methods).
*
* @param actionStubFactory to use
*/
public void setActionStubFactory(ActionStubFactory actionStubFactory) {
this.actionStubFactory = actionStubFactory;
if (actionStubFactory != null) {
actionStubFactory.init(new ActionStubFactoryContext() {

@Override
public ServiceUrlProvider getServiceUrlProvider() {
return moduleRegistry.getResourceRegistry().getServiceUrlProvider();
}

@Override
public HttpAdapter getHttpAdapter() {
return httpAdapter;
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.katharsis.client.action;

/**
* Used to create stubs for repository interface having action methods. Stub is only used
* to invoke the action, not the jsonapi methods.
*/
public interface ActionStubFactory {

void init(ActionStubFactoryContext context);

<T> T createStub(Class<T> interfaceClass);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.katharsis.client.action;

import io.katharsis.client.http.HttpAdapter;
import io.katharsis.resource.registry.ServiceUrlProvider;

public interface ActionStubFactoryContext {

ServiceUrlProvider getServiceUrlProvider();

HttpAdapter getHttpAdapter();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.katharsis.client.action;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;

import org.glassfish.jersey.client.proxy.WebResourceFactory;

import io.katharsis.resource.registry.ServiceUrlProvider;

public class JerseyActionStubFactory implements ActionStubFactory {

private Client client;

private ActionStubFactoryContext context;

private JerseyActionStubFactory() {
}

public static JerseyActionStubFactory newInstance() {
return newInstance(ClientBuilder.newClient());
}

public static JerseyActionStubFactory newInstance(Client client) {
JerseyActionStubFactory factory = new JerseyActionStubFactory();
factory.client = client;
return factory;
}

@Override
public void init(ActionStubFactoryContext context) {
this.context = context;
}

@Override
public <T> T createStub(Class<T> interfaceClass) {
ServiceUrlProvider serviceUrlProvider = context.getServiceUrlProvider();
String serviceUrl = serviceUrlProvider.getUrl();

WebTarget target = client.target(serviceUrl);
return WebResourceFactory.newResource(interfaceClass, target);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.katharsis.client.internal;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import io.katharsis.client.KatharsisClient;
import io.katharsis.client.QuerySpecResourceRepositoryStub;
import io.katharsis.client.action.ActionStubFactory;

public class ClientStubInvocationHandler implements InvocationHandler {

private static final Set<String> REPOSITORY_METHODS = getMethodNames(QuerySpecResourceRepositoryStub.class);

private QuerySpecResourceRepositoryStub<?, Serializable> repositoryStub;

private Object actionStub;

public ClientStubInvocationHandler(QuerySpecResourceRepositoryStub<?, Serializable> repositoryStub, Object actionStub) {
this.repositoryStub = repositoryStub;
this.actionStub = actionStub;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class || REPOSITORY_METHODS.contains(method.getName())) {
// execute repository method
return method.invoke(repositoryStub, args);
}
else if (actionStub != null) {
// execute action
return method.invoke(actionStub, args);
}
else {
throw new IllegalStateException("cannot execute actions, no " + ActionStubFactory.class.getSimpleName() + " set with "
+ KatharsisClient.class.getName());
}
}

private static Set<String> getMethodNames(Class<?> clazz) {
Set<String> repositoryMethods = new HashSet<>();
Method[] repositoryMethodObjects = clazz.getMethods();
for (Method repositoryMethodObject : repositoryMethodObjects) {
repositoryMethods.add(repositoryMethodObject.getName());
}
return repositoryMethods;
}
}
Loading

0 comments on commit e689445

Please sign in to comment.