Common-gcore-resources
common-gcore-resources
contains an object-based implementation of the gCube Resource Model, i.e. a set of classes that represent known resource types.
The object model improves over its counterpart in the gCube Core Framework
(gCF) in that:
- it has no dependency on the
gCore
stack, or in fact any other 3rd party library.
- it offers fluent APIs for the construction and inspection of resources.
- it offers a range of facilities to validate, display, and declaratively navigate resources.
Fluent APIs and management facilities are standard expectations for a second attempt at object-based resource modelling. The independence from gCore, however, is the key feature and motivation for common-gcore-resources
, as the library can be easily embedded in a variety of client runtimes without out-of-band installation or configuration requirements. Clients may be external to gCube, or they may be 2nd-generation gCube services developed and running on stacks other than gCore stack. Indeed, common-gcore-resources
is a key part of the Featherweight Stack for gCube clients.
common-gcore-resources
is available in our Maven repositories with the following coordinates:
<artifactId>common-gcore-resources</artifactId> <groupId>org.gcube.resources</groupId>
Model Classes
The model includes the following top-level resource classes:
-
Software
: describes services, libraries, plugins and other gCube components (corresponds toGCUBEService
in the older model) -
GCoreEndpoint
: describes endpoints of gCore services (corresponds toGCUBERunningInstance
in the older model) -
ServiceEndpoint
: describes endpoints of non-gCore services (corresponds toGCUBERuntimeResource
in the oder model) -
ServiceInstance
describes instances of (stateful) gCore services (not direct counterpart in the older model). -
HostingNode
: describes gHNs (corresponds toGCUBEHostingNode
in the older model) -
GenericResource
: describes gCube resources which are not described by any of the previous classes (corresponds toGCUBEGenericResource
in the older model).
Each class relies on static inner classes to model the complex properties of the corresponding resource type.
Collectively, resourcel classes and their inner classes are referred to as model classes.
All resources classes have the following property:
- serialisation: their instances can be serialised and deserialised to and from XML with the RI implementation of JAXB which is part of the Java platform since version 1.6.
All resource classes except ServiceInstance have also the following property:
- validation: the serialisation of their instances can be validated against the schema definition of the corresponding resource type. There is otherwise little or no validation logic within the model. Note that there is also no schema definition for ServiceInstances.
Finally, all model classes have the following property:
- equivalence: their instances can be compared for equivalence (implement
equals()
) and can serve as keys in hash-based structures (implementhashCode()
).
The API of model classes are designed to support the following instance lifecycle:
- clients create instances and publish them with distinguished services of the gCube Information System.
- clients inspect instances retrieved from distinguished services of the gCube Information System.
- clients may update retrieved instances and re-publish them with distinguished services of the gCube Informaton System.
Clients that create or update instances are referred to as publishing clients. They interact with the write API of model classes.
Clients that inspect instances are referred to as discovery clients. They interact with the read API of model classes.
It is understood that publishing and discovery clients interact with clients libraries dedicated to resource publication and discovery.
Publishing libraries will serialise instances and discovery libraries will deserialise them. Publishing libraries may also validate them prior to publication.
While these libraries will have dependencies on the model, the model is totally independent from such libraries and the services of the gCube Information System.
Serialisation, Deserialisation, and Validation
Resource class instances can be serialised and deserialised using standard JAXB idioms. To illustrate, a GenericResource
may be serialised to an in-memory character stream as follows:
GenericResource generic = … JAXBContext ctx = JAXBContext.newInstance(GenericResource.class); Marshaller marshaller = ctx.createMarshaller(); StringWriter writer = new StringWriter(); marshaller.marshal(generic,writer);
Consult the JAXBContext and Marshaller APIs for various configuration and output options.
For convenience, the Resources
class defines static methods that encapsulate this idiom under a reduced API:
-
<T extends InputStream> InputStream marshal(Object,T)
-
<T extends Writer> marshal(Object,T)
-
<T extends Result> Result marshal(Object,T)
Refactoring the previous example:
Resources.marshal(generic,writer);
During testing, a common destination for instance serialisations is the standard output stream. The Resources
class includes a print() method for the purpose:
-
void print(Object)
Note: marshal()
methods return the input destination, for cases in which doing so is convenient (e.g. round-tripping tests).
Similarly, the GenericResource
may be deserialised from the previous character stream as follows:
Marshaller unmarshaller = ctx.createUnmarshaller(); StringReader reader = new StringReader(writer.toString()); GenericResource deserialised = (GenericResource) unmarhsaller.unmarshal(reader);
Again, consult the Unmarshaller APIs for different input options. For convenience, the Resources class defines static methods that encapsulate this idiom under a reduced API:
-
<T> T unmarshal(Class<T>,InputStream)
-
<T> T unmarshal(Class<T>,Reader)
-
<T> T unmarshal(Class<T>,Source)
Refactoring the previous example:
GenericResource deserialised = Resources.unmarshal(generic,reader);
'Note: since serialisation and deserialisation failures are on average unlikely and unrecoverable errors, marshal()
and unmarshal()
methods throws them as unchecked exceptions (RuntimeException
s).
Note also that the following holds true in all cases above:
generic.equals(unmarshalled);
Resource instance serialisations can then be validated using the following static method of the Resources
class:
-
validate(Object resource) throws IllegalArgumentException, Exception
The method throws an IllegalArgumentException
if it receives a resource without a known schema. It throws a generic Exception
if the resource has a known schema but it is not valid with respect to that schema.
Validation may be performed during testing, or before the instance serialisation is stored or transmitted over the network. Dedicates libraries for resource publishing will normally performed it a pre-condition to publication.
Read API
The read API of model classes is designed to support the inspection of resource class instances which have been deserialised from XML representations, where the representations are typically obtained from querying distinguished services of the gCube Information System.
Note: the deserialisation may be performed by the discovery APIs on behalf of their clients, or it may be directly performed by the clients with the Resource#unmarshal()
methods discussed above. In all cases, the read API assumes that the state of the instances is valid with respect to the corresponding schema definitions. In what follows, we refer to this assumption as to the validity assumption.
The inspection of model class instances relies on classic accessor methods, even though accessor names diverge from standard conventions for increased legibility of deep traversals.
For example, a deeply nested property of ServiceEndpoint
is accessed as follows:
ServiceEndpoint endpoint = .... short version = endpoint.profile().platform().version();
Based on the validity assumption, the design of accessors changes depending on whether properties are or optionals or mandatory in the corresponding schema definition. For optional properties in particular:
- if the property is atomic-valued, accessors return always a
null
-able object (e.g. a primitive wrapper). Clients are expected to performnull
checks on the returned value. - if the property is object-valued, accessors are always paired with a method that checks for the existence of a property value (e.g.
hasPlatform
). Clients may use this method to improve over the legibility of explicitnull
checks. - if the property is collection-valued, accessors return empty collections rather than
null
. Clients may thus avoid existence checks altogether.
In the example above, clients may dispense from existence checks, as profile, platform, and platform version are mandatory properties of ServiceEndpoint
s. On the other hand, platforms are optional properties for GCoreEndpoint
s and build versions are in turn optional properties of platforms. Accordingly, a client would navigate a GCoreEndpoint
as follows:
GCoreEndpoint endpoint = .... if (endpoint.profile().hasPlatform()) { Short version = endpoint.profile().platform().buildVersion(); if (version==null { ... } }
For collection-valued properties of type T
, accessors returns Collection<T>:
ServiceEndpoint endpoint = .... for (Function f: endpoint.profile().functions())) { ... }
Finally, some object-valued properties are unconstrained in the corresponding schema definitions (i.e. can be represented as free-form well-formed XML elements). In these cases, the accessor return always the non-<code>null Element
root of DOM document which has the contents of the properties as its children. As an example, if the body of a GenericResource
contains the following XML fragment:
<ns:a xmlns:ns="http://acme.org"> <ns:b>...</ns:b> <ns:c>...</ns:c> <ns:c>...</ns:c> </ns:a>
the following code:
GenericResource generic = .... Element body = generic.endpoint().body();
return the synthetic root of a DOM document which has the a
element as its only child.
The resulting document may be then be traversed from its root using the DOM API, or with any other XML API available to the client, including JAXB itself, if the document can be bound to objects known to the client. In many cases, inspection is most conveniently handled with XPath APIs, which are also bundled in the standard Java platform. In these cases, common-gcore-resources
includes XPathHelper
, a simple utility that dispenses clients from configuring the standard Java API for Xpath inspection. To illustrate, the body of the GenericResource
above, may be inspected as follows:
the following code:
GenericResource generic = .... Element body = generic.endpoint().body(); XPathHelper helper = new XPathHelper(body); helper.setNamespace("ns","http://acme.org"); List<String> cs = helper.evaluate("a/c")); for (String c : cs) { ..... }
Note that clients may also use XPathHelper#evalutateForNodes()
if they prefer results as NodeList
.
Write API
The write API of model classes simplifies the construction and update of resource instances by reducing to number of class instantiations and variable assignments which are required to build complex object graphs.
Instance construction and update rely on mutators, though these differ from conventional setters and adopt a builder-style that allows chaining of invocations. Like accessors, they also break with standard naming conventions to improve the legibility of chained statements. Finally, mutators subsume the instantiation of object-valued properties of instances, so as to avoid interruption of the construction flow.
The following example illustrates the approach for a ServiceEndpoint
:
ServiceEndpoint endpoint = new ServiceEndpoint(); endpoint.newProfile().category("...").description("...").name("...").version("...");
Here, newProfile()
creates the profile of the instance, serving at once as a setter and a factory method. description()
, name()
, and version()
set some of the atomic-valued properties of the profile returning the profile so that other mutators can be chained to the expression.
The pattern above is consistently replicated across all model classes, with the result that only the resource class needs explicit instantiation. Variable assignments can be also reduced, as the following example illustrates:
ServiceEndpoint endpoint = new ServiceEndpoint(); endpoint.newProfile().category("...").description("...").name("...").version("..."); endpoint.profile().newPlatform().name("...").version(...); endpoint.profile().newRuntime().hostedOn("...").ghnId("...").status("...");
After it has been created with newProfile()
, the resource profile can be retrieved with its accessor before its construction resumes. Of course, more conventional idioms are equally possible if preferred, e.g.:
ServiceEndpoint endpoint = new ServiceEndpoint(); endpoint.newProfile().category("...").description("...").name("...").version("..."); Platform platform = endpoint.profile().newPlatform(); platform.name("...").version(...); Runtime runtime = endpoint.profile().newRuntime(); runtime.hostedOn("...").ghnId("...").status("...");
Collection-valued properties do not need to be instantiated, rather they can be constructed through the corresponding accessors. We have discussed above that accessors return Collection<T>
and the Collection
API can be used to populate the collections. In fact, accessors return a specialised implementation of Collection<T>
, Group<T>
, and clients that build instances can take advantage of the extra factory method Group#add()
to avoid new
-based instantiation of the elements of the collection. The following example continues the previous example to illustrates the approach:
endpoint.profile().accessPoints().add().address("...");
Here we access the access points of the service endpoint through the accessPoints()
accessor. We then invoke the Group#add()
method to obtain an instance of the element type (AccessPoint
), which we then construct through its own mutators. Not only is the syntax streamlined, but the client does not need to know and import the model class that implements the element type. Again, we can avoid assignments as follows:
endpoint.profile().accessPoints().add().address("..."); endpoint.profile().accessPoints().add().address("...").description("...").credentials("...", "...");
Of course, more conventional idioms are possible:
Group<AccessPoint> aps = endpoint.profile().accessPoints(); aps.add().address("..."); aps.add().address("...").description("...").credentials("...", "...");
The only case in which assignments are necessary is with nested collections, e.g.:
Group<Property> props = endpoint.profile().accessPoints().add().address("...").properties(); props.add().nameAndValue("name","value"); props.add().nameAndValue("name2","value2").encrypted(true);