1

I am developing an architecture in Java using tomcat and I have come across a situation that I believe is very generic and yet, after reading several questions/answers in StackOverflow, I couldn't find a definitive answer. My architecture has a REST API (running on tomcat) that receives one or more files and their associated metadata and writes them to storage. The configuration of the storage layer has a 1-1 relationship with the REST API server, and for that reason the intuitive approach is to write a Singleton to hold that configuration.

Obviously I am aware that Singletons bring testability problems due to global state and the hardship of mocking Singletons. I also thought of using the Context pattern, but I am not convinced that the Context pattern applies in this case and I worry that I will end up coding using the "Context anti-pattern" instead.

Let me give you some more background on what I am writing. The architecture is comprised of the following components:

  • Clients that send requests to the REST API uploading or retrieving "preservation objects", or simply put, POs (files + metadata) in JSON or XML format.

  • The high level REST API that receives requests from clients and stores data in a storage layer.

  • A storage layer that may contain a combination of OpenStack Swift containers, tape libraries and file systems. Each of these "storage containers" (I'm calling file systems containers for simplicity) is called an endpoint in my architecture. The storage layer obviously does not reside on the same server where the REST API is.

The configuration of endpoints is done through the REST API (e.g. POST /configEndpoint), so that an administrative user can register new endpoints, edit or remove existing endpoints through HTTP calls. Whilst I have only implemented the architecture using an OpenStack Swift endpoint, I anticipate that the information for each endpoint contains at least an IP address, some form of authentication information and a driver name, e.g. "the Swift driver", "the LTFS driver", etc. (so that when new storage technologies arrive they can be easily integrated to my architecture as long as someone writes a driver for it).

My problem is: how do I store and load configuration in an testable, reusable and elegant way? I won't even consider passing a configuration object to all the various methods that implement the REST API calls.

A few examples of the REST API calls and where the configuration comes into play:

// Retrieve a preservation object metadata (PO)
@GET
@Path("container/{containername}/{po}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public PreservationObjectInformation getPOMetadata(@PathParam("containername") String containerName, @PathParam("po") String poUUID) {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - RETRIEVE THE METADATA FROM THE STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)
    // Pass poUUID as parameter

    // STEP 3 - CONVERT JSON/XML TO OBJECT
    // Unmarshall the file in JSON format
    PreservationObjectInformation poi = unmarshall(data);

    return poi;
}


// Delete a PO
@DELETE
@Path("container/{containername}/{po}")
public Response deletePO(@PathParam("containername") String containerName, @PathParam("po") String poName) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName); // Context
    // Configuration.getInstance(containerName); // Singleton
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - CONNECT TO THE STORAGE ENDPOINT
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    // STEP 3 - DELETE THE FILE

    return Response.ok().build();
}


// Submit a PO and its metadata
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("container/{containername}/{po}")
public Response submitPO(@PathParam("containername") String container, @PathParam("po") String poName, @FormDataParam("objectName") String objectName,
        @FormDataParam("inputstream") InputStream inputStream) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - WRITE THE DATA AND METADATA TO STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    return Response.created(new URI("container/" + container + "/" + poName))
            .build();
}

** UPDATE #1 - My implementation based on @mawalker's comment **

Find below my implementation using the proposed answer. A factory creates concrete strategy objects that implement lower-level storage actions. The context object (which is passed back and forth by the middleware) contains an object of the abstract type (in this case, an interface) StorageContainerStrategy (its implementation will depend on the type of storage in each particular case at runtime).

public interface StorageContainerStrategy {
    public void write();
    public void read();

    // other methods here
}

public class Context {
    public StorageContainerStrategy strategy;

    // other context information here...
}

public class StrategyFactory {
    public static StorageContainerStrategy createStorageContainerStrategy(Container c) {
        if(c.getEndpoint().isSwift())
            return new SwiftStrategy();
        else if(c.getEndpoint().isLtfs())
            return new LtfsStrategy();
        // etc.
        return null;
    }
}

public class SwiftStrategy implements StorageContainerStrategy {
    @Override
    public void write() {
        // OpenStack Swift specific code
    }

    @Override
    public void read() {
        // OpenStack Swift specific code
    }
}

public class LtfsStrategy implements StorageContainerStrategy {
    @Override
    public void write() {
        // LTFS specific code
    }

    @Override
    public void read() {
        // LTFS specific code
    }
}
MisterStrickland
  • 947
  • 1
  • 15
  • 35
  • 1
    Inject a configuration factory into your api classes. The factory will return the appropriate configuration by container name. For testing, you can inject a mocked factory that returns a known configuration. – dbugger Dec 23 '15 at 15:26
  • Thanks for the comment. I think the factory is the way to go. @mawalker's answer anticipated something, though - I expect that more and more layers are about to come. Therefore I am going to use a factory like you said, but a factory of strategies, making each strategy specific to the storage implementation. See my update above for the solution. – MisterStrickland Dec 28 '15 at 18:26

1 Answers1

1

Here is the paper Doug Schmidt (in full disclosure my current PhD Advisor) wrote on the Context Object Pattern.

https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf

As dbugger stated, building a factory into your api classes that returns the appropriate 'configuration' object is a pretty clean way of doing this. But if you know the 'context'(yes, overloaded usage) of the paper being discussed, it mainly for use in middleware. Where there are multiple layers of context changes. And note that under the 'implementation' section it recommends use of the Strategy Pattern for how to add each layer's 'context information' to the 'context object'.

I would recommend a similar approach. Each 'storage container' would have a different strategy associated with it. Each "driver" therefore has its own strategy impl. class. That strategy would be obtained from a factory, and then used as needed. (How to design your Strats... best way (I'm guessing) would be to make your 'driver strat' be generic for each driver type, and then configure it appropriately as new resources arise/the strat object is assigned)

But as far as I can tell right now(unless I'm reading your question wrong), this would only have 2 'layers' where the 'context object' would be aware of, the 'rest server(s)' and the 'storage endpoints'. If I'm mistaken then so be it... but with only 2 layers, You can just use 'strategy pattern' in the same way you were thinking 'context pattern', and avoid the issue of singletons/Context 'anti-pattern'. (You 'could' have a context object, which contains the strategy for which driver to use, and then a 'configuration' for that driver... that wouldn't be insane, and might fit well with your dynamic HTTP configuration.)

The Strategy(s) Factory Class doesn't 'have to' be singleton/have static factory methods either. I've made factories that are objects before just fine, even with D.I. for testing. There is always trade-offs to different approaches, but I've found better testing to be worth it in almost all cases I've ran into.

mawalker
  • 2,072
  • 2
  • 22
  • 34
  • Thanks for such a clarifying answer! I anticipate there will be more layers in the future so I decided to give it a try. I read the paper and wrote a solution (see update 1 above). I created a strategy factory using a static method (for simplicity - that may change in the future). The `StorageContainerStrategy` defines the contract that all drivers must be compliant with. Depending on the type of endpoint the factory will return a different implementation of `StorageContainerStrategy`. Each context object will contain a strat object plus other context information. Is that what you had in mind? – MisterStrickland Dec 28 '15 at 18:24
  • 1
    The strategy party looks fine, The 'key' would be the 'context' class, and how exactly you handle that, There are a LOT of ways to handle that. And will depend a LOT on the rest of your arch. but yes, this is generally the way to approach it. However, I would build your code from the ground up to support D.I. that way you don't have to go back and retrofit it to do so. – mawalker Dec 28 '15 at 18:33
  • 1
    I would probably add a new "configuration" interface/objects that are then D.I. inserted into the strategy impl. This allows the 'strategy impl' to remain static, and just have individual 'configs' passed into it. This will help with dynamic config,(and probably testing) – mawalker Dec 28 '15 at 18:37