diff --git a/.classpath b/.classpath index 0f53f3e..534b5e5 100644 --- a/.classpath +++ b/.classpath @@ -1,10 +1,36 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distro/changelog.xml b/distro/changelog.xml index bc6e67b..0d311a0 100644 --- a/distro/changelog.xml +++ b/distro/changelog.xml @@ -1,5 +1,11 @@ - + First Release + + Scope can now be propagated in Callable and Runnable tasks via ScopedTasks + Scopes can now be manipulated through ScopeBean objects + Scope maps are now versioned and newer versions take precedence at startup + Scope maps can now be successfully looked up in VRE scopes (bug fix) + \ No newline at end of file diff --git a/pom.xml b/pom.xml index f5b33d3..9b57227 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.gcube.core common-scope - 1.0.0-SNAPSHOT + 1.1.0-SNAPSHOT Common Scope Scope-related APIs @@ -33,6 +33,14 @@ 0.9.6 + + + org.gcube.core + common-scope-maps + [1.0.0-SNAPSHOT,) + compile + + junit junit diff --git a/src/main/java/org/gcube/common/scope/api/ServiceMap.java b/src/main/java/org/gcube/common/scope/api/ServiceMap.java index 536abf0..9c7bfa0 100644 --- a/src/main/java/org/gcube/common/scope/api/ServiceMap.java +++ b/src/main/java/org/gcube/common/scope/api/ServiceMap.java @@ -1,7 +1,5 @@ package org.gcube.common.scope.api; -import java.util.List; - import org.gcube.common.scope.impl.ScopedServiceMap; /** @@ -18,15 +16,25 @@ public interface ServiceMap { public static final ServiceMap instance = new ScopedServiceMap(); /** - * Returns the endpoints of a given service. + * Returns the endpoint of a given service. * @param service the service - * @return the endpoints, or null if the service is unknown. + * @return the endpoint + * @throws IllegalArgumentException if the service has no endpoint in the map + * @throws IllegalStateException if the service endpoint cannot be returned */ - List endpoint(String service); + String endpoint(String service); /** * Returns the associated scope. * @return the scope + * @throws IllegalStateException if the scope of the map cannot be returned */ String scope(); + + /** + * Returns the release version of the map. + * @return the version + * @throws IllegalStateException if the version of the map cannot be returned + */ + String version(); } diff --git a/src/main/java/org/gcube/common/scope/impl/DefaultServiceMap.java b/src/main/java/org/gcube/common/scope/impl/DefaultServiceMap.java index 2128fd5..a58e13c 100644 --- a/src/main/java/org/gcube/common/scope/impl/DefaultServiceMap.java +++ b/src/main/java/org/gcube/common/scope/impl/DefaultServiceMap.java @@ -1,7 +1,6 @@ package org.gcube.common.scope.impl; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import javax.xml.bind.annotation.XmlAttribute; @@ -22,8 +21,11 @@ public class DefaultServiceMap implements ServiceMap { @XmlAttribute private String scope; + @XmlAttribute + private String version; + @XmlJavaTypeAdapter(ServiceMapAdapter.class) - Map> services = new LinkedHashMap>(); + Map services = new LinkedHashMap(); @Override public String scope() { @@ -31,8 +33,19 @@ public class DefaultServiceMap implements ServiceMap { } @Override - public List endpoint(String service) { - return services.get(service); + public String version() { + return version; + } + + @Override + public String endpoint(String service) { + + String endpoint = services.get(service); + + if (endpoint==null) + throw new IllegalArgumentException("unknown service "+service); + + return endpoint; } diff --git a/src/main/java/org/gcube/common/scope/impl/ScopeBean.java b/src/main/java/org/gcube/common/scope/impl/ScopeBean.java new file mode 100644 index 0000000..682fb5f --- /dev/null +++ b/src/main/java/org/gcube/common/scope/impl/ScopeBean.java @@ -0,0 +1,139 @@ +package org.gcube.common.scope.impl; + + + +/** + * A object model of a scope. + * + * @author Fabio Simeoni + * + */ +public class ScopeBean { + + /** + * Scope separators used in linear syntax. + */ + protected static String separator = "/"; + + /** + * Scope types * + */ + public static enum Type implements Comparable {VRE,VO,INFRASTRUCTURE} + + /** + * The name of the scope. + */ + private String name; + + /** + * The type of the scope. + */ + private Type type; + + /** + * The enclosing scope, if any. + */ + private ScopeBean enclosingScope; + + + /** + * Returns the name of the scope. + * @return the name + */ + public String name() { + return name; + } + + /** + * Returns true if the scope has a given {@link Type}. + * @param type the type + * @return true if the scope has the given type, false otherwise + */ + public boolean is(Type type) { + return this.type.equals(type); + } + + /** + * Returns the {@link Type} of the scope. + * @return the type + */ + public Type type() { + return type; + } + + /** + * Returns the enclosing scope, if any. + * @return the enclosing scope, or null if the scope is top-level + */ + public ScopeBean enclosingScope() { + return enclosingScope; + } + + public ScopeBean(String scope) throws IllegalArgumentException { + + String[] components=scope.split(separator); + + if (components.length<2 || components.length>4) + throw new IllegalArgumentException("scope "+scope+" is malformed"); + + if(components.length>3) { + this.name=components[3]; + this.enclosingScope = new ScopeBean(separator+components[1]+separator+components[2]); + this.type=Type.VRE; + } + else if (components.length>2) { + this.name=components[2]; + this.enclosingScope=new ScopeBean(separator+components[1]); + this.type=Type.VO; + } + else { + this.name=components[1]; + this.type=Type.INFRASTRUCTURE; + } + + } + + /** + * Returns the linear expression of the scope. + */ + public String toString() { + return is(Type.INFRASTRUCTURE)?separator+name(): + enclosingScope().toString()+separator+name(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((enclosingScope == null) ? 0 : enclosingScope.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.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; + ScopeBean other = (ScopeBean) obj; + if (enclosingScope == null) { + if (other.enclosingScope != null) + return false; + } else if (!enclosingScope.equals(other.enclosingScope)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (type != other.type) + return false; + return true; + } + + +} diff --git a/src/main/java/org/gcube/common/scope/impl/ScopedServiceMap.java b/src/main/java/org/gcube/common/scope/impl/ScopedServiceMap.java index 36cb39b..f25d08c 100644 --- a/src/main/java/org/gcube/common/scope/impl/ScopedServiceMap.java +++ b/src/main/java/org/gcube/common/scope/impl/ScopedServiceMap.java @@ -1,6 +1,7 @@ package org.gcube.common.scope.impl; -import java.util.List; +import static org.gcube.common.scope.impl.ScopeBean.Type.*; + import java.util.Map; import org.gcube.common.scope.api.ScopeProvider; @@ -8,10 +9,10 @@ import org.gcube.common.scope.api.ServiceMap; /** * A {@link ServiceMap} that forwards requests to {@link ServiceMap}s associated - * with the scope of callers. + * with the current scope. *

* At construction time, it configures itself with all the service maps found - * the classpath (excluding URLs available to primordial and extension + * in the classpath (excluding URLs available to primordial and extension * classloader). Recognises service maps as resources whose names match a * {@link ServiceMapScanner#mapConfigPattern}. * @@ -29,18 +30,39 @@ public class ScopedServiceMap implements ServiceMap { @Override public String scope() { - return ScopeProvider.instance.get(); + return currentMap().scope(); + } + + @Override + public String version() { + return currentMap().version(); } @Override - public List endpoint(String service) { + public String endpoint(String service) throws IllegalArgumentException,IllegalStateException { - String currentScope = scope(); + return currentMap().endpoint(service); + } + + //helper + private ServiceMap currentMap() { + + String currentScope = ScopeProvider.instance.get(); + + if (currentScope==null) + throw new IllegalStateException("current scope is undefined"); + + ScopeBean bean = new ScopeBean(currentScope); + + if(bean.is(VRE)) + currentScope = bean.enclosingScope().toString(); ServiceMap map = maps.get(currentScope); - - return map == null ? null : map.endpoint(service); + + if (map==null) + throw new IllegalStateException("a map for "+currentScope+" is undefined"); + + return map; } - } diff --git a/src/main/java/org/gcube/common/scope/impl/ScopedTasks.java b/src/main/java/org/gcube/common/scope/impl/ScopedTasks.java new file mode 100644 index 0000000..bd3246f --- /dev/null +++ b/src/main/java/org/gcube/common/scope/impl/ScopedTasks.java @@ -0,0 +1,70 @@ +package org.gcube.common.scope.impl; + +import java.util.concurrent.Callable; + +import org.gcube.common.scope.api.ScopeProvider; + +/** + * Utility to bind the execution of standard tasks to the current scope. + * + * @author Fabio Simeoni + * + */ +public class ScopedTasks { + + /** + * Binds a {@link Callable} task to the current scope. + * @param task the task + * @return an equivalent {@link Callable} task bound to the current scope + */ + static public Callable bind(final Callable task) { + + final String callScope = ScopeProvider.instance.get(); + + return new Callable() { + @Override + public V call() throws Exception { + + //bind underlying thread to callscope + ScopeProvider.instance.set(callScope); + + try { + return task.call(); + + } + finally { + ScopeProvider.instance.reset(); + } + + } + }; + } + + /** + * Binds a {@link Runnable} task to the current scope. + * @param task the task + * @return an equivalent {@link Runnable} task bound to the current scope + */ + static public Runnable bind(final Runnable task) { + + final String callScope = ScopeProvider.instance.get(); + + return new Runnable() { + @Override + public void run() { + + //bind underlying thread to callscope + ScopeProvider.instance.set(callScope); + + try { + task.run(); + } + finally { + ScopeProvider.instance.reset(); + } + + } + }; + } + +} diff --git a/src/main/java/org/gcube/common/scope/impl/ServiceMapAdapter.java b/src/main/java/org/gcube/common/scope/impl/ServiceMapAdapter.java index 3092e89..95878b1 100644 --- a/src/main/java/org/gcube/common/scope/impl/ServiceMapAdapter.java +++ b/src/main/java/org/gcube/common/scope/impl/ServiceMapAdapter.java @@ -1,7 +1,6 @@ package org.gcube.common.scope.impl; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Set; @@ -17,7 +16,7 @@ import javax.xml.bind.annotation.adapters.XmlAdapter; * @author Fabio Simeoni * */ -public class ServiceMapAdapter extends XmlAdapter>> { +public class ServiceMapAdapter extends XmlAdapter> { @XmlRootElement(name="services") static class ValueServiceMap { @@ -32,25 +31,25 @@ public class ServiceMapAdapter extends XmlAdapter endpoints; + @XmlAttribute + private String endpoint; } @Override - public Map> unmarshal(ValueServiceMap valueMap) throws Exception { - Map> map = new LinkedHashMap>(); + public Map unmarshal(ValueServiceMap valueMap) throws Exception { + Map map = new LinkedHashMap(); for (ServiceEntry service : valueMap.services) - map.put(service.name,service.endpoints); + map.put(service.name,service.endpoint); return map; } @Override - public ValueServiceMap marshal(Map> map) throws Exception { + public ValueServiceMap marshal(Map map) throws Exception { ValueServiceMap valueMap = new ValueServiceMap(); - for (Map.Entry> e : map.entrySet()) { + for (Map.Entry e : map.entrySet()) { ServiceEntry entry = new ServiceEntry(); entry.name=e.getKey(); - entry.endpoints = e.getValue(); + entry.endpoint = e.getValue(); } return valueMap; } diff --git a/src/main/java/org/gcube/common/scope/impl/ServiceMapScanner.java b/src/main/java/org/gcube/common/scope/impl/ServiceMapScanner.java index 9389e79..fbcb29b 100644 --- a/src/main/java/org/gcube/common/scope/impl/ServiceMapScanner.java +++ b/src/main/java/org/gcube/common/scope/impl/ServiceMapScanner.java @@ -55,10 +55,20 @@ class ServiceMapScanner { .compile(mapConfigPattern))) { URL url = Thread.currentThread().getContextClassLoader() .getResource(resource); - log.trace("loading {} ", url); + log.info("loading {} ", url); DefaultServiceMap map = (DefaultServiceMap) um.unmarshal(url); - maps.put(map.scope(), map); - } + + ServiceMap current = maps.get(map.scope()); + if (current!=null && current.version()!=null) + if (current.version().compareToIgnoreCase(map.version())==1) { + log.warn("discarding {} because older (v.{}) than one previously loaded (v.{}) for {} ", new Object[]{url, map.version(), current.version(), map.scope()}); + continue; + } + else + log.info("overwriting older map (v.{}) with newer map (v.{}) for {} ", new Object[]{current.version(), map.version(), map.scope()}); + + maps.put(map.scope(), map); + } } catch (Exception e) { throw new RuntimeException("could not load service maps", e); } diff --git a/src/test/java/org/gcube/common/scope/BeanTest.java b/src/test/java/org/gcube/common/scope/BeanTest.java new file mode 100644 index 0000000..6b63d9c --- /dev/null +++ b/src/test/java/org/gcube/common/scope/BeanTest.java @@ -0,0 +1,38 @@ +package org.gcube.common.scope; + +import static org.junit.Assert.*; + +import org.gcube.common.scope.impl.ScopeBean; +import org.gcube.common.scope.impl.ScopeBean.Type; +import org.junit.Test; + +public class BeanTest { + + + @Test + public void beansAreParsedCorrectly() { + + String infra ="/infra"; + ScopeBean infraBean = new ScopeBean(infra); + assertEquals("infra",infraBean.name()); + assertTrue(infraBean.is(Type.INFRASTRUCTURE)); + assertNull(infraBean.enclosingScope()); + assertEquals(infra,infraBean.toString()); + assertEquals(infraBean,new ScopeBean(infra)); + + String vo =infra+"/vo"; + ScopeBean vobean = new ScopeBean(vo); + assertEquals("vo",vobean.name()); + assertTrue(vobean.is(Type.VO)); + assertEquals(infraBean,vobean.enclosingScope()); + assertEquals(vo,vobean.toString()); + + String vre = vo+"/vre"; + ScopeBean vrebean = new ScopeBean(vre); + assertEquals("vre",vrebean.name()); + assertTrue(vrebean.is(Type.VRE)); + assertEquals(vobean,vrebean.enclosingScope()); + assertEquals(vre,vrebean.toString()); + + } +} diff --git a/src/test/java/org/gcube/common/scope/BindingTest.java b/src/test/java/org/gcube/common/scope/BindingTest.java new file mode 100644 index 0000000..a44bdb4 --- /dev/null +++ b/src/test/java/org/gcube/common/scope/BindingTest.java @@ -0,0 +1,63 @@ +package org.gcube.common.scope; + +import static org.junit.Assert.*; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.common.scope.impl.ScopedTasks; +import org.junit.Test; + +public class BindingTest { + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + + @Test + public void callablesAreBound() throws Exception { + + final String callScope = "somescope"; + + ScopeProvider.instance.set(callScope); + + Callable unbound = new Callable() { + @Override + public Void call() throws Exception { + assertEquals("task thread is not bound to call scope, but to "+ScopeProvider.instance.get(),callScope,ScopeProvider.instance.get()); + return null; + } + }; + + Callable bound = ScopedTasks.bind(unbound); + + String newScope = "newscope"; + + //scope in current thread changes + ScopeProvider.instance.set(newScope); + + //task is submittted + executor.submit(bound).get(); + + //resetting task + assertEquals("call thread does not retain its latest scope",newScope,ScopeProvider.instance.get()); + + //reset call scope + ScopeProvider.instance.reset(); + + Callable cleanupTest = new Callable() { + @Override + public Void call() throws Exception { + assertNull(ScopeProvider.instance.get()); + return null; + } + }; + + executor.submit(cleanupTest).get(); + + } + + + +} diff --git a/src/test/java/org/gcube/common/scope/ServiceMapTest.java b/src/test/java/org/gcube/common/scope/ServiceMapTest.java index 78a3a5c..71dd712 100644 --- a/src/test/java/org/gcube/common/scope/ServiceMapTest.java +++ b/src/test/java/org/gcube/common/scope/ServiceMapTest.java @@ -3,8 +3,6 @@ package org.gcube.common.scope; import static org.junit.Assert.*; import java.io.StringReader; -import java.util.Arrays; -import java.util.List; import javax.xml.bind.JAXBContext; @@ -16,36 +14,48 @@ import org.junit.Test; public class ServiceMapTest { - private static String map = "" + "" - + "" - + "http://acme.org:8000/service1" - + "http://acme2.org:8000/service1" - + "" + "" - + "http://acme3.org:8000/service2" - + "http://acme4.org:8000/service2" - + "" + "" + ""; + @Test public void serviceMapsBindCorrectly() throws Exception { + String map = + "" + + "" + + "" + + "" + + "" + + ""; + JAXBContext context = JAXBContext.newInstance(DefaultServiceMap.class); DefaultServiceMap serviceMap = (DefaultServiceMap) context .createUnmarshaller().unmarshal(new StringReader(map)); assertEquals("scope", serviceMap.scope()); - - List expected = Arrays.asList("http://acme.org:8000/service1","http://acme2.org:8000/service1"); - assertEquals(expected, serviceMap.endpoint("service1")); - expected = Arrays.asList("http://acme3.org:8000/service2","http://acme4.org:8000/service2"); - assertEquals(expected, serviceMap.endpoint("service2")); + assertEquals("1.0", serviceMap.version()); + + assertEquals("http://acme.org:8000/service1", serviceMap.endpoint("service1")); + + assertEquals("http://acme2.org:8000/service2", serviceMap.endpoint("service2")); } @Test public void serviceMapsDiscoveredCorrectly() throws Exception { - ScopeProvider.instance.set("scope"); + ScopeProvider.instance.set("/infra/vo"); + + assertNotNull(ServiceMap.instance.endpoint("service1")); + + assertEquals("2.3",ServiceMap.instance.version()); + + } + + @Test + public void serviceMapsCanBeLookedupInVREScope() throws Exception { + + ScopeProvider.instance.set("/infra/vo/vre"); assertNotNull(ServiceMap.instance.endpoint("service1")); diff --git a/src/test/resources/oldermap.servicemap b/src/test/resources/oldermap.servicemap new file mode 100644 index 0000000..61a38e4 --- /dev/null +++ b/src/test/resources/oldermap.servicemap @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/test/resources/testmap.servicemap b/src/test/resources/testmap.servicemap index 8d3b384..0fcd3f8 100644 --- a/src/test/resources/testmap.servicemap +++ b/src/test/resources/testmap.servicemap @@ -1 +1,6 @@ -http://acme.org:8000/service1http://acme2.org:8000/service1http://acme3.org:8000/service2http://acme4.org:8000/service2 \ No newline at end of file + + + + + + \ No newline at end of file