commit 6b65acf8a89f88b941ace17dc59df490c7756c7b Author: fabio.simeoni Date: Wed Sep 5 09:46:20 2012 +0000 branch for 2.0.x releases (first release in gCube 2.10.0) git-svn-id: http://svn.research-infrastructures.eu/public/d4science/gcube/branches/common/common-clients/2.0@57618 82a268e6-3cf1-43bd-a215-b396298e98cf diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..0f53f3e --- /dev/null +++ b/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..e81ceb6 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + common-clients + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/distro/INSTALL b/distro/INSTALL new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/distro/INSTALL @@ -0,0 +1 @@ + diff --git a/distro/LICENSE b/distro/LICENSE new file mode 100644 index 0000000..630ba97 --- /dev/null +++ b/distro/LICENSE @@ -0,0 +1,6 @@ +gCube System - License +------------------------------------------------------------ + +The gCube/gCore software is licensed as Free Open Source software conveying to the EUPL (http://ec.europa.eu/idabc/eupl). +The software and documentation is provided by its authors/distributors "as is" and no expressed or +implied warranty is given for its use, quality or fitness for a particular case. diff --git a/distro/MAINTAINERS b/distro/MAINTAINERS new file mode 100644 index 0000000..3d20d89 --- /dev/null +++ b/distro/MAINTAINERS @@ -0,0 +1,2 @@ +* Fabio Simeoni (fabio.simeoni@fao.org), FAO of the UN, Italy +* Rena Tsantouli (e.tsantoylh@di.uoa.gr), University of Athens, Greece \ No newline at end of file diff --git a/distro/README b/distro/README new file mode 100644 index 0000000..ec74aa9 --- /dev/null +++ b/distro/README @@ -0,0 +1,38 @@ +The gCube System - ${name} +---------------------- + +This work has been partially supported by the following European projects: DILIGENT (FP6-2003-IST-2), D4Science (FP7-INFRA-2007-1.2.2), +D4Science-II (FP7-INFRA-2008-1.2.2), iMarine (FP7-INFRASTRUCTURES-2011-2), and EUBrazilOpenBio (FP7-ICT-2011-EU-Brazil). + +Authors +------- + +* Fabio Simeoni (fabio.simeoni@fao.org), FAO of the UN, Italy. + +Version and Release Date +------------------------ +${version} + +Description +----------- +${description} + +Download information +-------------------- + +Source code is available from SVN: +${scm.url} + +Binaries can be downloaded from: + + +Documentation +------------- +Documentation is available on-line from the Projects Documentation Wiki: +https://https://gcube.wiki.gcube-system.org/gcube/index.php/Integration_and_Interoperability_Facilities_Framework:_Client_Libraries_Framework + + +Licensing +--------- + +This software is licensed under the terms you may find in the file named "LICENSE" in this directory. diff --git a/distro/changelog.xml b/distro/changelog.xml new file mode 100644 index 0000000..b0e0716 --- /dev/null +++ b/distro/changelog.xml @@ -0,0 +1,8 @@ + + + First Release + + + Rewritten as a framework for the implementation of client libraries that comply with the CL Design Model + + \ No newline at end of file diff --git a/distro/descriptor.xml b/distro/descriptor.xml new file mode 100644 index 0000000..21d8c88 --- /dev/null +++ b/distro/descriptor.xml @@ -0,0 +1,42 @@ + + servicearchive + + tar.gz + + / + + + ${distroDirectory} + / + true + + README + LICENSE + INSTALL + MAINTAINERS + changelog.xml + + 755 + true + + + + + ${distroDirectory}/profile.xml + / + true + + + target/${build.finalName}.jar + /${artifactId} + + + ${distroDirectory}/svnpath.txt + /${artifactId} + true + + + \ No newline at end of file diff --git a/distro/profile.xml b/distro/profile.xml new file mode 100644 index 0000000..91c49e4 --- /dev/null +++ b/distro/profile.xml @@ -0,0 +1,26 @@ + + + + Service + + ${description} + Common + ${artifactId} + 1.0.0 + + + ${artifactId} + ${version} + + ${groupId} + ${artifactId} + ${version} + + + ${build.finalName}.jar + + + + + + diff --git a/distro/svnpath.txt b/distro/svnpath.txt new file mode 100644 index 0000000..f416f9d --- /dev/null +++ b/distro/svnpath.txt @@ -0,0 +1 @@ +${scm.url} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..56dfc01 --- /dev/null +++ b/pom.xml @@ -0,0 +1,117 @@ + + 4.0.0 + + maven-parent + org.gcube.tools + 1.0.0 + + + + org.gcube.core + common-clients + 2.0.0-SNAPSHOT + + Common Clients + A framework for client APIs + + + distro + + + + scm:svn:http://svn.d4science.research-infrastructures.eu/gcube/trunk/Common/common-clients + scm:svn:https://svn.d4science.research-infrastructures.eu/gcube/trunk/Common/common-clients + http://svn.d4science.research-infrastructures.eu/gcube/trunk/Common/common-clients + + + + + + org.gcube.core + common-scope + [1.0.0-SNAPSHOT,2.0.0-SNAPSHOT) + + + + org.slf4j + slf4j-api + 1.6.4 + + + + junit + junit + 4.10 + test + + + + org.mockito + mockito-core + 1.8.5 + test + + + + org.slf4j + slf4j-simple + 1.6.4 + test + + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.5 + + + copy-profile + install + + copy-resources + + + target + + + ${distroDirectory} + true + + profile.xml + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + ${distroDirectory}/descriptor.xml + + + + + servicearchive + install + + single + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/gcube/common/clients/Call.java b/src/main/java/org/gcube/common/clients/Call.java new file mode 100644 index 0000000..8f78704 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/Call.java @@ -0,0 +1,26 @@ +package org.gcube.common.clients; + +/** + * A call to an endpoint of a given service. + * + *

+ * + * Calls interact with service endpoints at addresses provided by clients. + * + * @author Fabio Simeoni + * + * @param the type of service stubs + * @param the type of values returned from the call + * + */ +public interface Call { + + /** + * Calls a given service endpoint. + * + * @param address a proxy of the endpoint + * @return the value returned by the call + * @throws Exception if the call fails + */ + R call(S endpoint) throws Exception; +} diff --git a/src/main/java/org/gcube/common/clients/builders/AbstractBuilder.java b/src/main/java/org/gcube/common/clients/builders/AbstractBuilder.java new file mode 100644 index 0000000..e8d2ea3 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/AbstractBuilder.java @@ -0,0 +1,130 @@ +package org.gcube.common.clients.builders; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import javax.xml.ws.wsaddressing.W3CEndpointReference; + +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.config.DiscoveryConfig; +import org.gcube.common.clients.config.EndpointConfig; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.delegates.DirectDelegate; +import org.gcube.common.clients.delegates.DiscoveryDelegate; +import org.gcube.common.clients.delegates.ProxyDelegate; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.queries.Query; + +/** + * Partial implementation of proxy builders. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * @param

the type of service proxies + */ +public abstract class AbstractBuilder { + + /** + * Default proxy timeout. + */ + public static final int defaultTimeout = (int)TimeUnit.SECONDS.toMillis(10); + + private final ProxyPlugin plugin; + private Query query; + private W3CEndpointReference address; + private final EndpointCache cache; + private final Map properties = new HashMap(); + + /** + * Constructs an instance with a given {@link ProxyPlugin}, and {@link EndpointCache}, and zero or more default {@link Property}s. + * @param plugin the plugin + * @param cache the cache + * @param properties the properties + */ + protected AbstractBuilder(ProxyPlugin plugin,EndpointCache cache,Property ... properties) { + + this.plugin=plugin; + this.cache=cache; + + //sets default timeout, may be overridden by custom properties below + setTimeout(defaultTimeout); + + //add custom properties + for (Property property : properties) + addProperty(property); + } + + /** + * Returns the {@link ProxyPlugin}. + * @return the plugin + */ + protected ProxyPlugin plugin() { + return plugin; + } + /** + * Sets the query. + * @param query the query + */ + protected void setQuery(Query query) { + this.query = query; + } + + /** + * Sets the timeout. + * @param timeout the timout + */ + protected void setTimeout(int timeout) { + addProperty(Property.timeout(timeout)); + } + + /** + * Sets the address. + * @param address the address + */ + protected void setAddress(W3CEndpointReference address) { + this.address=address; + } + + /** + * Adds a custom property. + * @param property the property + */ + protected void addProperty(Property property) { + properties.put(property.name(),property.value()); + } + + //shared among subclasses + public P build() { + + ProxyDelegate delegate = null; + if (address==null) { + DiscoveryConfig config = + new DiscoveryConfig(plugin,query,cache); + for (Entry prop : properties.entrySet()) + config.addProperty(prop.getKey(),prop.getValue()); + delegate = new DiscoveryDelegate(config); + } + else { + EndpointConfig config = new EndpointConfig(plugin,convertAddress(address)); + for (Entry prop : properties.entrySet()) + config.addProperty(prop.getKey(),prop.getValue()); + delegate = new DirectDelegate(config); + } + + return plugin.newProxy(delegate); + } + + /** + * Converts a {@link W3CEndpointReference} in a service address. + * @param address the address as a {@link W3CEndpointReference} + * @return the converted address + */ + protected abstract A convertAddress(W3CEndpointReference address); + + + protected abstract String contextPath(); +} diff --git a/src/main/java/org/gcube/common/clients/builders/AbstractSingletonBuilder.java b/src/main/java/org/gcube/common/clients/builders/AbstractSingletonBuilder.java new file mode 100644 index 0000000..e0619bf --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/AbstractSingletonBuilder.java @@ -0,0 +1,77 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import org.gcube.common.clients.builders.SingletonBuilderAPI.Builder; +import org.gcube.common.clients.builders.SingletonBuilderAPI.FinalClause; +import org.gcube.common.clients.builders.SingletonBuilderAPI.SecondClause; +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.queries.Query; + +/** + * Partial implementation of proxy builders for singleton services, i.e. stateful services with a known single instance. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * @param

the type of service proxies + */ +public abstract class AbstractSingletonBuilder extends AbstractBuilder implements Builder,SecondClause

,FinalClause

{ + + /** + * Constructs an instance with a given {@link ProxyPlugin}, and {@link EndpointCache}, and zero or more default {@link Property}s. + * @param plugin the plugin + * @param plugin the cache + * @param properties the properties + */ + protected AbstractSingletonBuilder(ProxyPlugin plugin, EndpointCache cache,Property ... properties) { + super(plugin,cache,properties); + } + + @Override + public SecondClause

matching(Query query) { + setQuery(query); + return this; + }; + + @Override + public SecondClause

at(String host, int port) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), host, port)); + return this; + } + + @Override + public SecondClause

at(URL address) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), address)); + return this; + } + + @Override + public SecondClause

at(URI address) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), address)); + return this; + } + + @Override + public FinalClause

withTimeout(int duration, TimeUnit unit) { + setTimeout((int) unit.toMillis(duration)); + return this; + } + + @Override + public SecondClause

with(Property property) { + addProperty(property); + return this; + } + + @Override + public SecondClause

with(String name, T value) { + addProperty(new Property(name, value)); + return this; + } +} diff --git a/src/main/java/org/gcube/common/clients/builders/AbstractStatefulBuilder.java b/src/main/java/org/gcube/common/clients/builders/AbstractStatefulBuilder.java new file mode 100644 index 0000000..70f6550 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/AbstractStatefulBuilder.java @@ -0,0 +1,85 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import javax.xml.ws.wsaddressing.W3CEndpointReference; + +import org.gcube.common.clients.builders.StatefulBuilderAPI.Builder; +import org.gcube.common.clients.builders.StatefulBuilderAPI.FinalClause; +import org.gcube.common.clients.builders.StatefulBuilderAPI.SecondClause; +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.queries.Query; + +/** + * Partial implementation of proxy builders for stateful services. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * @param

the type of service proxies + */ +public abstract class AbstractStatefulBuilder extends AbstractBuilder implements Builder,SecondClause

,FinalClause

{ + + /** + * Constructs an instance with a given {@link ProxyPlugin}, and {@link EndpointCache}, and zero or more default {@link Property}s. + * @param plugin the plugin + * @param plugin the cache + * @param properties the properties + */ + protected AbstractStatefulBuilder(ProxyPlugin plugin, EndpointCache cache,Property ... properties) { + super(plugin,cache,properties); + } + + @Override + public SecondClause

matching(Query query) { + setQuery(query); + return this; + }; + + @Override + public SecondClause

at(W3CEndpointReference address) { + setAddress(address); + return this; + } + + @Override + public SecondClause

at(String key, String host, int port) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), plugin().namespace(), key, host, port)); + return this; + } + + @Override + public SecondClause

at(String key, URL address) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), plugin().namespace(), key, address)); + return this; + } + + @Override + public SecondClause

at(String key, URI address) { + this.setAddress(AddressingUtils.address(contextPath(),plugin().name(), plugin().namespace(), key, address)); + return this; + } + + @Override + public FinalClause

withTimeout(int duration, TimeUnit unit) { + setTimeout((int) unit.toMillis(duration)); + return this; + } + + @Override + public SecondClause

with(Property property) { + addProperty(property); + return this; + } + + @Override + public SecondClause

with(String name, T value) { + addProperty(new Property(name, value)); + return this; + } +} diff --git a/src/main/java/org/gcube/common/clients/builders/AbstractStatelessBuilder.java b/src/main/java/org/gcube/common/clients/builders/AbstractStatelessBuilder.java new file mode 100644 index 0000000..5088a1a --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/AbstractStatelessBuilder.java @@ -0,0 +1,73 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import org.gcube.common.clients.builders.StatelessBuilderAPI.Builder; +import org.gcube.common.clients.builders.StatelessBuilderAPI.FinalClause; +import org.gcube.common.clients.builders.StatelessBuilderAPI.SecondClause; +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.queries.Query; + +/** + * Partial implementation of proxy builders for stateless services. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * @param

the type of service proxies + */ +public abstract class AbstractStatelessBuilder extends AbstractBuilder implements Builder

,SecondClause

,FinalClause

{ + + /** + * Constructs an instance with a given {@link ProxyPlugin}, an {@link EndpointCache}, a {@link Query}, and zero or more default {@link Property}s. + * @param plugin the plugin + * @param the cache + * @param query the query + * @param properties the properties + */ + protected AbstractStatelessBuilder(ProxyPlugin plugin,EndpointCache cache,Query query,Property ... properties) { + super(plugin,cache,properties); + setQuery(query); + } + + @Override + public SecondClause

at(String host, int port) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), host, port)); + return this; + } + + @Override + public SecondClause

at(URL address) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), address)); + return this; + } + + @Override + public SecondClause

at(URI address) { + setAddress(AddressingUtils.address(contextPath(),plugin().name(), address)); + return this; + } + + @Override + public FinalClause

withTimeout(int duration, TimeUnit unit) { + setTimeout((int) unit.toMillis(duration)); + return this; + } + + @Override + public Builder

with(Property property) { + addProperty(property); + return this; + } + + @Override + public Builder

with(String name, T value) { + addProperty(new Property(name, value)); + return this; + } +} diff --git a/src/main/java/org/gcube/common/clients/builders/AddressingUtils.java b/src/main/java/org/gcube/common/clients/builders/AddressingUtils.java new file mode 100644 index 0000000..c0421ff --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/AddressingUtils.java @@ -0,0 +1,153 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.ws.wsaddressing.W3CEndpointReference; +import javax.xml.ws.wsaddressing.W3CEndpointReferenceBuilder; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Factory methods for addresses of service endpoints and service instances. + * + * @author Fabio Simeoni + * + */ +public class AddressingUtils { + + public static final String keyElement="ResourceKey"; + + + private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + private static final String scheme_prefix = "http://"; + private static final String keyElementPrefix = "key"; + + static { + factory.setNamespaceAware(true); + } + + /** + * Return the address of a service endpoint. + * @param contextPath the context path of the service + * @param service the name of the service + * @param host the host of the endpoint + * @param port the port of the endpoint + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the inputs + */ + public static W3CEndpointReference address(String contextPath,String service,String host, int port) throws IllegalArgumentException { + + W3CEndpointReferenceBuilder builder = new W3CEndpointReferenceBuilder(); + builder.address(join(contextPath,service,host,port)); + return builder.build(); + } + + /** + * Return the address of a service endpoint. + * @param contextPath the context path of the service + * @param service the name of the service + * @param address the address of the endpoint as a {@link URL} + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the inputs + */ + public static W3CEndpointReference address(String contextPath,String service,URL address) throws IllegalArgumentException { + return address(contextPath,service, address.getHost(),portFrom(address)); + } + + /** + * Return the address of a service endpoint. + * @param contextPath the context path of the service + * @param service the name of the service + * @param address the address of the endpoint as a {@link URI} + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the inputs + */ + public static W3CEndpointReference address(String contextPath,String service,URI address) throws IllegalArgumentException { + return address(contextPath,service, address.getHost(),portFrom(address)); + } + + /** + * Returns the address of a service instance. + * @param contextPath the context path of the service + * @param service the name of the service + * @param namespace the namespace of the service + * @param key the key of the instance + * @param host the host of the instance + * @param port the port of the instance + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the inputs + */ + public static W3CEndpointReference address(String contextPath,String service, String namespace, String key, String host, int port) throws IllegalArgumentException { + + W3CEndpointReferenceBuilder builder = new W3CEndpointReferenceBuilder(); + builder.address(join(contextPath,service,host,port)); + builder.referenceParameter(key(namespace,key)); + return builder.build(); + + } + + /** + * Returns the address of a service instance. + * @param contextPath the context path of the service + * @param service the name of the service + * @param namespace the namespace of the service + * @param key the key of the instance + * @param address the address of the endpoint as a {@link URL}. + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the inputs + */ + public static W3CEndpointReference address(String contextPath,String service, String namespace, String key, URL address) throws IllegalArgumentException { + return address(contextPath,service,namespace,key,address.getHost(),portFrom(address)); + + } + + /** + * Returns the address of a service instance. + * @param contextPath the context path of the service + * @param service the name of the service + * @param namespace the namespace of the service + * @param key the key of the instance + * @param address the address of the endpoint as a {@link URI}. + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the inputs + */ + public static W3CEndpointReference address(String contextPath,String service, String namespace, String key, URI address) throws IllegalArgumentException { + return address(contextPath,service,namespace,key,address.getHost(),portFrom(address)); + } + + //helper + private static String join(String path,String service,String host, int port) { + //some tolerance + if (host.startsWith(scheme_prefix)) + host = host.substring(scheme_prefix.length(),host.length()); + String address = scheme_prefix + host + ":" + port + path + service; + return address; + } + + //helper + public static Element key(String namespace,String value) { + try { + Document document = factory.newDocumentBuilder().newDocument(); + Element key = document.createElementNS(namespace,keyElementPrefix+":"+keyElement); + key.setAttribute("xmlns:"+keyElementPrefix,namespace); + key.appendChild(document.createTextNode(value)); + return key; + } + catch(Exception e) { + throw new RuntimeException("programming error in AddressingUtils#key"); + } + } + + //helper + private static int portFrom(URI address) { + return address.getPort()!=-1?address.getPort():80; + } + + //helper + private static int portFrom(URL address) { + return portFrom(URI.create(address.toExternalForm())); + } +} diff --git a/src/main/java/org/gcube/common/clients/builders/SingletonBuilderAPI.java b/src/main/java/org/gcube/common/clients/builders/SingletonBuilderAPI.java new file mode 100644 index 0000000..efae02b --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/SingletonBuilderAPI.java @@ -0,0 +1,121 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.queries.Query; + +/** + * Defines a DSL for proxy builders of singleton services, i.e. stateful services with a known single instance. + * + * @author Fabio Simeoni + * + */ +public class SingletonBuilderAPI { + + /** + * The first clause of the DSL. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param

the type of service proxies + */ + public static interface Builder { + + /** + * Configures a query for service instances. + * @param query the query + * @return further configuration options + */ + public SecondClause

matching(Query query); + + /** + * Configures the address of a given service endpoint. + * @param address the address of the endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(URL address) throws IllegalArgumentException; + + /** + * Configures the address of a given service endpoint. + * @param address the address of the endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(URI address) throws IllegalArgumentException; + + /** + * Configures the address of a given service instance. + * @param address the address of the corresponding service endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + + public SecondClause

at(String host, int port) throws IllegalArgumentException; + + } + + /** + * The second clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface SecondClause

{ + + /** + * Configures the timeout for the proxy. + * @param timeoutDuration the duration of the timeout + * @param timeoutUnit the time unit of the timeout + * @return further configuration options + */ + public FinalClause

withTimeout(int timeoutDuration, TimeUnit timeoutUnit); + + /** + * Set a configuration property for the proxy. + * @param name the name of the property + * @param value the value of the property + * @return further configuration options + * + * @param the type of the property value + */ + public SecondClause

with(String name, T value); + + /** + * Set a configuration property for the proxy. + * @param name the property + * @return further configuration options + */ + public SecondClause

with(Property property); + + /** + * Returns a configured proxy. + * @return the proxy + */ + public P build(); + } + + /** + * The final clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface FinalClause

{ + + /** + * Returns a configured proxy. + * @return the proxy + */ + public P build(); + + } + + +} diff --git a/src/main/java/org/gcube/common/clients/builders/StatefulBuilderAPI.java b/src/main/java/org/gcube/common/clients/builders/StatefulBuilderAPI.java new file mode 100644 index 0000000..1b2539b --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/StatefulBuilderAPI.java @@ -0,0 +1,134 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import javax.xml.ws.wsaddressing.W3CEndpointReference; + +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.queries.Query; + +/** + * Defines a DSL for proxy builders of stateful services. + * + * @author Fabio Simeoni + * + */ +public class StatefulBuilderAPI { + + /** + * The first clause of the DSL. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param

the type of service proxies + */ + public static interface Builder { + + /** + * Configures a query for service instances. + * @param query the query + * @return further configuration options + */ + public SecondClause

matching(Query query); + + /** + * Configures the address of a given service instance. + * @param address the address + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(W3CEndpointReference address) throws IllegalArgumentException; + + /** + * Configures the address of a given service instance. + * @param the key of the instance + * @param host the host of the corresponding service endpoint + * @param port the port of the corresponding service endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(String key, String host, int port) throws IllegalArgumentException; + + /** + * Configures the address of a given service instance. + * @param the key of the instance + * @param address the address of the corresponding service endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(String key, URL url) throws IllegalArgumentException; + + /** + * Configures the address of a given service instance. + * @param the key of the instance + * @param address the address of the corresponding service endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(String key, URI url) throws IllegalArgumentException; + + } + + /** + * The second clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface SecondClause

{ + + /** + * Configures the timeout for the proxy. + * @param timeoutDuration the duration of the timeout + * @param timeoutUnit the time unit of the timeout + * @return further configuration options + */ + public FinalClause

withTimeout(int timeoutDuration, TimeUnit timeoutUnit); + + /** + * Set a configuration property for the proxy. + * @param name the name of the property + * @param value the value of the property + * @return further configuration options + * + * @param the type of the property value + */ + public SecondClause

with(String name, T value); + + /** + * Set a configuration property for the proxy. + * @param name the property + * @return further configuration options + */ + public SecondClause

with(Property property); + + /** + * Returns a configured proxy. + * @return the proxy + */ + public P build(); + } + + /** + * The final clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface FinalClause

{ + + /** + * Returns a configured proxy. + * @return the proxy + */ + public P build(); + + } + + +} diff --git a/src/main/java/org/gcube/common/clients/builders/StatelessBuilderAPI.java b/src/main/java/org/gcube/common/clients/builders/StatelessBuilderAPI.java new file mode 100644 index 0000000..bcfd231 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/builders/StatelessBuilderAPI.java @@ -0,0 +1,129 @@ +package org.gcube.common.clients.builders; + +import java.net.URI; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import org.gcube.common.clients.config.Property; + +/** + * Defines a DSL for proxy builders of stateless services. + * + * @author Fabio Simeoni + * + */ +public class StatelessBuilderAPI { + + /** + * The first clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface Builder

{ + + + /** + * Configures the address of a given service instance. + * @param address the address of the corresponding service endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + + public SecondClause

at(String host, int port) throws IllegalArgumentException; + + /** + * Configures the address of a given service endpoint. + * @param address the address of the endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(URL address) throws IllegalArgumentException; + + /** + * Configures the address of a given service endpoint. + * @param address the address of the endpoint + * @return further configuration options + * @throws IllegalArgumentException if the address is invalid + */ + public SecondClause

at(URI address) throws IllegalArgumentException; + + /** + * Configures the timeout for the proxy. + * @param timeoutDuration the duration of the timeout + * @param timeoutUnit the time unit of the timeout + * @return further configuration options + */ + public FinalClause

withTimeout(int timeoutDuration, TimeUnit timeoutUnit); + + /** + * Set a configuration property for the proxy. + * @param name the name of the property + * @param value the value of the property + * @return further configuration options + * + * @param the type of the property value + */ + public Builder

with(String name, T value); + + /** + * Set a configuration property for the proxy. + * @param name the property + * @return further configuration options + */ + public Builder

with(Property property); + + /** + * Configures the timeout for the proxy. + * @param timeoutDuration the duration of the timeout + * @param timeoutUnit the time unit of the timeout + * @return further configuration options + */ + public P build(); + } + + /** + * The second clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface SecondClause

{ + + /** + * Configures the timeout for the proxy. + * @param timeoutDuration the duration of the timeout + * @param timeoutUnit the time unit of the timeout + * @return further configuration options + */ + public FinalClause

withTimeout(int timeoutDuration, TimeUnit timeoutUnit); + + /** + * Returns a configured proxy. + * @return the proxy + */ + public P build(); + } + + /** + * The final clause of the DSL. + * + * @author Fabio Simeoni + * + * @param

the type of service proxies + */ + public static interface FinalClause

{ + + /** + * Returns a configured proxy. + * @return the proxy + */ + public P build(); + + } + + + +} diff --git a/src/main/java/org/gcube/common/clients/cache/DefaultEndpointCache.java b/src/main/java/org/gcube/common/clients/cache/DefaultEndpointCache.java new file mode 100644 index 0000000..0fd9991 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/cache/DefaultEndpointCache.java @@ -0,0 +1,63 @@ +package org.gcube.common.clients.cache; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.gcube.common.clients.delegates.DiscoveryDelegate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base implementation of {@link EndpointCache}. + * + * @author Fabio Simeoni + * + * @param the type of the service addresses + * + * @see DiscoveryDelegate + */ +public class DefaultEndpointCache implements EndpointCache { + + private static Logger logger = LoggerFactory.getLogger(DefaultEndpointCache.class); + + /** Service map. */ + private Map cache = Collections.synchronizedMap(new HashMap()); + + @Override + public void clear(Key key) throws IllegalArgumentException { + + assertnotNull(key,"key"); + + logger.debug("clearing cache {} for {}",this,key); + + cache.put(key, null); + } + + @Override + public A get(Key key) throws IllegalArgumentException { + + assertnotNull(key,"key"); + + return cache.get(key); + } + + @Override + public void put(Key key, A address) throws IllegalArgumentException { + + assertnotNull(key,"key"); + assertnotNull(address,"address"); + + logger.debug("caching {} for {}",address,key); + + cache.put(key,address); + + } + + //helper + private void assertnotNull(Object object, String msg) throws IllegalArgumentException { + if (object==null) + throw new IllegalArgumentException(msg+" is null"); + } + +} diff --git a/src/main/java/org/gcube/common/clients/cache/EndpointCache.java b/src/main/java/org/gcube/common/clients/cache/EndpointCache.java new file mode 100644 index 0000000..165efc8 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/cache/EndpointCache.java @@ -0,0 +1,41 @@ +package org.gcube.common.clients.cache; + +import org.gcube.common.clients.delegates.DiscoveryDelegate; + +/** + * A cross-service cache of endpoint addresses used by {@link DiscoveryDelegate}s. + * + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * + * @see Key + * @see DiscoveryDelegate + * + */ +public interface EndpointCache { + + /** + * Resets the cache for a given {@link Key}. + * @param key the key + * @throws IllegalArgumentException if the key is null + */ + void clear(Key key) throws IllegalArgumentException; + + /** + * Returns the address cached for a given {@link Key} + * @param key the key + * @return the endpoint address, or null if there is no endpoint address cached for the service + * @throws IllegalArgumentException if the key is null + */ + A get(Key key) throws IllegalArgumentException; + + /** + * Caches an endpoint address for a given {@link Key} + * @param key the key + * @param address the address + * @throws IllegalArgumentException if the key or the address are null + */ + void put(Key Key,A address) throws IllegalArgumentException; +} diff --git a/src/main/java/org/gcube/common/clients/cache/Key.java b/src/main/java/org/gcube/common/clients/cache/Key.java new file mode 100644 index 0000000..a00ffef --- /dev/null +++ b/src/main/java/org/gcube/common/clients/cache/Key.java @@ -0,0 +1,81 @@ +package org.gcube.common.clients.cache; + +import org.gcube.common.clients.queries.Query; +import org.gcube.common.scope.api.ScopeProvider; + +/** + * Keys for cross-service {@link EndpointCache}s comprised of a service name, a {@link Query}, and a scope. + * + *

+ * Keys are value objects with informative string representations. + * + * @author Fabio Simeoni + * + */ +public final class Key { + + private final String name; + private final Query query; + private final String scope; + + /** + * Creates a {@link Key} with a given service name and {@link Query} + * @param name the name + * @param query the query + * @return the key + */ + public static Key key(String name, Query query) { + return new Key(name, query); + } + + //private + private Key(String name, Query query) { + this.name = name; + this.query = query; + this.scope = ScopeProvider.instance.get(); + } + + @Override + public String toString() { + return "Key [name=" + name + ", query=" + query + ", scope=" + scope + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((query == null) ? 0 : query.hashCode()); + result = prime * result + ((scope == null) ? 0 : scope.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Key other = (Key) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (query == null) { + if (other.query != null) + return false; + } else if (!query.equals(other.query)) + return false; + if (scope == null) { + if (other.scope != null) + return false; + } else if (!scope.equals(other.scope)) + return false; + return true; + } + + +} diff --git a/src/main/java/org/gcube/common/clients/config/AbstractConfig.java b/src/main/java/org/gcube/common/clients/config/AbstractConfig.java new file mode 100644 index 0000000..53bf9a4 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/config/AbstractConfig.java @@ -0,0 +1,69 @@ +package org.gcube.common.clients.config; + +import java.util.HashMap; +import java.util.Map; + +import org.gcube.common.clients.delegates.ProxyPlugin; + +/** + * Partial implementation of {@link ProxyConfig}. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + */ +public abstract class AbstractConfig implements ProxyConfig { + + private final ProxyPlugin plugin; + private final Map properties = new HashMap(); + + /** + * Creates an instance with a given {@link ProxyPlugin}. + * @param plugin the plugin + */ + protected AbstractConfig(ProxyPlugin plugin) { + this.plugin = plugin; + } + + @Override + public ProxyPlugin plugin() { + return plugin; + } + + @Override + public long timeout() throws IllegalArgumentException { + if (!hasProperty(Property.timeout)) + throw new IllegalArgumentException("timeout property is undefined"); + else + return property(Property.timeout,Long.class); + } + + + @Override + public void addProperty(String name, T value) { + properties.put(name, value); + } + + @Override + public void addProperty(Property property) { + properties.put(property.name(),property.value()); + } + + @Override + public boolean hasProperty(String property) { + return properties.containsKey(property); + } + + @Override + public T property(String property, Class clazz) throws IllegalStateException, IllegalArgumentException { + if (!hasProperty(property)) + throw new IllegalStateException(property+" is unknown"); + try { + return clazz.cast(properties.get(property)); + } + catch(Exception e) { + throw new IllegalArgumentException("could not retrieve "+property+" as "+clazz,e); + } + } +} diff --git a/src/main/java/org/gcube/common/clients/config/DiscoveryConfig.java b/src/main/java/org/gcube/common/clients/config/DiscoveryConfig.java new file mode 100644 index 0000000..85f628b --- /dev/null +++ b/src/main/java/org/gcube/common/clients/config/DiscoveryConfig.java @@ -0,0 +1,51 @@ +package org.gcube.common.clients.config; + +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.delegates.DiscoveryDelegate; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.queries.Query; + +/** + * The configuration of a proxy created in discovery mode. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * + * @see DiscoveryDelegate + * + */ +public class DiscoveryConfig extends AbstractConfig { + + private final Query query; + private final EndpointCache cache; + + /** + * Creates an instance with a given {@link ProxyPlugin}, {@link Query} and call timeout. + * @param plugin the plugin + * @param query the query + * @param the timeout + */ + public DiscoveryConfig(ProxyPlugin plugin,Query query, EndpointCache cache) { + super(plugin); + this.query=query; + this.cache=cache; + } + + /** + * Returns the address cache used by the proxy. + * @return the cache + */ + public EndpointCache cache() { + return cache; + } + + /** + * Returns the query used by the proxy. + * @return the query + */ + public Query query() { + return query; + } +} diff --git a/src/main/java/org/gcube/common/clients/config/EndpointConfig.java b/src/main/java/org/gcube/common/clients/config/EndpointConfig.java new file mode 100644 index 0000000..586c9d9 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/config/EndpointConfig.java @@ -0,0 +1,28 @@ +package org.gcube.common.clients.config; + +import org.gcube.common.clients.delegates.DirectDelegate; +import org.gcube.common.clients.delegates.ProxyPlugin; + +/** + * The configuration of a proxy created in direct mode. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * + * @see DirectDelegate + */ +public class EndpointConfig extends AbstractConfig { + + private final A address; + + public EndpointConfig(ProxyPlugin plugin, A address) { + super(plugin); + this.address=address; + } + + public A address() { + return address; + } +} diff --git a/src/main/java/org/gcube/common/clients/config/Property.java b/src/main/java/org/gcube/common/clients/config/Property.java new file mode 100644 index 0000000..ebc7339 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/config/Property.java @@ -0,0 +1,87 @@ +package org.gcube.common.clients.config; + +import java.util.concurrent.TimeUnit; + +import org.gcube.common.clients.builders.AbstractStatefulBuilder; +import org.gcube.common.clients.builders.AbstractStatelessBuilder; + + +/** + * A custom configuration property for proxies. + * + * @author Fabio Simeoni + * + * @see AbstractStatefulBuilder + * @see AbstractStatelessBuilder + * + * @param the type of the property value + */ +public class Property { + + /** + * The name of the call timeout {@link Property}. + */ + public static final String timeout = "timeout"; + + /** + * The name of the sticky session property {@link Property}. + */ + public static final String sticky_session = "sticky_session"; + + /** + * Return the call timeout {@link Property}. + * @param value the property value + * @return the property + */ + public static Property timeout(long value) { + return new Property(timeout,value); + } + + /** + * Return the call timeout {@link Property}. + * @param duration the duration of the timeout + * @param unit the time unit of the timeout + * @return the property + */ + public static Property timeout(long duration, TimeUnit unit) { + return new Property(timeout,unit.toMillis(duration)); + } + + /** + * Return the sticky session {@link Property}. + * @param value the property value + * @return the property + */ + public static Property sticky_session(boolean value) { + return new Property(sticky_session,value); + } + + private final String name; + private final T value; + + /** + * Creates an instance with a name and a value. + * @param name the name + * @param value the value + */ + public Property(String name, T value) { + this.name=name; + this.value=value; + } + + /** + * Returns the name of the property. + * @return the name + */ + public String name() { + return name; + } + + /** + * Returns the value of the property. + * @return the value + */ + public T value() { + return value; + } +} diff --git a/src/main/java/org/gcube/common/clients/config/ProxyConfig.java b/src/main/java/org/gcube/common/clients/config/ProxyConfig.java new file mode 100644 index 0000000..35d9532 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/config/ProxyConfig.java @@ -0,0 +1,62 @@ +package org.gcube.common.clients.config; + +import org.gcube.common.clients.delegates.ProxyPlugin; + +/** + * The configuration of service proxies. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + */ +public interface ProxyConfig { + + /** + * Returns the timeout. + * @return the timeout + */ + public long timeout(); + + + /** + * Returns the {@link ProxyPlugin}. + * @return the plugin + */ + public ProxyPlugin plugin(); + + /** + * Adds a custom property to the configuration. + * @param name the name of the property + * @param value the value of the property + * + * @param the type of the property value + */ + public void addProperty(String name, T value); + + /** + * Adds a custom property to the configuration. + * @param property the property + */ + public void addProperty(Property property); + + /** + * Returns true if the configuration includes a given custom property. + * @param property the name of the property + * @return + */ + public boolean hasProperty(String property); + + /** + * Returns the value of a given custom property. + * @param property the name of the property + * @param clazz the type of the property value + * @return the property value + * @throws IllegalStateException if the property is not included in the configuration + * @throws IllegalArgumentException if the property exists but its value has a different value + * + * * @param the type of the property value + */ + T property(String property, Class clazz) throws IllegalStateException, IllegalArgumentException; + +} diff --git a/src/main/java/org/gcube/common/clients/delegates/AbstractDelegate.java b/src/main/java/org/gcube/common/clients/delegates/AbstractDelegate.java new file mode 100644 index 0000000..08e8f4a --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/AbstractDelegate.java @@ -0,0 +1,35 @@ +package org.gcube.common.clients.delegates; + +import org.gcube.common.clients.config.ProxyConfig; + +/** + * Partial implementation of {@link ProxyDelegate}s + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * @param the type of {@link ProxyConfig} used by the delegate + */ +public abstract class AbstractDelegate> implements ProxyDelegate { + + private final C config; + + /** + * Constructs an instance with a given configuration + * @param config the configuration + */ + public AbstractDelegate(C config) { + this.config=config; + } + + @Override + public C config() { + return config; + } + + @Override + public String toString() { + return config().plugin().name()+"'s proxy"; + } +} diff --git a/src/main/java/org/gcube/common/clients/delegates/AsyncProxyDelegate.java b/src/main/java/org/gcube/common/clients/delegates/AsyncProxyDelegate.java new file mode 100644 index 0000000..22c1ae5 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/AsyncProxyDelegate.java @@ -0,0 +1,205 @@ +package org.gcube.common.clients.delegates; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.gcube.common.clients.Call; +import org.gcube.common.clients.config.ProxyConfig; +import org.gcube.common.scope.api.ScopeProvider; + +/** + * A {@link ProxyDelegate} that delivers the outcome of {@link Call}s asynchronously, either through polling or + * notifications. + *

+ * The delegates use {@link ExecutorService}s to make calls in separate threads. If required, clients may provide their own + * {@link ExecutorService}s at the point of call submission. + * + * @author Fabio Simeoni + * + * @param the type of service stubs + */ +public class AsyncProxyDelegate implements ProxyDelegate { + + // we try to cope with demand within holding on to threads that may never be used + private final static ExecutorService service = Executors.newCachedThreadPool(); + + // quits the default service when JVM does + static { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + service.shutdown(); + } + }); + } + + // the inner synchronous delegate + private final ProxyDelegate inner; + + /** + * Creates an instance with a (synchronous) {@link ProxyDelegate} + * + * @param delegate the delegate + */ + public AsyncProxyDelegate(ProxyDelegate delegate) { + this.inner = delegate; + } + + @Override + public V make(Call call) throws Exception { + return inner.make(call); + } + + @Override + public ProxyConfig config() { + return inner.config(); + } + + /** + * Makes a {@link Call} to a service endpoint asynchronously, returning a {@link Future} that clients can use to + * poll for and obtain the call outcome, or to cancel the call (assuming that the call is designed for cancellation + * or has not been made yet). + * + * @param call the {@link Call} to be made asynchronously + * @return the {@link Future} of the {@link Call} outcome + * + * @param the type of the value returned from the {@link Call} + * + * @throws RejectedExecutionException if the call cannot not be submitted for asynchronous execution + */ + public Future makeAsync(Call call) throws RejectedExecutionException { + + return makeAsync(call, service); + + } + + /** + * Makes a {@link Call} to a service endpoint asynchronously, returning a {@link Future} that clients can use to + * poll for and obtain the call outcome, or to cancel the call (assuming that the call is designed for cancellation + * or has not been made yet). + * + * @param call the {@link Call} to be executed asynchronously + * @param service a {@link ExecutorService} to which the {@link Call} should be submitted for execution + * + * @return the {@link Future} of the {@link Call} outcome + * + * @param the type of the value returned from the {@link Call} + * + * @throws RejectedExecutionException if the call cannot not be submitted for asynchronous execution + * + */ + public Future makeAsync(final Call call, ExecutorService service) throws RejectedExecutionException { + + final String callScope = ScopeProvider.instance.get(); + + // create task from call + Callable callTask = new Callable() { + + @Override + public V call() throws Exception { + + ScopeProvider.instance.set(callScope); + + return inner.make(call); + } + }; + + // submit task + return service.submit(callTask); + } + + /** + * Makes a {@link Call} to a service endpoint asynchronously, notifying a {@link Callback} of its outcome. Returns a + * {@link Future} that clients can use to cancel the execution of the call (assuming that the call is designed for + * cancellation or has not been made yet). + * + * @param call the {@link Call} + * @param callback the {@link Callback} + * + * @return the {@link Future} of call submission + * + * @throws RejectedExecutionException if the call cannot not be submitted for asynchronous execution + */ + public Future makeAsync(final Call call, final Callback callback) throws RejectedExecutionException { + + return makeAsync(call, callback, service); + } + + /** + * Makes a {@link Call} to a service endpoint asynchronously, notifying a {@link Callback} of its outcome. Returns a + * {@link Future} that clients can use to cancel the execution of the call (assuming that the call is designed for + * cancellation or has not been made yet). + * + * @param call the {@link Call} + * @param callback the {@link Callback} + * @param service the {@link ExecutorService} that executes the call + * + * @return the {@link Future} of call submission + * + * @throws RejectedExecutionException if the call cannot not be submitted for asynchronous execution + */ + public Future makeAsync(final Call call, final Callback callback, ExecutorService service) + throws RejectedExecutionException { + + // submit call + final Future callFuture = makeAsync(call, service); + + // create a task that blocks waiting on outome + Runnable waitingTask = new Runnable() { + + @Override + public void run() { + try { + + long timeout = callback.timeout(); + + V outcome = null; + + // honour callback timeout + if (timeout==0) + //not only may clients want to wait indefinitely, they may have set the timeout on proxy + outcome = callFuture.get(); + else + outcome = callFuture.get(timeout, TimeUnit.MILLISECONDS); + + // notify callback + callback.done(outcome); + + } catch (InterruptedException e) { + + // we assume client has cancelled task, hence do not notify it of its own actions + // but we reset the flag for other consumers, such as the executor service + // so clients do not need to worry about it. + Thread.currentThread().interrupt(); + + } catch (ExecutionException e) { + + // notify callback of underlying failure + // by now, it will have already beeen converted + callback.onFailure(e.getCause()); + + } catch (TimeoutException e) { + + // notify callback the required timeout has expired + callback.onFailure(e); + + // attempt to cancel the call, in case it's designed for it + callFuture.cancel(true); + + } + } + }; + + // submits waiting task + service.submit(waitingTask); + + // return call task future, rather than waiting task + return callFuture; + } +} diff --git a/src/main/java/org/gcube/common/clients/delegates/Callback.java b/src/main/java/org/gcube/common/clients/delegates/Callback.java new file mode 100644 index 0000000..4fd2b10 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/Callback.java @@ -0,0 +1,43 @@ +package org.gcube.common.clients.delegates; + +import java.util.concurrent.TimeoutException; + +import org.gcube.common.clients.Call; + +/** + * Asynchronous {@link Call} listeners. + * + * @author Fabio Simeoni + * + * @param the type of value returned by the call + * + * @see Call + */ +public interface Callback { + + /** + * Invoked when the value returned by the call is available. + * @param value the value + */ + void done(V value); + + /** + * Invoked when the call does not complete successfully. + *

+ * Failures may be generated by a {@link Call}s, or by the expiration of timeouts set on their + * asynchronous execution. In the latter case, the failures are {@link TimeoutException}s. + * + * @param failure the failure + */ + void onFailure(Throwable failure); + + + /** + * The time to wait on the value returned by the call. + * @return the timeout + */ + long timeout(); + + + +} diff --git a/src/main/java/org/gcube/common/clients/delegates/DirectDelegate.java b/src/main/java/org/gcube/common/clients/delegates/DirectDelegate.java new file mode 100644 index 0000000..ee02414 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/DirectDelegate.java @@ -0,0 +1,60 @@ +package org.gcube.common.clients.delegates; + +import org.gcube.common.clients.Call; +import org.gcube.common.clients.config.EndpointConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + + +/** + * A {@link ProxyDelegate} that sends {@link Call}s to service endpoints at known addresses. + *

+ * This is a no-op {@link ProxyDelegate}, i.e. it executes the {@link Call} interface and converts its faults. + * It exists to support uniform programming against the {@link ProxyDelegate} interface. + * + * @author Fabio Simeoni + * + * @param the of service addresses + * @param the type of service proxies + * + */ +public final class DirectDelegate extends AbstractDelegate> implements ProxyDelegate { + + private static Logger log = LoggerFactory.getLogger(DirectDelegate.class); + + /** + * Creates an instance with a {@link ProxyPlugin} and an endpoint address. + * @param plugin the plugin + * @param address the address + */ + public DirectDelegate(EndpointConfig config) { + super(config); + } + + @Override + public V make(Call call) throws Exception { + + ProxyPlugin plugin = config().plugin(); + + A address = config().address(); + + log.info("calling {} @ {}",plugin.name(),address); + + S stub =null; + try { + stub = plugin.resolve(address,config()); + } + catch(Exception e) { + throw new IllegalStateException("could not resolve "+address,e); + } + + try { + return call.call(stub); + } + catch(Exception fault) { + throw plugin.convert(fault,config()); + } + } + +} diff --git a/src/main/java/org/gcube/common/clients/delegates/DiscoveryDelegate.java b/src/main/java/org/gcube/common/clients/delegates/DiscoveryDelegate.java new file mode 100644 index 0000000..328ea12 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/DiscoveryDelegate.java @@ -0,0 +1,180 @@ +package org.gcube.common.clients.delegates; + +import static org.gcube.common.clients.cache.Key.*; + +import java.util.ArrayList; +import java.util.List; + +import org.gcube.common.clients.Call; +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.cache.Key; +import org.gcube.common.clients.config.DiscoveryConfig; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.exceptions.DiscoveryException; +import org.gcube.common.clients.queries.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link ProxyDelegate} that discovers service endpoints. + * + *

+ * + * The delegates attempt to make {@link Call}s to endpoints cached in an {@link EndpointCache}. + * If the calls fail, or the cache is empty, they execute a {@link Query} for endpoints and call the results in turn until the call succeeds or there are no + * more endpoints to call. If the call succeeds with one endpoint, the delegates cache the endpoint in the {@link EndpointCache}. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of service stubs + * + * @see Query + * @see EndpointCache + */ +public class DiscoveryDelegate extends AbstractDelegate> { + + private static Logger log = LoggerFactory.getLogger(DiscoveryDelegate.class); + + /** + * Creates an instance with a {@link ProxyPlugin}, a {@link Query}, and an {@link EndpointCache}. + * + * @param plugin the plugin + * @param query the query + * @param cache the cache + */ + public DiscoveryDelegate(DiscoveryConfig config) { + super(config); + } + + //helper + private boolean isAnchored() { + return config().hasProperty(Property.sticky_session) ? + config().property(Property.sticky_session,Boolean.class): + false; + } + + @Override + public V make(Call call) throws Exception { + + ProxyPlugin plugin = config().plugin(); + Query query = config().query(); + EndpointCache cache = config().cache(); + + // create key in call scope + Key key = key(plugin.name(),query); + + // try with each endpoint in turn + Exception lastFault = null; + + // use cache first, if any + A lgeAddress = cache.get(key); + + use_cache:if (lgeAddress != null) + + try { + + log.info("calling {} @ {} (cached)", plugin.name(),lgeAddress); + + S lge = null; + + try { + lge=plugin.resolve(lgeAddress,config()); + } + catch(Exception e) { + log.error("could not resolve "+lgeAddress,e); + lastFault = e; + break use_cache; + } + + return call.call(lge); + + } catch (Exception fault) { + + cache.clear(key); + + fault = plugin.convert(fault,config()); + + if (isUnrecoverable(fault) || isAnchored()) // exit now + throw fault; + else + lastFault = fault; //move on to querying + } + + List results = null; + + try { + + log.info("executing query for {} endpoints: {}", plugin.name(),query); + + // execute query + results = query.fire(); + + // exclude cached endpoint (we do not use 'remove' in case list implementation does not support it) + + results = filterResults(lgeAddress, results); + + if (results.size() == 0) + throw new DiscoveryException("no endpoints found for "+query); + + } + catch(DiscoveryException fault) { + if (lastFault == null) + throw fault; + else + throw lastFault; + } + + + //try with each endpoint in turn + for (A address : results) + try { + + log.info("calling {} @ {}", plugin.name(), address); + + S stub = null; + + try { + stub=plugin.resolve(address,config()); + } + catch(Exception e) { + log.error("could not resolve "+address,e); + lastFault = e; + } + + V result = call.call(stub); + + cache.put(key, address); + + return result; + + } catch (Exception fault) { + + lastFault= plugin.convert(fault,config()); + + if (isUnrecoverable(lastFault) || isAnchored()) // exit now + break; + } + + throw lastFault; + } + + //helper + private boolean isUnrecoverable(Exception e) { + + try { + return e.getClass().isAnnotationPresent(Unrecoverable.class); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + // helper + private List filterResults(A cached, List results) { + List endpoints = new ArrayList(); + for (A result : results) + if (!result.equals(cached)) + endpoints.add(result); + return endpoints; + } +} diff --git a/src/main/java/org/gcube/common/clients/delegates/MockDelegate.java b/src/main/java/org/gcube/common/clients/delegates/MockDelegate.java new file mode 100644 index 0000000..18f3db6 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/MockDelegate.java @@ -0,0 +1,52 @@ +package org.gcube.common.clients.delegates; + +import org.gcube.common.clients.Call; +import org.gcube.common.clients.builders.AbstractBuilder; +import org.gcube.common.clients.config.AbstractConfig; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.config.ProxyConfig; + +/** + * A {@link ProxyDelegate} for mock testing. + * + * @author Fabio Simeoni + * + * @param the type of service endpoint stubs used by {@link Call}s + */ +public class MockDelegate implements ProxyDelegate { + + /** + * Creates an instance with a {@link ProxyPlugin} and a mock endpoints. + * @param plugin the plugin + * @param endpoint the endpoint + */ + public static ProxyDelegate mockDelegate(ProxyPlugin plugin,S endpoint) { + return new MockDelegate(plugin, endpoint); + }; + + private ProxyConfig config; + private S mockEndpoint; + + + @SuppressWarnings("all") + private MockDelegate(ProxyPlugin plugin,S endpoint) { + this.config = new AbstractConfig(plugin) {}; + this.config().addProperty(Property.timeout(AbstractBuilder.defaultTimeout)); + this.mockEndpoint=endpoint; + } + + @Override + public V make(Call call) throws Exception { + try { + return call.call(mockEndpoint); + } + catch(Exception e) { + throw config.plugin().convert(e,config); + } + } + + @Override + public ProxyConfig config() { + return config; + } +} diff --git a/src/main/java/org/gcube/common/clients/delegates/ProxyDelegate.java b/src/main/java/org/gcube/common/clients/delegates/ProxyDelegate.java new file mode 100644 index 0000000..0f46e50 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/ProxyDelegate.java @@ -0,0 +1,38 @@ +package org.gcube.common.clients.delegates; + +import org.gcube.common.clients.Call; +import org.gcube.common.clients.config.ProxyConfig; + + +/** + * Makes {@link Call}s to service endpoints on behalf a service proxy. + *

+ * Delegates obtain the addresses of service endpoints according to some strategy, using information + * found in their {@link ProxyConfig}. + * + * @author Fabio Simeoni + * + * @param the type of service stubs + * + * @see Call + */ +public interface ProxyDelegate { + + /** + * Makes a {@link Call} to a given service endpoint. + * + * @param call the call + * @param the type of the value returned from the call + * @return the value returned from the call + * @throws Exception if the call fails + * + */ + V make(Call call) throws Exception; + + + /** + * Returns the configuration of the proxy. + * @return the configuration + */ + ProxyConfig config(); +} diff --git a/src/main/java/org/gcube/common/clients/delegates/ProxyPlugin.java b/src/main/java/org/gcube/common/clients/delegates/ProxyPlugin.java new file mode 100644 index 0000000..1988888 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/ProxyPlugin.java @@ -0,0 +1,56 @@ +package org.gcube.common.clients.delegates; + +import org.gcube.common.clients.Call; +import org.gcube.common.clients.config.ProxyConfig; + + + + +/** + * Provides information to customise the strategy of a {@link ProxyDelegate}. + * + * @author Fabio Simeoni + * + * @see Call + * @see ProxyDelegate + */ +public interface ProxyPlugin { + + /** + * Returns the name of the service. + * + * @return the name + */ + String name(); + + /** + * Returns the namespace of the service + * @return the namespace + */ + String namespace(); + + /** + * Converts a fault raised by a {@link Call} into a fault that can be recognised by {@link ProxyDelegate} clients. + * + * @param fault the original fault + * @return the converted fault + */ + Exception convert(Exception fault, ProxyConfig config); + + /** + * Returns a stub for a given service endpoint + * @param address the address of the endpoint + * @return the stub + * @throws Exception if the address cannot be resolved + */ + S resolve(A address, ProxyConfig config) throws Exception; + + + /** + * Returns a proxy with a given delegate + * @param delegate the delegate + * @return the proxy + */ + P newProxy(ProxyDelegate delegate); + +} diff --git a/src/main/java/org/gcube/common/clients/delegates/Unrecoverable.java b/src/main/java/org/gcube/common/clients/delegates/Unrecoverable.java new file mode 100644 index 0000000..50d2e41 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/delegates/Unrecoverable.java @@ -0,0 +1,24 @@ +package org.gcube.common.clients.delegates; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.gcube.common.clients.Call; + +/** + * Specified on {@link Call#call(Object)} to short-circuit endpoint iteration in the interaction strategy of {@link DiscoveryDelegate}s. + * + * @author Fabio Simeoni + * @see Call + * @see DiscoveryDelegate + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Inherited +@Documented +public @interface Unrecoverable {} diff --git a/src/main/java/org/gcube/common/clients/exceptions/DiscoveryException.java b/src/main/java/org/gcube/common/clients/exceptions/DiscoveryException.java new file mode 100644 index 0000000..7ba0c69 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/DiscoveryException.java @@ -0,0 +1,32 @@ +package org.gcube.common.clients.exceptions; + + +/** + * Raised when services endpoints cannot be discovered. + * + * @author Fabio Simeoni + * + */ +public class DiscoveryException extends ServiceException { + + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance with a message. + * @param msg the message + */ + public DiscoveryException(String msg) { + super(msg); + } + + /** + * Creates an instance from a cause. + * @param cause the cause + */ + public DiscoveryException(Throwable cause) { + super(cause); + } + + +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/FaultDSL.java b/src/main/java/org/gcube/common/clients/exceptions/FaultDSL.java new file mode 100644 index 0000000..10d3fe0 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/FaultDSL.java @@ -0,0 +1,116 @@ +package org.gcube.common.clients.exceptions; + +/** + * A simple DSL for fault conversion. + * + * @author Fabio Simeoni + * + */ +public class FaultDSL { + + /** + * Fault narrowing clause; + * @author Fabio Simeoni + * + */ + public static class AsClause { + + final Throwable fault; + + /** + * Creates an instance with the fault to narrow. + * @param fault the fault + */ + AsClause(Throwable fault) { + this.fault=fault; + } + + /** + * Throws a {@link ServiceException} caused by a given fault + * @param fault the fault + * @return unused, allows clients to throw invocations of this method + */ + public ServiceException asServiceException() { + + if (fault instanceof ServiceException) + throw (ServiceException) fault; + + throw new ServiceException(fault); + } + + /** + * Rethrows the fault with a narrower type, or wraps it in {@link ServiceException} if its type cannot be narrowed. + * @param clazz1 the narrower type + * @return unused, allows clients to throw invocations of this method + * @throws T1 the narrower type + */ + public ServiceException as(Class clazz1) throws T1 { + + if (clazz1.isInstance(fault)) throw clazz1.cast(fault); + + else return asServiceException(); + } + + /** + * Rethrows the fault with a narrower type, or wraps it in {@link ServiceException} if its type cannot be narrowed. + * @param clazz1 the narrower type + * @param clazz2 an alternative narrower type + * @return unused, allows clients to throw invocations of this method + * @throws T1 the narrower type + * @throws T2 the second narrower type + */ + public ServiceException as(Class clazz1,Class clazz2) throws T1,T2 { + + if (clazz2.isInstance(fault)) throw clazz2.cast(fault); + + else return as(clazz1); + } + + /** + * Rethrows the fault with a narrower type, or wraps it in {@link ServiceException} if its type cannot be narrowed. + * @param clazz1 the narrower type + * @param clazz2 an alternative narrower type + * @param clazz3 an alternative narrower type + * @return unused, allows clients to throw invocations of this method + * @throws T1 the narrower type + * @throws T2 the second narrower type + * @throws T3 the second narrower type + */ + public ServiceException as(Class clazz1,Class clazz2, Class clazz3) throws T1,T2,T3 { + + if (clazz3.isInstance(fault)) throw clazz3.cast(fault); + + else return as(clazz1,clazz2); + } + + /** + * Rethrows the fault with a narrower type, or wraps it in {@link ServiceException} if its type cannot be narrowed. + * @param clazz1 the narrower type + * @param clazz2 an alternative narrower type + * @param clazz3 an alternative narrower type + * @param clazz4 an alternative narrower type + * @return unused, allows clients to throw invocations of this method + * @throws T1 the narrower type + * @throws T2 the second narrower type + * @throws T3 the second narrower type + * @throws T4 the second narrower type + */ + public ServiceException as(Class clazz1,Class clazz2, Class clazz3, Class clazz4) throws T1,T2,T3,T4 { + + if (clazz4.isInstance(fault)) throw clazz4.cast(fault); + + else return as(clazz1,clazz2,clazz3); + } + } + + /** + * Indicates a fault to be rethrown with a narrower type. + * @param fault the fault + * @return the next clause in the sentence + */ + public static AsClause again(Throwable fault) { + + return new AsClause(fault); + + } +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/IllegalScopeException.java b/src/main/java/org/gcube/common/clients/exceptions/IllegalScopeException.java new file mode 100644 index 0000000..420692e --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/IllegalScopeException.java @@ -0,0 +1,27 @@ +package org.gcube.common.clients.exceptions; + +/** + * A {@link ServiceException} raised when attempt to contact service endpoints outside the scope. + * + * @author Fabio Simeoni + * + */ +public class IllegalScopeException extends InvalidRequestException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance. + */ + public IllegalScopeException(){ + super(); + } + + /** + * Creates an instance with a given message. + * @param msg the message + */ + public IllegalScopeException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/InvalidRequestException.java b/src/main/java/org/gcube/common/clients/exceptions/InvalidRequestException.java new file mode 100644 index 0000000..67ea94a --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/InvalidRequestException.java @@ -0,0 +1,48 @@ +package org.gcube.common.clients.exceptions; + +import org.gcube.common.clients.delegates.Unrecoverable; + +/** + * A {@link ServiceException} raised when client requests are invalid for services. + * + * @author Fabio Simeoni + * + */ +@Unrecoverable +public class InvalidRequestException extends ServiceException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance. + */ + public InvalidRequestException(){ + super(); + } + + /** + * Creates an instance with a given message. + * @param msg the message + */ + public InvalidRequestException(String msg) { + super(msg); + } + + /** + * Creates an instance from an underlying cause. + * + * @param cause the cause + */ + public InvalidRequestException(Throwable cause) { + super(cause); + } + + /** Creates an instance with a given message and an underlying cause. + * + * @param msg the message + * @param cause the cause + */ + public InvalidRequestException(String msg,Throwable cause) { + super(msg,cause); + } +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/NoSuchEndpointException.java b/src/main/java/org/gcube/common/clients/exceptions/NoSuchEndpointException.java new file mode 100644 index 0000000..06b5c3a --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/NoSuchEndpointException.java @@ -0,0 +1,20 @@ +package org.gcube.common.clients.exceptions; + +/** + * A {@link ServiceException} raised when services endpoints are not reachable. + * + * @author Fabio Simeoni + * + */ +public class NoSuchEndpointException extends ServiceException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance from an underlying cause. + * @param cause the cause + */ + public NoSuchEndpointException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/ServiceException.java b/src/main/java/org/gcube/common/clients/exceptions/ServiceException.java new file mode 100644 index 0000000..d7b154f --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/ServiceException.java @@ -0,0 +1,44 @@ +package org.gcube.common.clients.exceptions; + +/** + * An exceptions that occurs in the attempt to communicate with service endpoints. + * + * @author Fabio Simeoni + * + */ +public class ServiceException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance. + */ + public ServiceException() {} + + /** + * Creates an instance with a given message. + * + * @param msg the message + */ + public ServiceException(String msg) { + super(msg); + } + + /** + * Creates an instance from an underlying cause. + * + * @param cause the cause + */ + public ServiceException(Throwable cause) { + super(cause); + } + + /** Creates an instance with a given message and an underlying cause. + * + * @param msg the message + * @param cause the cause + */ + public ServiceException(String msg,Throwable cause) { + super(msg,cause); + } +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/UnsupportedOperationException.java b/src/main/java/org/gcube/common/clients/exceptions/UnsupportedOperationException.java new file mode 100644 index 0000000..f978263 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/UnsupportedOperationException.java @@ -0,0 +1,28 @@ +package org.gcube.common.clients.exceptions; + +import org.gcube.common.clients.delegates.Unrecoverable; + +/** + * Raised with requests to service operations that are not supported. + * + * @author Fabio Simeoni + * + */ +@Unrecoverable +public class UnsupportedOperationException extends InvalidRequestException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance. + */ + public UnsupportedOperationException() {} + + /** + * Creates an instance with a message. + * @param msg the message + */ + public UnsupportedOperationException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/gcube/common/clients/exceptions/UnsupportedRequestException.java b/src/main/java/org/gcube/common/clients/exceptions/UnsupportedRequestException.java new file mode 100644 index 0000000..a08962b --- /dev/null +++ b/src/main/java/org/gcube/common/clients/exceptions/UnsupportedRequestException.java @@ -0,0 +1,45 @@ +package org.gcube.common.clients.exceptions; + +import org.gcube.common.clients.delegates.Unrecoverable; + +/** + * Raised with requests to services which are not supported by the target plugin. + * + * @author Fabio Simeoni + * + */ +@Unrecoverable +public class UnsupportedRequestException extends InvalidRequestException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance. + */ + public UnsupportedRequestException() {} + + /** + * Creates an instance with a message. + * @param msg the message + */ + public UnsupportedRequestException(String msg) { + super(msg); + } + + /** + * Creates an instance with a message and a cause. + * @param msg the message + * @param cause the cause + */ + public UnsupportedRequestException(String msg, Throwable cause) { + super(msg,cause); + } + + /** + * Creates an instance with a cause. + * @param cause the cause + */ + public UnsupportedRequestException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/gcube/common/clients/queries/AbstractQuery.java b/src/main/java/org/gcube/common/clients/queries/AbstractQuery.java new file mode 100644 index 0000000..d69c1a0 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/queries/AbstractQuery.java @@ -0,0 +1,134 @@ +package org.gcube.common.clients.queries; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.exceptions.DiscoveryException; + +/** + * Partial implementation of {@link Query}s. + * + * @author Fabio Simeoni + * + * @param the type of service addresses + * @param the type of query results + */ +public abstract class AbstractQuery implements Query { + + private final ProxyPlugin plugin; + private final Map conditions = new HashMap(); + + //default matcher does not filter out any result + private ResultMatcher matcher = new ResultMatcher() { + @Override public boolean match(R doc) { + return true; + } + }; + + /** + * Creates an instance with a {@link ISFacade}, a {@link PluginAdapter}, and a type of IS queries + * @param facade the facade + * @param plugin the plugin + * @param queryClass the query type + */ + protected AbstractQuery(ProxyPlugin plugin) { + this.plugin = plugin; + } + + /** + * Adds a condition to the query. + * @param property an expression that identifies a property of service endpoints + * @param value the value of the property + */ + public void addCondition(String property, String value) { + this.conditions.put(property,value); + } + + /** + * Sets an {@link ResultMatcher} for the query. + * @param matcher the matcher. + */ + public void setMatcher(ResultMatcher matcher) { + this.matcher=matcher; + } + + @Override + public final List fire() throws DiscoveryException { + + //delegate actual execution to subclass-specific mechanisms + List results = fire(conditions); + + //from results to addresses + List endpoints = new ArrayList(); + + for (R result : results) + try { + if (matcher.match(result)) //should we include this? ask matcher + endpoints.add(address(result)); //extract address + } + catch(IllegalArgumentException e) { + //skip result, this is just a signal from subclasses + }; + + return endpoints; + } + + /** + * Executes the query through implementation-specific means. + * @param conditions the conditions to apply on the query prior to its execution + * @return the query results + * @throws DiscoveryException if the query could not be executed + */ + protected abstract List fire(Map conditions) throws DiscoveryException; + + /** + * Returns an endpoint address from a query result. + * @param result the result + * @return the address + * @throws IllegalArgumentException if an address cannot be derived from the result + */ + protected abstract A address(R result) throws IllegalArgumentException; + + /** + * Returns the {@link ProxyPlugin}. + * @return the plugin + */ + protected ProxyPlugin plugin() { + return plugin; + } + + //queries are value objects based on properties + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractQuery other = (AbstractQuery) obj; + if (conditions == null) { + if (other.conditions != null) + return false; + } else if (!conditions.equals(other.conditions)) + return false; + return true; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((conditions == null) ? 0 : conditions.hashCode()); + return result; + } + + @Override + public final String toString() { + return conditions.toString(); + } +} diff --git a/src/main/java/org/gcube/common/clients/queries/Query.java b/src/main/java/org/gcube/common/clients/queries/Query.java new file mode 100644 index 0000000..b83fbf6 --- /dev/null +++ b/src/main/java/org/gcube/common/clients/queries/Query.java @@ -0,0 +1,37 @@ +package org.gcube.common.clients.queries; + +import java.util.List; + +import org.gcube.common.clients.exceptions.DiscoveryException; + + + +/** + * A query for the endpoints of a given service. + * + * @author Fabio Simeoni + * + * @param the type of service endpoint addresses + */ +public interface Query { + + /** + * Executes the query. + * + * @return the addresses of the discovered endpoints + * @throws DiscoveryException if query execution fails + */ + List fire() throws DiscoveryException; + + + //emphasise + + @Override + public boolean equals(Object query); + + @Override + public int hashCode(); + + @Override + public String toString(); +} diff --git a/src/main/java/org/gcube/common/clients/queries/ResultMatcher.java b/src/main/java/org/gcube/common/clients/queries/ResultMatcher.java new file mode 100644 index 0000000..5c8cb8c --- /dev/null +++ b/src/main/java/org/gcube/common/clients/queries/ResultMatcher.java @@ -0,0 +1,23 @@ +package org.gcube.common.clients.queries; + + +/** + * A callback to filter out {@link Query} results. + * + * @author Fabio Simeoni + * + * + * @param the type of query results + * + * @see AbstractQuery + * + */ +public interface ResultMatcher { + + /** + * Returns true if the result should be retained. + * @param result the result + * @return true if the result should be retained + */ + boolean match(R result); +} diff --git a/src/test/java/org/gcube/common/clients/AsyncDelegateTest.java b/src/test/java/org/gcube/common/clients/AsyncDelegateTest.java new file mode 100644 index 0000000..861dcbb --- /dev/null +++ b/src/test/java/org/gcube/common/clients/AsyncDelegateTest.java @@ -0,0 +1,270 @@ +package org.gcube.common.clients; + +import static java.util.concurrent.TimeUnit.*; +import static junit.framework.Assert.*; +import static org.gcube.common.clients.delegates.MockDelegate.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.gcube.common.clients.delegates.AsyncProxyDelegate; +import org.gcube.common.clients.delegates.Callback; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.scope.api.ScopeProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +@RunWith(MockitoJUnitRunner.class) +public class AsyncDelegateTest { + + AsyncProxyDelegate delegate; + + @Mock ProxyPlugin plugin; + @Mock Object endpoint; + + @Mock Call call; + + @Mock Object value; + @Mock Exception original; + @Mock Exception converted; + + @Before + @SuppressWarnings("all") + public void setup() throws Exception { + + + //create subject-under-testing + delegate =new AsyncProxyDelegate(mockDelegate(plugin,endpoint)); + + //common configuration staging: mocking a delegate is not that immediate.. + when(plugin.name()).thenReturn("some service"); + when(plugin.convert(original,delegate.config())).thenReturn(converted); + + + } + + @Test + public void asyncCallsReturnFutureValues() throws Exception { + + //stage call + when(call.call(endpoint)).thenReturn(value); + + Future future = delegate.makeAsync(call); + + Object output = future.get(); + + assertEquals(value,output); + + assertFalse(future.isCancelled()); + + assertTrue(future.isDone()); + } + + @Test + public void asyncCallsTimeout() throws Exception { + + //stage call + Answer slowly = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Thread.sleep(300); //simulate longer process within call timeout + return value; + } + }; + when(call.call(endpoint)).thenAnswer(slowly); + + Future future = delegate.makeAsync(call); + + try { + future.get(100,TimeUnit.MILLISECONDS); + fail(); + } + catch(TimeoutException e) {} + + } + + @Test + public void asyncCallsExecuteInCallScope() throws Exception { + + final String scope = "a/b/c"; + ScopeProvider.instance.set(scope); + + //stage call + Answer checkingScope= new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + assertEquals(scope,ScopeProvider.instance.get()); + return value; + } + }; + + when(call.call(endpoint)).thenAnswer(checkingScope); + + Future future = delegate.makeAsync(call); + + future.get(); + } + + @Test + public void asyncCallReturnConvertedFaultsAsInnerCauses() throws Exception { + + //stage call + when(call.call(endpoint)).thenThrow(original); + + Future future = delegate.makeAsync(call); + + try { + future.get(); + fail(); + } + catch(Exception fault) { + assertEquals(converted,fault.getCause()); + } + + } + + @Test + public void asyncCallsAreInterrupted() throws Exception { + + final String scope = "a/b/c"; + ScopeProvider.instance.set(scope); + + //stage call + Answer slowly = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Thread.sleep(300); //simulate longer process within call timeout + return value; + } + }; + when(call.call(endpoint)).thenAnswer(slowly); + + final Future future = delegate.makeAsync(call); + + new Thread() { + public void run() { + + future.cancel(true); + + }; + }.start(); + + try { + future.get(); + fail(); + } + catch(CancellationException fault) { + assertTrue(future.isCancelled()); + } + + } + + @Test + public void callbacksGetResults() throws Exception { + + final CountDownLatch latch = new CountDownLatch(1); + + //stage call + Answer answer = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + latch.countDown(); + return value; + } + }; + + when(call.call(endpoint)).thenAnswer(answer); + + @SuppressWarnings("all") + Callback callback = mock(Callback.class); + + Future future = delegate.makeAsync(call,callback); + + //make sure we test after call has been delivered + latch.await(1, SECONDS); + + verify(callback).done(value); + + assertTrue(future.isDone()); + + } + + @Test + public void callbacksGetTimeoutErrors() throws Exception { + + //stage call + Answer slowly = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Thread.sleep(1000); + return value; + } + }; + + when(call.call(endpoint)).thenAnswer(slowly); + + @SuppressWarnings("all") + Callback callback = mock(Callback.class); + when(callback.timeout()).thenReturn(50L); + + Future future = delegate.makeAsync(call,callback); + + final CountDownLatch latch = new CountDownLatch(1); + + Answer unblock = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + latch.countDown(); + return value; + } + }; + + doAnswer(unblock).when(callback).onFailure(any(TimeoutException.class)); + + //makes sure callback has been invoked + latch.await(1,SECONDS); + + assertTrue(future.isCancelled()); + } + + @Test + public void callbacksGetFaults() throws Exception { + + when(call.call(endpoint)).thenThrow(original); + + @SuppressWarnings("all") + Callback callback = mock(Callback.class); + + final CountDownLatch latch = new CountDownLatch(1); + + Answer unblock = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Throwable e = (Throwable) invocation.getArguments()[0]; + assertEquals(converted,e); + //male sure this method has been invoked + latch.countDown(); + return null; + } + }; + + doAnswer(unblock).when(callback).onFailure(any(Throwable.class)); + + delegate.makeAsync(call,callback); + + //makes sure callback has been invoked + latch.await(1,SECONDS); + + } + +} diff --git a/src/test/java/org/gcube/common/clients/BuildersTest.java b/src/test/java/org/gcube/common/clients/BuildersTest.java new file mode 100644 index 0000000..5bdba83 --- /dev/null +++ b/src/test/java/org/gcube/common/clients/BuildersTest.java @@ -0,0 +1,229 @@ +package org.gcube.common.clients; + +import static java.util.concurrent.TimeUnit.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.StringWriter; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.xml.transform.stream.StreamResult; +import javax.xml.ws.wsaddressing.W3CEndpointReference; + +import org.gcube.common.clients.builders.AbstractBuilder; +import org.gcube.common.clients.builders.AbstractStatefulBuilder; +import org.gcube.common.clients.builders.AbstractStatelessBuilder; +import org.gcube.common.clients.builders.AddressingUtils; +import org.gcube.common.clients.builders.StatefulBuilderAPI; +import org.gcube.common.clients.builders.StatelessBuilderAPI; +import org.gcube.common.clients.cache.DefaultEndpointCache; +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.config.DiscoveryConfig; +import org.gcube.common.clients.config.EndpointConfig; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.config.ProxyConfig; +import org.gcube.common.clients.delegates.DirectDelegate; +import org.gcube.common.clients.delegates.DiscoveryDelegate; +import org.gcube.common.clients.delegates.ProxyDelegate; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.queries.Query; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +@RunWith(MockitoJUnitRunner.class) +public class BuildersTest { + + @Mock ProxyPlugin plugin; + + @Mock(name="some cache") EndpointCache cache; + @Mock(name="sample query") Query query; + + URI uriAddress = URI.create("http://foobar.org"); + + Property testProp = new Property("name","value"); + + StatelessBuilderAPI.Builder statelessBuilder; + StatefulBuilderAPI.Builder statefulBuilder; + + //we build these proxies that give access to delegate so that we can make assertions + class SampleProxy { + + ProxyDelegate delegate; + + public SampleProxy(ProxyDelegate delegate) { + this.delegate=delegate; + } + } + + class SampleStatelessBuilder extends AbstractStatelessBuilder { + + public SampleStatelessBuilder(EndpointCache cache,Query query, Property...properties) { + super(plugin, cache, query,properties); + } + + @Override protected W3CEndpointReference convertAddress(W3CEndpointReference address) { + return address; + } + + @Override + protected String contextPath() { + return "/context/path/"; + } + }; + + class SampleStatefulBuilder extends AbstractStatefulBuilder { + + public SampleStatefulBuilder(EndpointCache cache, Property...properties) { + super(plugin,cache,properties); + } + + @Override protected W3CEndpointReference convertAddress(W3CEndpointReference address) { + return address; + } + + @Override + protected String contextPath() { + return "/context/path/"; + } + }; + + @Before + @SuppressWarnings("all") + public void setup() throws Exception { + + when(plugin.name()).thenReturn("someservice"); + when(plugin.namespace()).thenReturn("http://acme.org"); + when(plugin.newProxy(any(ProxyDelegate.class))).thenAnswer(new Answer() { + public SampleProxy answer(InvocationOnMock invocation) throws Throwable { + return new SampleProxy((ProxyDelegate) invocation.getArguments()[0]); + } + }); + + } + + @Test + public void buildersBuildStatelessDirectProxy() { + + statelessBuilder = new SampleStatelessBuilder(cache,query); + + SampleProxy proxy = statelessBuilder.at(uriAddress).build(); + + ProxyDelegate delegate = proxy.delegate; + + assertTrue(delegate instanceof DirectDelegate); + + ProxyConfig config = delegate.config(); + assertEquals(AbstractBuilder.defaultTimeout, config.timeout()); + assertEquals(plugin, config.plugin()); + assertTrue(config instanceof EndpointConfig); + + @SuppressWarnings("unchecked") + EndpointConfig econfig = (EndpointConfig) config; + + StringWriter w1 = new StringWriter(); + StringWriter w2 = new StringWriter(); + AddressingUtils.address("/context/path/",plugin.name(),uriAddress).writeTo(new StreamResult(w1)); + econfig.address().writeTo(new StreamResult(w2)); + + assertEquals(w1.toString(),w2.toString()); + } + + @Test + public void buildersBuildStatefulDirectProxy() throws Exception { + + //with new timeout default + statefulBuilder = new SampleStatefulBuilder(cache,Property.timeout(15,SECONDS)); + + //with client-driven timeout + SampleProxy proxy = statefulBuilder.at("key",uriAddress.toURL()).with(Property.timeout(20,SECONDS)).build(); + + ProxyDelegate delegate = proxy.delegate; + + assertTrue(delegate instanceof DirectDelegate); + + ProxyConfig config = delegate.config(); + + //client-driven timeout wins + assertEquals((int)TimeUnit.SECONDS.toMillis(20), config.timeout()); + + assertEquals(plugin, config.plugin()); + + assertTrue(config instanceof EndpointConfig); + + @SuppressWarnings("unchecked") + EndpointConfig econfig = (EndpointConfig) config; + + StringWriter w1 = new StringWriter(); + StringWriter w2 = new StringWriter(); + AddressingUtils.address("/context/path/",plugin.name(),plugin.namespace(),"key",uriAddress).writeTo(new StreamResult(w1)); + econfig.address().writeTo(new StreamResult(w2)); + + System.out.println(w2.toString()); + assertEquals(w1.toString(),w2.toString()); + } + + @Test + public void buildersBuildStatelessDiscoveryProxy() throws Exception { + + EndpointCache newCache = new DefaultEndpointCache(); + + statelessBuilder = new SampleStatelessBuilder(newCache,query,Property.timeout(15,SECONDS),testProp); + + SampleProxy proxy = statelessBuilder.build(); + + ProxyDelegate delegate = proxy.delegate; + + assertTrue(delegate instanceof DiscoveryDelegate); + + ProxyConfig config = delegate.config(); + + assertEquals(TimeUnit.SECONDS.toMillis(15), config.timeout()); + assertTrue(config.hasProperty("name")); + assertEquals(config.property("name",String.class),"value"); + + assertEquals(plugin, config.plugin()); + + assertTrue(config instanceof DiscoveryConfig); + + @SuppressWarnings("unchecked") + DiscoveryConfig dconfig = (DiscoveryConfig) config; + + assertEquals(newCache, dconfig.cache()); + assertEquals(query, dconfig.query()); + } + + + @Test + public void buildersBuildStatefulDiscoveryProxy() { + + statefulBuilder = new SampleStatefulBuilder(cache); + + SampleProxy proxy = statefulBuilder.matching(query).with(testProp).withTimeout(15,SECONDS).build(); + + ProxyDelegate delegate = proxy.delegate; + + assertTrue(delegate instanceof DiscoveryDelegate); + + ProxyConfig config = delegate.config(); + + assertEquals(TimeUnit.SECONDS.toMillis(15), config.timeout()); + assertTrue(config.hasProperty(testProp.name())); + assertEquals(config.property(testProp.name(),Object.class),testProp.value()); + + assertEquals(plugin, config.plugin()); + + assertTrue(config instanceof DiscoveryConfig); + + @SuppressWarnings("unchecked") + DiscoveryConfig dconfig = (DiscoveryConfig) config; + + assertEquals(query, dconfig.query()); + } +} diff --git a/src/test/java/org/gcube/common/clients/DirectDelegateTest.java b/src/test/java/org/gcube/common/clients/DirectDelegateTest.java new file mode 100644 index 0000000..6414428 --- /dev/null +++ b/src/test/java/org/gcube/common/clients/DirectDelegateTest.java @@ -0,0 +1,79 @@ +package org.gcube.common.clients; + +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +import org.gcube.common.clients.config.EndpointConfig; +import org.gcube.common.clients.delegates.DirectDelegate; +import org.gcube.common.clients.delegates.ProxyDelegate; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DirectDelegateTest { + + @Mock Call call; + @Mock ProxyPlugin plugin; + @Mock EndpointConfig config; + @Mock(name="some address") Object address; + @Mock Object endpoint; + @Mock Object value; + @Mock Exception original; + @Mock Exception converted; + + + ProxyDelegate delegate; + + @Before + @SuppressWarnings("all") + public void setup() throws Exception { + + //common configuration staging: mocking a delegate is not that immediate.. + when(config.plugin()).thenReturn((ProxyPlugin) plugin); + when(config.address()).thenReturn(address); + when(plugin.name()).thenReturn("some service"); + when(plugin.convert(original,config)).thenReturn(converted); + when(plugin.resolve(address,config)).thenReturn(endpoint); + + //create subject-under-testing + delegate = new DirectDelegate(config); + + } + + @Test + public void proxiesResolveAddressesAndMakeCalls() throws Exception { + + //stage call + when(call.call(endpoint)).thenReturn(value); + + Object output = delegate.make(call); + + assertEquals(value,output); + + //note: this ensures that proxy has resolved address + + } + + @Test + public void proxiesConvertAndReturnFaults() throws Exception { + + //stage call + when(call.call(endpoint)).thenThrow(original); + + + //exercise client passes on failures + try { + delegate.make(call); + fail(); + } + catch(Exception fault) { + assertEquals(converted,fault); + } + + } + +} diff --git a/src/test/java/org/gcube/common/clients/DiscoveryDelegateTest.java b/src/test/java/org/gcube/common/clients/DiscoveryDelegateTest.java new file mode 100644 index 0000000..1a50f26 --- /dev/null +++ b/src/test/java/org/gcube/common/clients/DiscoveryDelegateTest.java @@ -0,0 +1,293 @@ +package org.gcube.common.clients; + +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static junit.framework.Assert.*; +import static org.gcube.common.clients.cache.Key.*; +import static org.mockito.Mockito.*; + +import org.gcube.common.clients.cache.DefaultEndpointCache; +import org.gcube.common.clients.cache.EndpointCache; +import org.gcube.common.clients.cache.Key; +import org.gcube.common.clients.config.DiscoveryConfig; +import org.gcube.common.clients.config.Property; +import org.gcube.common.clients.delegates.DiscoveryDelegate; +import org.gcube.common.clients.delegates.ProxyDelegate; +import org.gcube.common.clients.delegates.ProxyPlugin; +import org.gcube.common.clients.delegates.Unrecoverable; +import org.gcube.common.clients.exceptions.DiscoveryException; +import org.gcube.common.clients.queries.Query; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DiscoveryDelegateTest { + + @Mock Call call; + @Mock ProxyPlugin plugin; + @Mock Query query; + @Mock DiscoveryConfig config; + + @Mock Object endpoint; + @Mock Object endpoint2; + + @Mock(name="some address") Object address; + @Mock(name="some address2") Object address2; + + @Mock(name="some value") Object value; + @Mock(name="some value2") Object value2; + + @Mock(name="some recoverable fault") Exception recoverable; + @Mock(name="some unrecoverable fault") Exception unrecoverable;; + + EndpointCache cache = new DefaultEndpointCache(); + Key key; + + ProxyDelegate delegate; + + @Unrecoverable + public static class UnrecoverableFault extends Exception { + private static final long serialVersionUID = 1L; + } + + @Before + @SuppressWarnings("all") + public void setup() throws Exception { + + String serviceName = "some service"; + + //common staging for delegate + when(plugin.name()).thenReturn(serviceName); + when(plugin.convert(recoverable,config)).thenReturn(recoverable); + when(plugin.convert(unrecoverable,config)).thenReturn(new UnrecoverableFault()); + + when(config.plugin()).thenReturn((ProxyPlugin)plugin); + when(config.query()).thenReturn(query); + when(config.cache()).thenReturn(cache); + + when(plugin.resolve(address,config)).thenReturn(endpoint); + when(plugin.resolve(address2,config)).thenReturn(endpoint2); + + key = key(serviceName,query); + + delegate = new DiscoveryDelegate(config); + + } + + @Test + public void proxiesDiscoverCallAndCacheEndpoints() throws Exception { + + //stage + when(query.fire()).thenReturn(asList(address)); + when(call.call(endpoint)).thenReturn(value); + + //exercise client + Object output = delegate.make(call); + + //address is discovered, resolved into an endpoint and endpoint is called + assertEquals(output,value); + + //endpoint is cached + assertEquals(address,cache.get(key)); + + } + + @Test + public void proxiesUseLGEBeforeExecutingQueries() throws Exception { + + //stage + when(query.fire()).thenReturn(asList(address)); + when(call.call(endpoint)).thenReturn(value); + + //cache LGE + cache.put(key, address); + + //exercise client + delegate.make(call); + + //query is never fired + verify(query,never()).fire(); + + } + + @Test + public void proxiesStopAndClearCacheIfLGEFailsUnrecoverably() throws Exception { + + //stage + when(call.call(endpoint)).thenThrow(unrecoverable); + + //cache LGE + cache.put(key, address); + + try { + //exercise client + delegate.make(call); + fail(); + } + catch(UnrecoverableFault fault) { + //fault has been converted + } + + //query was never fired + verify(query,never()).fire(); + + //cache has been cleared + assertNull(cache.get(key)); + + } + + @Test + public void proxiesDiscoverOtherEndpointsWhenLGEFailsRecoverably() throws Exception { + + //stage + when(query.fire()).thenReturn(asList(address,address2)); + when(call.call(endpoint)).thenThrow(recoverable); + when(call.call(endpoint2)).thenReturn(value2); + + //cache LGE + cache.put(key, address); + + //exercise client + Object output = delegate.make(call); + + //LGE fails, query is executed, LGE is filtered from results + //and remaining endpoint is successfully invoked + assertEquals(output,value2); + + //cache has new LGE + assertEquals(address2,cache.get(key)); + + } + + + @Test + public void proxiesReturnLGEFailureIfQueryFails() throws Exception { + + //stage + when(query.fire()).thenThrow(new DiscoveryException("error")); + when(call.call(endpoint)).thenThrow(recoverable); + + //cache LGE + cache.put(key, address); + + //exercise client + try { + delegate.make(call); + fail(); + } + catch(DiscoveryException unexpected) { + fail(); + } + catch(Exception LGEfault) { + //fault has been converted + } + + } + + @Test + public void proxiesReturnLGEFailureIfQueryHasNoResults() throws Exception { + + //stage + when(query.fire()).thenReturn(emptyList()); + when(call.call(endpoint)).thenThrow(recoverable); + + //cache LGE + cache.put(key, address); + + //exercise client + try { + delegate.make(call); + fail(); + } + catch(DiscoveryException unexpected) { + fail(); + } + catch(Exception LGEfault) { + //fault has been converted + } + + } + + @Test + public void proxiesReturnQueryFailureIfLGEIsUndefined() throws Exception { + + //stage + when(query.fire()).thenThrow(new DiscoveryException("error")); + + //exercise client + try { + delegate.make(call); + fail(); + } + catch(DiscoveryException fault) {} + + } + + @Test + public void proxiesReturnFailureIfQueryHasNoResults() throws Exception { + + //stage + when(query.fire()).thenReturn(emptyList()); + + //exercise client + try { + delegate.make(call); + fail(); + } + catch(DiscoveryException fault) {} + + } + + @Test + public void proxiesRecoverIfQueryResultFailsRecoverably() throws Exception { + + //stage + when(query.fire()).thenReturn(asList(address,address2)); + when(call.call(endpoint)).thenThrow(recoverable); + when(call.call(endpoint2)).thenReturn(value2); + + Object output = delegate.make(call); + + assertEquals(output,value2); + } + + @Test + public void proxiesStopIfQueryResultFailsUnrecoverably() throws Exception { + + //stage + when(query.fire()).thenReturn(asList(address,address2)); + when(call.call(endpoint)).thenThrow(unrecoverable); + when(call.call(endpoint2)).thenReturn(value2); + + try { + delegate.make(call); + fail(); + } + catch(UnrecoverableFault fault){} + + } + + @Test + public void proxiesStopIfCallFailsRecoverablyButSessionIsSticky() throws Exception { + + //stage + when(query.fire()).thenReturn(asList(address,address2)); + + when(call.call(endpoint)).thenThrow(recoverable); + when(call.call(endpoint2)).thenReturn(value2); + + when(config.hasProperty(Property.sticky_session)).thenReturn(true); + when(config.property(Property.sticky_session,Boolean.class)).thenReturn(true); + + try { + delegate.make(call); + fail(); + } + catch(Exception fault){} + + } + +}