Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Firstly we need to get commands:

Code Block
        
        // 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()))
            {

...

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".

Code Block

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 =       List<String> argsToPass = Arrays.asList(args);

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

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


        // Instantiate and execute the class
        try {

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

Class\[\]  argTypes = {         Class[] argTypes = {useargs.getClass()};
Object\[\            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

...

Code Block
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 intvoid 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()){
     {
                // 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

...

Code Block
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="
<!--

    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

Code Block
 <!-- Instantiate the Launcher Service and Autowire its dependencies -->
<bean class="org.dspace.app.launcher.CommandScriptLauncher">
<property nameautowire="byType"/>

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

Code Block
<bean class="org.dspace.app.launcher.Command">
   <property name="name" value="checker"/>
   <property name="description" value="Run the checksum checker"/>
   name" value="dsrun"/>
<property name="description" value="Run a class directly"/>
<property name="steps">
     <list>
       <bean class="org.dspace.app.launcher.DSRUNStepStep"></bean>
</list>
</property>
</bean>


</beans>

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

Code Block
 <\!-\- Instantiate the Launcher Service and Autowire its dependencies \-->
<bean class>
         <property name="className" value="org.dspace.app.launcher.ScriptLauncher" autowire="byType"/>

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

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.

Code Block
 <bean class
Code Block
 <bean class="org.dspace.app.launcher.Command">
    <property name="name" value="checkerdsrun"/>
    <property name="description" value="Run thea checksumclass checkerdirectly"/>
    <property name="steps">
       <list>
          <bean class="org.dspace.app.launcher.StepDSRUNStep">
<property name="className" value="org.dspace.app.checker.ChecksumChecker"/>
</bean>
</list>
></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.

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.

Code Block
package 
Code Block
 <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.

Code Block
package org.dspace.app.launcher;

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

import org.dspace.core.ConfigurationManager;
import org.dspace.servicemanager.DSpaceKernelImpl;
import org.dspace.servicemanager.DSpaceKernelInit;
import org.dspace.services.RequestService;

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

public class DSRUNStep extends Step {

public int exec(String\[\] args) {

if (args.length < 1){
            throw new RuntimeException("Error in launcher: dsrun command missing class name.");ScriptLauncher
{
    /** The service manager kernel */
    private static transient DSpaceKernelImpl kernelImpl;

    /**
     * Execute the DSpace script launcher
     *
     * @param  }



Step ds = new Step();
ds.setArguments(this.getArguments());   args Any parameters required to be passed to the scripts it executes
     ds.setPassUserArgs(true);
/\* get the classname from arguments \*/
ds.setClassName(args[0]);

/\* 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)

Code Block
<?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)

*/
    public static void main(String[] args)
    {

        //...

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

        // Establish the request service startup
        RequestService requestService = kernelImpl.getServiceManager().getServiceByName(RequestService.class.getName(), RequestService.class);
        if (requestService == null) {
             throw new IllegalStateException("Could not get the DSpace RequestService to start the request transaction");
        }

        try {
            service.exec(Arrays.copyOfRange(args,1,args.length));
        } catch (Exception e) {
            System.err.println("Exception: " + e.getMessage());
            e.printStackTrace();
            // ...
        }
        finally
        {
             //...
        }

        // The command wasn't found
        System.err.println("Command not found: " + args[0]);
        display(commands);
        System.exit(1);
    }
}

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)

Code Block
<?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)

code
Code Block
<?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/

--
Code Block
<?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="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>
 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, I'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 a configuration file or other source of configuration, and how to lightly wire it together so that others can easily extend the implementation on their own. Likewise, I've show you the power of how to separate the configuration of the Script Launcher to a file that 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.  The final take home mantra for you to repeat 1000 times tonight before going to sleep... "If I don't have to alter configuration to add my changes into DSpace, I wouldn't need to merge differences when I upgrade."