Contribute to the DSpace Development Fund

The newly established DSpace Development Fund supports the development of new features prioritized by DSpace Governance. For a list of planned features see the fund wiki page.

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 22 Next »

DSpace Service Manager Tutorial

Introduction

The objectives of this tutorial are to provide the general DSpace Developer with an understanding of what the DSpace Service Manager is and what we are attempting to attain through its usage and some basic software development practices and design principles.  The tutorial will break these practices and the overall development platform down into developer centric terms that should, if successful, give the developer a reference for how to best design their code.  Goals of the Service Manager are to assist in separating the dependencies of individual functional areas of DSpace on one another, by eliminating significant dependencies on other parts of the codebase and the prevalence of "StaticManager" Classes.

The DSpace Services Framework is a back-porting of the DSpace 2.0 Development Group's work in creating a reasonable and simple "Core Services" layer for DSpace. The Services Framework provides a means for application developers to both lookup and register their own "services" or JAVA objects that can be referred to by the application.

What are services?

Answer: services are a generic term for the business actions that provide functionality that will complete a specific task in the application.

In DSpace Services are conceptually similar to OSGi Services , where an addon library (a OSGi Bundle) delivers a singleton instance of a class as a service for other application code to utilize.  In OSGi the Service often has a Java Interface class and constitutes a "Contract" for the application.  

From a Design Standpoint, The Service Manager is a Service Locator Pattern. This shift represents a "best practice" for new DSpace architecture and the implementation of extensions to the DSpace application. DSpace Services are best described as a "Registry" of Services that are delivered to the application for use by the use of a Spring Application Context. The original (DSpace 2.0 ) core services are the main services that make up a DSpace Service Manager system. These include services for the application "Configuration", "Transactional Context", "Requests" and user "Session",  "Persistence" things like user and permissions management and storage and caching. These services can be used by any developer writing DS2 plugins (e.g. statistics), providers (e.g. authentication), or user interfaces (e.g. JSPUI).

What is OSGi?

An OSGi service is a java object instance, registered into an OSGi framework with a set of properties. Any java object can be registered as a service, but typically it implements a well-known interface. Considerable has evolved in both the Spring and OSGi Communities.

What is Spring?

Spring is an Inversion of Control (IoC) container that utilizes Dependency Injection (a fancy way of just saying that Spring creates and hands the JAVA objects you would normally have had to create in your code).

Example of Not Using Spring

As a brief example Here is a class that does not use Dependency Injection, it calls "new SomeOtherClass()" directly in its constructor.

public class Example {

    private SomeOtherClass myObject = null;

    public Example(){
        myObject = new SomeOtherClass();
    }
}

Example Using Spring

Here is an example of a better coding practice, where we give up the responsibility for the creation of the class and allow the IoC/DI container to be responsible for its creation.

public class Example {

    private SomeOtherClass myObject = null;

    public Example(SomeOtherClass object){
        this.myObject = object;
    }
}

This immediately opens the door for "SomeOtherClass" to be changed out with new/other subclasses of "SomeOtherClass".  In Spring, the definition "example.xml" file might look like this:

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="someObject" class="com.example.SomeOtherClass"/>

    <bean id="ny-example" class="com.example.Example">
        <constructor-arg ref="someObject"/>
    </bean>

</beans>

Where, when the ServiceManager is started, two beans are instantiated by Spring, and one is used in the constructor argument of the other. The simplest instantiation of a Spring Container might look like the following.

ApplicationContext context = new ClassPathXmlApplicationContext(
        new String[] {"example.xml"});
Example myExample = applicationContext.getBean("my-example");

/* Go on to do something interesting with the service */

The Case For Spring

This tutorial focuses on adoption of Spring as a best practice for many aspects of DSpace development, from Core Library definition and instantiation to Application Developer implementation of customizations and addons.

  • Spring focuses around providing a way to manage your business objects. (DSpace currently lacks this capability).
  • Spring is both comprehensive and modular. Spring has a layered architecture, you can choose to use just about any part of it in isolation.
  • It is easy to introduce Spring incrementally into existing projects. (The Latest DSpace WebMVC, REST and XMLUI development efforts already leverage Spring WebMVC in the application tier).
  • Spring is designed from the ground up to help you write code that's easy to test. Spring is an ideal framework for test driven projects. (DSpace has only just introduced a JUnit Test Suite, which does not leverage Spring in its solution. However, the DSpace Service Manager already delivers a testing suite leverages Spring to support testing configuration).
  • Spring is an increasingly important integration technology, its role recognized by several large vendors. By utilizing Spring, DSpace will be able to incrementally improve its architecture to be more robust, and more "enterprise grade".

The Service Manager

The ServiceManager provides the DSpace Application with the above Spring ApplicationContext so that the developer  does not need to be responsible for its creation when developing against DSpace. Thus, to extend the pervious example, the DSpace class and its underlying Service Manager can be utilized to get at any object that has been instantiated as a service bean by core or addon application code.Example example = new DSpace().getSingletonService("my-example", Example.class);
/* Go on to do something interesting with the service */

The DSpace Service Manager implementation manages the entire lifecycle of a running DSpace application and provides access to services by Applications that may be executing external to this "kernel" of DSpace Services. Via Spring and loading of individual dspace.cfg properties the ServiceManager manages the configuration of those services, (either through providing those properties to the Spring Application Context where they can be injected in Spring definition xml files and/or "annotations" or by exposing those properties via the injection of the DSpace ConfigurationService.

DSpace ConfigurationService

The ServiceManagerSystem abstraction allows the DSpace ServiceManager to use different systems to manage its services. The current implementation is Spring Framework based. The original design intent of the Service Manager was to support more than one IoC/DI Solution, however, as work has progressed with DSpace, it has become clear that there are trade offs to consider in its usage.

  1. Spring Injection, when used properly, means we do not reallyneed to make "lookup calls" to a central "ServiceManager" or "DSpace" object to acquire the service beans we our own code to work with.
  2. Fewer lookups mean less centralized dependencies
  3. Fewer Centralized Dependencies means fewer bottlenecks in source Code Dependency Management.
  4. Fewer Bottlenecks means greater modularity and encapsulation, less need to carry around all the source code when overriding and customizing "dspace".
  5. More use of binary distributions means greater ease in upgrading DSpace.

It is clear that Spring has become quite dominant a solution in DSpace with the adoption of Apache Cocoon 2.2 for the Manakin XMLUI and Spring MVC for the Freemarker Prototype Webapplication currently under Development in the community.

This leads us to our first best practice:

Best Practice 1: If possible, do use Dependency Injection to acquire your services. This will be of benefit down the road if we make changes to the service manager architecture.

In creating classes that work within the system, the ServiceManager will inject those services you need for you. 

public class Example {

    ConfigurationService configurationService;

    RequestService requestService;

    public Example(ConfigurationService cs, RequestService rs)
    {
        this.configurationService = cs;
        this.requestService = rs;
    }

}

Where in our spring configuration we would register this Service via the following configuration:

    <bean class="org.dspace.MyService" autowire="byType"/>

Spring AutoWiring looks for other bean of our specific type elsewhere in our configuration and injects them into our service. This is the basic mechanism whereby Addon Modules can reuse existing services or even services provided by other third party modules without having to explicitly depend on any specific implementation of those services.

The DSpace Application Lifecycle

The life cycle of the container and the services therein is controlled by the Web-application context or the DSpace CLI ScriptLauncher main executable in which the servlet has been deployed. The Lifecycle assures that when the Service managers (and specifically Spring in this case) are initialized, the required core and addon services are wired and made available to your application.

The ServiceManager Request Cycle

The ServiceManager Request Cycle is similar to the Webapplication Request Cycle.  In the case of the Service Request, There is an incoming "Request" object with the state of the a call from the client, this Request is then enterpreted by the Container and mapped to a specific Servlet, which executes to completion, On Completion, the Serlvet Generates a

When a request is made by either the Webapplication or the CLI initialization, then the Request Lifecycle is engaged:

Basic Usage

To use the Framework you must begin by instantiating and starting a DSpaceKernel. The kernel will give you references to the ServiceManager and the ConfigurationService. The ServiceManager can be used to get references to other services and to register services which are not part of the core set. For standalone applications, access to the kernel is provided via the Kernel Manager and the DSpace object which will locate the kernel object and allow it to be used.

/* Instantiate the Utility Class */
DSpace dspace = new DSpace();


/* Access get the Service Manager by convenience method */
ServiceManager manager = dspace.getServiceManager();


/* Or access by convenience method for core services */
EventService service = manager.getServiceBydspace.getEventService();

The DSpace launcher ([~mdiggory:dspace]/bin/dspace) initializes a kernel before dispatching to the selected command.

The Service Manager Interface

public interface ServiceManager {

    public <T> List<T> getServicesByType(Class<T> type);

    public <T> T getServiceByName(String name, Class<T> type);

    public boolean isServiceExists(String name);

    public List<String> getServicesNames();

    public void registerService(String name, Object service);

    public <T> T registerServiceClass(String name, Class<T> type);

    public void unregisterService(String name);


    public void pushConfig(Map<String, String> settings);

}

Core Services

Configuration Service

ConfigurationService contributed to DSpace 1.7.1 (Service Manager Version 2.0.3) And maintains Parity with the existing DSpace ConfigurationManager in supporting "dspace.cfg" and modular "config/modules/module.cfg" configuration.

The ConfigurationService controls the external and internal configuration of DSpace 2. It reads Properties files when the kernel starts up and merges them with any dynamic configuration data which is available from the services. This service allows settings to be updated as the system is running, and also defines listeners which allow services to know when their configuration settings have changed and take action if desired. It is the central point to access and manage all the configuration settings in DSpace.

Manages the configuration of the DSpace ServiceManager system. Can be used to manage configuration for any Service Bean within the ServiceManager

Acquiring the Configuration Service

/* Instantiate the Utility Class */
DSpace dspace = new DSpace();

/* Access get the Service Manager by convenience method */
ConfigurationService service = dspace.getSingletonService(ConfigurationService.class);

The ConfigurationService API

public interface ConfigurationService {

    /**
     * Get a configuration property (setting) from the system as a
     * specified type.
     *
     * @param <T>
     * @param name the property name
     * @param type the type to return the property as
     * @return the property value OR null if none is found
     * @throws UnsupportedOperationException if the type cannot be converted to the requested type
     */
    public <T> T getPropertyAsType(String name, Class<T> type);

    /**
     * Get a configuration property (setting) from the system, or return
     * a default value if none is found.
     *
     * @param <T>
     * @param name the property name
     * @param defaultValue the value to return if this name is not found
     * @return the property value OR null if none is found
     * @throws IllegalArgumentException if the defaultValue type does not match the type of the property by name
     */
    public <T> T getPropertyAsType(String name, T defaultValue);

    /**
     * Get a configuration property (setting) from the system, or return
     * (and possibly store) a default value if none is found.
     *
     * @param <T>
     * @param name the property name
     * @param defaultValue the value to return if this name is not found
     * @param setDefaultIfNotFound if this is true and the config value
     * is not found then the default value will be set in the
     * configuration store assuming it is not null.  Otherwise the
     * default value is just returned but not set.
     * @return the property value OR null if none is found
     * @throws IllegalArgumentException if the defaultValue type does not match the type of the property by name
     */
    public <T> T getPropertyAsType(String name, T defaultValue, boolean setDefaultIfNotFound);

    /**
     * Get all currently known configuration settings
     *
     * @return all the configuration properties as a map of name -> value
     */
    public Map<String, String> getAllProperties();

    /**
     * Convenience method - get a configuration property (setting) from
     * the system.
     *
     * @param name the property name
     * @return the property value OR null if none is found
     */
    public String getProperty(String name);

    /**
     * Convenience method - get all configuration properties (settings)
     * from the system.
     *
     * @return all the configuration properties in a properties object (name -> value)
     */
    public Properties getProperties();

    /**
     * Set a configuration property (setting) in the system.
     * Type is not important here since conversion happens automatically
     * when properties are requested.
     *
     * @param name the property name
     * @param value the property value (set this to null to clear out the property)
     * @return true if the property is new or changed from the existing value, false if it is the same
     * @throws IllegalArgumentException if the name is null
     * @throws UnsupportedOperationException if the type cannot be converted to something that is understandable by the system as a configuration property value
     */
    public boolean setProperty(String name, Object value);

}

Benefits over the Legacy DSpace ConfigurationManager

Type Casting and Array Parsing
  • Type Casting: Common Configuration Interface supports type casting of configuration values of the type required by the caller.
  • Array Parsing: As part of this type casting, the Configuration Service will split comma separated values for you when you request the property as type "Array"

  

/* type casting */
int value = configurationService.getPropertyAsType("some-integer",int.class);

/* Array Parsing */
String[] values = configurationService.getPropertyAsType("some-array", String[].class);

/* Default Values */
int value = configurationService.getPropertyAsType("some-integer",1);

/* Default Array Values */
String[] values = configurationService.getPropertyAsType("some-array",new String[]{"my", "own", "array"});
        
Modular Default Configuration

[~mdiggory:addon.jar]/config/[~mdiggory:service].cfg 

Any service can provide sane defaults in a java properties configuration file. These properties will be able to be looked up directly using a prefix as syntax.

Example of Usage:

ConfigurationService cs = new DSpace().getConfigurationService();
String prop = cs.getProperty("prefix.property");
Modularization of Configuration Not Bound to API signature.

[~mdiggory:dspace]/config/module/[~mdiggory:prefix].cfg

Any service can provide overrides in the DSpace home configuration directory sane defaults in a java properties configuration file. These properties will be able to be looked up directly using a prefix as syntax.

Example of Usage:

ConfigurationService cs = new DSpace().getConfigurationService();
String prop = cs.getProperty("prefix.property");

In DSpace 1.7.0 enhanced capabilities were added to the ConfigurationManager to support the separation of of properties into individual files. The name of these files is utilized as a "prefix" to isolate properties that are defined across separate files from colliding.

Example of Usage:

String prop = ConfigurationManager.getProperty("prefix", "property");

Use commas for lists of values, use lookups (If you end up thinking you want to create maps in your properties, your doing it in the wrong place look instead at Spring Configuration and objectifying your configuration)

Objectifying Configuration... If you Configuration is too complex, then it probably should be an Object Model

Request Service

A request is an atomic transaction in the system. It is likely to be an HTTP request in many cases but it does not have to be. This service provides DSpace with a way to manage atomic transactions so that when a request comes in which requires multiple things to happen they can either all succeed or all fail without each service attempting to manage this independently.

In a nutshell this simply allows identification of the current request and the ability to discover if it succeeded or failed when it ends. Nothing in the system will enforce usage of the service, but we encourage developers who are interacting with the system to make use of this service so they know if the request they are participating in with has succeeded or failed and can take appropriate actions.

public interface Request {

    public String getRequestId();

    public Session getSession();

    public Object getAttribute(String name);

    public void setAttribute(String name, Object o);

    public ServletRequest getServletRequest();

    public HttpServletRequest getHttpServletRequest();

    public ServletResponse getServletResponse();

    public HttpServletResponse getHttpServletResponse();

}

The DSpace Session Service

The Session represents a user's session (login session) in the system. Can hold some additional attributes as needed, but the underlying implementation may limit the number and size of attributes to ensure session replication is not impacted negatively. A DSpace session is like an HttpSession (and generally is actually one) so this service is here to allow developers to find information about the current session and to access information in it. The session identifies the current user (if authenticated) so it also serves as a way to track user sessions. Since we use HttpSession directly it is easy to mirror sessions across multiple servers in order to allow for no-interruption failover for users when servers go offline.

public interface Session extends HttpSession {

    public String getSessionId();

    public String getUserId();

    public String getUserEID();

    public boolean isActive();

    public String getServerId();

    public String getOriginatingHostIP();

    public String getOriginatingHostName();

    public String getAttribute(String key);

    public void setAttribute(String key, String value);

    public Map<String, String> getAttributes();

    public void clear();

Do not pass Http Request or Session Objects in your code. Use Dependency Injection to make the RequestService, SessionService and Configuration Service Available in your Service Classes. Or use ServiceManager lookups if your work is out of scope of the ServiceManager.

DSpace Context Service COMING SOON

The DSpace Context Service is part of the DSpace Domain Model refactoring work and provides an easy means for any Service Bean to gain access to a DSpace Context object that is in scope for the current user request cycle. This Context will be managed by the ServiceManager RequestService and represents a means to maintain a "Transactional Envelope" for attaining "Atomic" changes to DSpace (Add Item, Update Item, Edit Metadata, etc).

DSpace Legacy DataSource Service COMING SOON

Similar to the Context Service, The DSpace Legacy DataSource Service is part of the Domain Model refactoring work and bring the preexisting DSpace DataSource instantiated within the the DSpace DatabaseManager into the Spring Application Context. The exposure of the DataSource will enable Service Beans in the DSpace ServiceManager to utilize popular tools for ORM such as Hibernate, JPA2, ibatis, Spring Templates, or your own custom persistence support to be used when developing your Services for DSpace.

Test Driven Development

Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: first the developer writes a failing automated test case that defines a desired improvement or new function, then produces code to pass that test and finally refactors the new code to acceptable standards. Kent Beck, who is credited with having developed or 'rediscovered' the technique, stated in 2003 that TDD encourages simple designs and inspires confidence.[http://en.wikipedia.org/wiki/Test-driven_development]

http://en.wikipedia.org/wiki/Test-driven_development#cite_note-Beck-0

We want to clarify that that the Testing Framework in the DSpace Services Module Predated the actual JUnit testing support that was added to dspace-api.  Testing is a very beneficial practice where the developer writes small java based test of the code they are going to produce.  The 

Test-driven development is related to the test-first programming concepts of extreme programming, begun in 1999,[~mdiggory:[2]|http://en.wikipedia.org/wiki/Test-driven_development#cite_note-Cworld92-1] but more recently has created more general interest in its own right.[~mdiggory:[3]|http://en.wikipedia.org/wiki/Test-driven_development#cite_note-Newkirk-2]

Using the Service Manager Testing Framework

DSpaceAbstractRequestTest

This is an abstract class which makes it easier to test execution of your service within a DSpace "Request Cycle" and includes an automatic request wrapper around every test method which will start and end a request, the default behavior is to end the request with a failure which causes a rollback and reverts the storage to the previous values

public abstract class DSpaceAbstractRequestTest extends DSpaceAbstractKernelTest {

    /**
     * @return the current request ID for the current running request
     */
    public String getRequestId() {
        return requestId;
    }

    @BeforeClass
    public static void initRequestService() {
        _initializeRequestService();
    }

    @Before
    public void startRequest() {
        _startRequest();
    }

    @After
    public void endRequest() {
        _endRequest();
    }

    @AfterClass
    public static void cleanupRequestService() {
        _destroyRequestService();
    }

}

Refactoring Legacy DSpace Code, An Example:

DSpace Services are about using Spring to Simplify your development life. And for our first example showing how to create services we will refactor the DSpace Launcher to no longer utilize the launcher.xml and instead load configured Commands from a spring configuration.  The result of this approach will allow for Addons to easily provide additional commands into the launcher without having to physically alter a configuration.xml file in a config directory.  To start with we will review the ScriptLauncher and discuss the problems that exist with it. For a full view of the source please see: ScriptLauncher.java

DSpace Legacy ScriptLauncher

First and formost, we see that ScriptLauncher begins the process of execution by bringing the ServiceManager into existence.  This is important so that services are available to the executed commandline tools, these services currently wire discovery and statistics into dspace.  

 */
public class ScriptLauncher
{
    /** The service manager kernel */
    private static transient DSpaceKernelImpl kernelImpl;

    /**
     * Execute the DSpace script launcher
     *
     * @param args Any parameters required to be passed to the scripts it executes
     */
    public static void main(String[] args)
    {
        // Check that there is at least one argument
        if (args.length < 1)
        {
            System.err.println("You must provide at least one command argument");
            display();
            System.exit(1);
        }

        // Initialise the service manager kernel
        try {
            kernelImpl = DSpaceKernelInit.getKernel(null);
            if (!kernelImpl.isRunning())
            {
                kernelImpl.start(ConfigurationManager.getProperty("dspace.dir"));
            }
        } catch (Exception e)
        {
            // Failed to start so destroy it and log and throw an exception
            try
            {
                kernelImpl.destroy();
            }
            catch (Exception e1)
            {
                // Nothing to do
            }
            String message = "Failure during filter init: " + e.getMessage();
            System.err.println(message + ":" + e);
            throw new IllegalStateException(message, e);
        }

But, here, we just focus on what ScriptLauncher itself is doing and how we can improve it with services. So looking further on we see that we parse the xml file with JDOM and use xpaths navigate the configuration, which means if we want to do anything new in the Launcher in the future, we may need to extend the XML file and alter the parsing. So, what we do see is that execution of the Commmand is very tightly bound to the parsing and iteration over the xml file, in fact, they are so tightly bound together that new code would need to be written if you wanted to get a commands available outside the launcher.

Firstly we need to get commands:

        // Parse the configuration file looking for the command entered
        Document doc = getConfig();
        String request = args[0];
        Element root = doc.getRootElement();
        List<Element> commands = root.getChildren("command");
        for (Element command : commands)
        {
            if (request.equalsIgnoreCase(command.getChild("name").getValue()))
            {

Then we need to get the steps and process them

                // Run each step
                List<Element> steps = command.getChildren("step");
                for (Element step : steps)
                {
                    ...
                    className = step.getChild("class").getValue();

decide about passing them on to the steps

                // Run each step
                List<Element> steps = command.getChildren("step");
                for (Element step : steps)
                {
                    // Instantiate the class
                    Class target = null;

                    // Is it the special case 'dsrun' where the user provides the class name?
                    String className;
                    if ("dsrun".equals(request))
                    {
                        if (args.length < 2)
                        {
                            System.err.println("Error in launcher.xml: Missing class name");
                            System.exit(1);
                        }
                        className = args[1];
                    }
                    else {
                        className = step.getChild("class").getValue();
                    }
                    try
                    {
                        target = Class.forName(className,
                                               true,
                                               Thread.currentThread().getContextClassLoader());
                    }
                    catch (ClassNotFoundException e)
                    {
                        System.err.println("Error in launcher.xml: Invalid class name: " + className);
                        System.exit(1);
                    }

decide to pass arguments to the step

                    // Strip the leading argument from the args, and add the arguments
                    // Set <passargs>false</passargs> if the arguments should not be passed on
                    String[] useargs = args.clone();
                    Class[] argTypes = {useargs.getClass()};
                    boolean passargs = true;
                    if ((step.getAttribute("passuserargs") != null) &&
                        ("false".equalsIgnoreCase(step.getAttribute("passuserargs").getValue())))
                    {
                        passargs = false;
                    }


assign args in the child elements of the steps,

                    if ((args.length == 1) || (("dsrun".equals(request)) && (args.length == 2)) || (!passargs))
                    {
                        useargs = new String[0];
                    }
                    else
                    {
                        // The number of arguments to ignore
                        // If dsrun is the command, ignore the next, as it is the class name not an arg
                        int x = 1;
                        if ("dsrun".equals(request))
                        {
                            x = 2;
                        }
                        String[] argsnew = new String[useargs.length - x];
                        for (int i = x; i < useargs.length; i++)
                        {
                            argsnew[i - x] = useargs[i];
                        }
                        useargs = argsnew;
                    }


The Big Problem : The ScriptLauncher hardwires XML Configuration to Business Logic

What we see is that the Script Launcher hardwires all Business Logic to XML configuration, and while this is a great prototype, certainly not very extensible.  For instance, what if we may want to initialize other details into the command or step?  What if we are calling something that uses a different method than a static main to execute?  and what if we want to set certain properties on its state beforehand. To do these tasks we would either need to rewrite the class with a main method, or we would either need to extend the launcher to do this, or add those details from the dspace.cfg. So what we will show you in refactoring this code is that you can get a great deal more flexibility out of Spring with a great deal less work on your part. Spring applies the concept of "inversion of control". The Inversion of Control (IoC) and Dependency Injection (DI) patterns are all about removing dependencies from your code.  I our case, we will be removing a dependency on the  hardcoded xml file for configuration and liberating the Command/Step domain model from the ScriptLauncher, allowing for possible reuse in other areas of the application.

Spring Based DSpace Script Launcher

Firstly we recognize that we have few domain objects here that we can work with, Command, Step and Arguments are all ripe to be defined as interfaces or implementation classes that capture the domain of the Launcher that we will execute.  Perhaps by creating these we can disentangle the Business logic of the Launcher from its configuration in xml.  Doing so without Spring, we might still ahve to bother with parsing the xml file.  So with Spring we get a luxury that we no longer need to think about that.

We will do this initial example directly in the dspace-api project so you do not get sidetracked in terms of dealing with Maven poms....

Step 1: Domain model

Command:

Command holds the details about the command we will call. Notice it doesn't parse any xml and most importantly it does not instantiate any Step directly, that is decoupled from the Command and we will show you later how it is assembled. All Commands do when getting created is sit there like a good old JAVA Bean should. It just "IS"

package org.dspace.app.launcher;

import java.util.List;

public class Command implements Comparable<Command> {

    public String name;

    public String description;

    public List<Step> steps;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<Step> getSteps() {
        return steps;
    }

    public void setSteps(List<Step> steps) {
        this.steps = steps;
    }

    @Override
    public int compareTo(Command c){

        return name.compareTo(c.name);
    }
}

Step

Step will be responsible for retaining the details about the step being executed, you will notice it knows nothing about a Command and in our default case, it just know a little bit about how to reflectively execute a Main Class.  Again, it knows little about its instantiation nor anything specifically on how it is configured. It just "IS".

package org.dspace.app.launcher;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Step {


    public List<String> arguments;

    public String className;

    /**
     * User Arguments Defaults to True.
     */
    public boolean passUserArgs = true;


    public boolean isPassUserArgs() {
        return passUserArgs;
    }

    public void setPassUserArgs(boolean passUserArgs) {
        this.passUserArgs = passUserArgs;
    }

    public List<String> getArguments() {
        return arguments;
    }

    public void setArguments(List<String> arguments) {
        this.arguments = arguments;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public int exec(String[] args) {

        //Merge the arguments
        List<String> argsToPass = Arrays.asList(args);

        if(this.getArguments() != null)
            argsToPass.addAll(this.getArguments());

        String[] useargs = argsToPass.toArray(new String[0]);


        // Instantiate and execute the class
        try {

            Class target = Class.forName(getClassName(),
                    true, Thread.currentThread().getContextClassLoader());

            Class[] argTypes = {useargs.getClass()};
            Object[] finalargs = {useargs};

            Method main = target.getMethod("main", argTypes);
            main.invoke(null, finalargs);
        }
        catch (ClassNotFoundException e) {
            System.err.println("Error in command: Invalid class name: " + getClassName());
            return 1;
        }
        catch (Exception e) {

            // Exceptions from the script are reported as a 'cause'
            System.err.println("Exception: " + e.getMessage());
            e.printStackTrace();
            return 1;
        }

        return 0;
    }
}

Step 2: The Command Service

Ok at this point your probably itching to combine all these together and get your Launcher to execute.  But at this point, well, we still have some more work to do to get ready.  And likewise, we are still keeping completely hands off of any configuration detail at this point. Do not worry, we will get there soon enough.  

Next we have the CommandService, this is a "Controller" which you will use to execute a Commnad, it doesn't have a main method, because, well, you can execute your commands entirely independent of a CLI, wouldn't that be nice in Curation Tasks and Filter Media? Yes, sure it would, but I digress, lets get back to the topic at hand. The Command Service...

All it will do is contain the buisness logic to call a command.  In this case, you will see, it just has no clue at all how it gets those commands, but it surely just understands how to execute them, pretty smart little service, it just does what its told. Doesn't think about much else.

package org.dspace.app.launcher;

import org.dspace.services.RequestService;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class CommandService {

    public List<Command> getCommands() {
        return commands;
    }

    public void setCommands(List<Command> commands) {
        this.commands = commands;
    }

    public List<Command> commands;

    public void exec(String[] args){


        // Check that there is at least one argument
        if (args.length < 1)
        {
            System.err.println("You must provide at least one command argument");
            return;
        }

        for (Command command : commands)
        {
            // Parse the configuration file looking for the command entered
            if (args[0].equalsIgnoreCase(command.getName()))
            {
                // Run each step
                for (Step step : command.getSteps())
                {
                    int result = 0;

                        if(step.isPassUserArgs())
                            /* remove command from Args */
                            result = step.exec(Arrays.copyOfRange(args,1,args.length));
                        else
                            /* send empty args */
                            result = step.exec(new String[0]);


                    return result;
                }
            }
        }
    }
}

Step 3: The Main Function

Finally, we want a way to run this from the commandline. Note I've simplified it a little to get the DSpace Services Kernel instantiation out of the way, but its still pretty basic here.

package org.dspace.app.launcher;

...
public class ScriptLauncher
{

private static transient DSpaceKernelImpl kernelImpl;

public static void main(String\[\] args)
{

...

CommandService service = kernelImpl.getServiceManager().getServiceByName(CommandService.class.getName(),CommandService.class);

...

try {
            service.exec(Arrays.copyOfRange(args,1,args.length));
        } catch (Exception e) {
            ...
        }
&nbsp;
...
}

}

Ok, at this point your saying. But, wait a second, how did you configure the CommandService, Commands and steps so they could be available.  Thats the majic of Spring, and theres a couple ways we do this, but I will take the most explicit approach that still will allow you to wire in your own Commands later.

Step 4: The Spring Configuration

We place the Spring Configuration in a special place for the DSpace Service Manager, and the Service Manager is pretty smart about finding these.  It expects us to place these files in a couple different places.  But in our example I will show the current places in DSpace 1.7.x. In all cases we want to be placing these files under [~mdiggory:dspace-module]/src/main/resources/spring

dspace-spring-core-service.xml

This location allows us to "augment the existing services without actually overriding them", we generally reserve this for the core dspace services like RequestService, ConfigurationService, SessionService, etc

dspace-spring-addon-[~mdiggory:a unique name]-service.xml

This location allows us to "override" the XML loading a specific service by overwriting its configuration in one of our own
Now to show you our Launcher Service. The trick here is we will use a feature in Spring called, autowire byType, it will collect all the Commands and wire them together for us, not matter the type. I'll save you having to view the whole file, DSpace Spring Services Tutorial~mdiggory:you can see it here if you like.

<?xml version="1.0" encoding="UTF-8"?>
<!--

    The contents of this file are subject to the license and copyright
    detailed in the LICENSE and NOTICE files at the root of the source
    tree and available online at

    http://www.dspace.org/license/

-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <!-- allows us to use spring annotations in beans -->
    <context:annotation-config/>


    <!-- Instantiate the Launcher Service and Autowire its dependencies -->
    <bean class="org.dspace.app.launcher.ScriptLauncher" autowire="byType"/>

    <bean class="org.dspace.app.launcher.Command">
        <property name="name" value="checker"/>
        <property name="description" value="Run the checksum checker"/>
        <property name="steps">
            <list>
                <bean class="org.dspace.app.launcher.Step">
                    <property name="className" value="org.dspace.app.checker.ChecksumChecker"/>
                </bean>
            </list>
        </property>
    </bean>

You'll notice we now have created the ScriptLauncher Service via a bean definition

 <!-- Instantiate the Launcher Service and Autowire its dependencies -->
<bean class="org.dspace.app.launcher.ScriptLauncher" autowire="byType"/>

And that we have created the Command for the Checksum Checker here...

<bean class="org.dspace.app.launcher.Command">
   <property name="name" value="checker"/>
   <property name="description" value="Run the checksum checker"/>
   <property name="steps">
     <list>
       <bean class="org.dspace.app.launcher.Step">
         <property name="className" value="org.dspace.app.checker.ChecksumChecker"/>
       </bean>
     </list>
    </property>
</bean>

You also recall that with DSpace we have a DSRun command that gives us a different behavior that normal commands, it rather wants to recieve the class, rather than defining it itself, so we exended Step and introduce a special Step in DSpace that just knows how to do that.

 <bean class="org.dspace.app.launcher.Command">
    <property name="name" value="dsrun"/>
    <property name="description" value="Run a class directly"/>
    <property name="steps">
       <list>
          <bean class="org.dspace.app.launcher.DSRUNStep"></bean>
       </list>
    </property>
 </bean>

So yes again, we made sure that our DSRUNStep is smart and simple minded, it just knows how to do one thing very well, thats execute a class passed as an option on the commandline. It does it smartly reusing a generic Step and setting all those values it needs within it.

package org.dspace.app.launcher;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class DSRUNStep extends Step {


    public int exec(String[] args) {

        Step ds = new Step();

        ds.setArguments(this.getArguments());

        System.out.println(args.length);
        System.out.println(args[0]);


        if (args.length < 1)
        {
            throw new RuntimeException("Error in launcher: DSRun Step Missing class name.");
        }

        /* get the classname from arguments */
        ds.setClassName(args[0]);


        ds.setPassUserArgs(true);

        /* Execute the requested class with the appropriate args (remove  classname) */
        return ds.exec(Arrays.copyOfRange(args,1,args.length));
    }

}

Adding Commands in other Addon Modules

One of the luxuries we get from using the autowire byType in our spring configuration within the ServiceManager, is that now we can allow others to toss in their commands regardless of what they are named we just need to know that they implement our Command interface and provide Steps we can execute.  For instance, to introduce a command that will allow us to index discovery or operate on teh statistics indexes, we can, in our Discovery and Statistics Addons_ add additional src/main/resources/spring/dspace-spring-addon-discovery-service.xml_, do the following:

Discovery (dspace/trunk/dspace-discovery/dspace-discovery-provider/src/main/resources/spring)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-2.5.xsd">

   <bean class="org.dspace.app.launcher.Command">
      <property name="name" value="update-discovery-index"/>
      <property name="description" value="Update Discovery Solr Search Index"/>
      <property name="steps">
        <list>
          <bean class="org.dspace.app.launcher.Step">
             <property name="className" value="org.dspace.discovery.IndexClient"/>
          </bean>
        </list>
      </property>
    </bean>
</beans>

Statistics (dspace/trunk/dspace-statistics/src/main/resources/spring)

<?xml version="1.0" encoding="UTF-8"?>
<!--

    The contents of this file are subject to the license and copyright
    detailed in the LICENSE and NOTICE files at the root of the source
    tree and available online at

    http://www.dspace.org/license/

-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd">


    <bean class="org.dspace.app.launcher.Command">
        <property name="name" value="stats-log-converter"/>
        <property name="description" value="Convert dspace.log files ready for import into solr statistics"/>
        <property name="steps">
            <list>
                <bean class="org.dspace.app.launcher.Step">
                    <property name="className" value="org.dspace.statistics.util.ClassicDSpaceLogConverter"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean class="org.dspace.app.launcher.Command">
        <property name="name" value="stats-log-importer"/>
        <property name="description" value="Import previously converted log files into solr statistics"/>
        <property name="steps">
            <list>
                <bean class="org.dspace.app.launcher.Step">
                    <property name="className" value="org.dspace.statistics.util.StatisticsImporter"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean class="org.dspace.app.launcher.Command">
        <property name="name" value="stats-util"/>
        <property name="description" value="Statistics Client for Maintenance of Solr Statistics Indexes"/>
        <property name="steps">
            <list>
                <bean class="org.dspace.app.launcher.Step">
                    <property name="className" value="org.dspace.statistics.util.StatisticsClient"/>
                </bean>
            </list>
        </property>
    </bean>

</beans>

Summary

So in this tutorial, We've introduced you to The ServiceManager, how to rethink your approach using Spring and not hardwire your application by binding its business logic to the parsing of configuration files or other sources of configuration, and how to lightly wire it together so that others can easily extend the implementation on their own. Likewise, we've shown you the power of how to separate the configuration of the Script Launcher into Spring configuration such that new Commands can be added to DSpace Maven Addon Module so that you can write your own commands without having to do anything at all in the dspace.cfg or launcher.xml in the DSpace config directory to enable them. A solution that will no doubt make your maintenence of those code changes much much easier as DSpace releases new versions and you need to merge yout config files to stay in line.

It is my belief that these approaches, when applied to more integral parts of DSpace such as the Core Data Model, Workflows, Metadata Registries, Format Registries, Curation Tools, Media Filters, EventManager Consumers will lead to a DSpace platform that will be easier to customize, capable of greater customization, and easier to upgrade. 

Further Reading and Resources:

  • No labels