Schmant Task Author's guide

Revision History
Revision 1.02009.07.17

Table of Contents

1. Introduction
This manual
2. Writing tasks
Development environment
Implementation example
3. Creating a task package
Extension task packages
Task package file layout
Accessing task package resources from tasks
4. Writing task reference documentation
Reference documentation for a task
Reference documentation for a task package
Building task reference documentation
5. Packaging a task package
Workspace layout
Build
Bibliography

List of Tables

3.1. Plugin extension points

List of Examples

2.1. Implementation of RobbersLanguageTranslatorTask
2.2. Implementation of RobbersLanguageTranslatorTF
2.3. Using the Robber's language translator task
3.1. Findbugs task package manifest
3.2. PDFDoclet task package manifest
3.3. Launching a process using extlib files
4.1. Task reference documentation
4.2. Task package documentation

Schmant tasks are the building blocks of build scripts. Since build scripts are programs, it's entirely possible to program all build code in them. However, it can sometimes be worth while to implement commonly used build logic in tasks. Some advantages of tasks compared with having complex code in build scripts are:

  • Tasks are easier to reuse.
  • Tasks are easier to unit test.
  • Tasks are easier to run in a TaskExecutor.
  • Tasks can be distributed to others in task packages.

The disadvantage is that tasks require a greater effort to implement.

Collections of tasks are packaged in task packages, which is the standard way of distributing Schmant tasks. A task package is a Zip file containing zero or more task implementations, other classes and the task reference documentation sources.

A build script uses TaskFactory:s to configure and create Task:s. Behind the scenes, a task factory uses a TaskSpecification object for storing task configuration in. When the build script asks the task factory to create a Task, the task factory calls the task specification's createTask method. If the build script continues to configure the task factory after it has created the task, the factory makes a copy of the task specification using the specification's copyProperties method and proceeds configuring the copy. By doing so, it makes sure that the created task can read its configuration from the specification without the risk of anyone modifying it.

Writing a Schmant task involves implementing a Task, a TaskSpecification and a TaskFactory class. There are a number of abstract stub implementations that a Task implementation may inherit. It is highly recommended, but not required, that it inherits AbstractTask or any of its subclasses. AbstractTask implements both Task and TaskSpecification. Task factory classes may inherit AbstractTaskFactory or any of its subclasses.

See the API documentation for details on the abstract Task and TaskFactory implementations.

The fastest way of implementing a new task is probably to get, um, inspiration from an existing task. Schmant's source distribution contains the source code for all of Schmant's own tasks.

Below is a commented version of the task and task factory implementation for a fictive task that translate the contents of a text file to the Robber's language.

Example 2.1. Implementation of RobbersLanguageTranslatorTask

// 
package se.rovarspraket;

// The translator task is a process task. It takes the file from its source
// property, translates it, and puts the result in the location referenced by
// the target property.
//
// The task produces one object (the translated file), which makes it a
// Producer.
//
// Note: This task would have been easier to implement if it had inherited the
// AbstractTextInsertionTask. For pedagogic reasons, it does not.
public final class RobbersLanguageTranslatorTask extends
  AbstractProcessTask<
    RobbersLanguageTranslatorTask> implements
    Producer<WritableFile>
{
  // This ObjectTransformer is used to cast each argument given to the
  // addTranslatedCharacters method into a Character object when flattening the
  // arguments.
  //
  // Inherit from CastingTransformer.
  private static final class CastingToCharacterTransformer
    extends CastingTransformer<Character>
  {
    // Define a singleton instance.
    private static final CastingToCharacterTransformer INSTANCE =
      new CastingToCharacterTransformer();
  }
  
  // The characters that are translated by default
  private static final Set<Character> DEFAULT_TRANSLATED_CHARACTERS =
    new HashSet<Character>(
      Arrays.asList(
        new Character[] { 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n',
          'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z' }));
  
  // The vowel that should be inserted. Default is "o".
  private char m_vowel = 'o';
  // A set of characters to translate
  private final Set<Character> m_translatedCharacters = 
    new HashSet<Character>(DEFAULT_TRANSLATED_CHARACTERS);
  
  // The translated file is assigned to this property when the task is run.
  private final AtomicReference<WritableFile> m_produced =
    new AtomicReference<WritableFile>();

  // Make the constructor package private. We want that only the task factory
  // should be able to instantiate this class.
  RobbersLanguageTranslatorTask()
  {
    // Nothing
  }
  
  // This is required by the Producer interface.
  @Override
  public WritableFile get()
  {
    return m_produced.get();
  }
  
  // Setters called by the task factory
  
  void setVowel(char c)
  {
    m_vowel = c;
  }
  
  void addTranslatedCharacter(char c)
  {
    m_translatedCharacters.add(c);
  }
  
  // Add one or several translated characters. This method will flatten the
  // argument into a list.
  void addTranslatedCharacters(Object o)
  {
    // This method is inherited from the AbstractArgumentChecker class. It
    // checks that none of the supplied objects are null
    check(o);
    
    // Flatten the argument by adding it to a FlatteningList.
    FlatteningList<Character> l = new FlatteningList<Character>();
    // Add all arguments. use a custom ObjectTransformer that casts each
    // argument into a Character.
    l.add(o, CastingToCharacterTransformer.INSTANCE);
    
    // Add the flattened argument to the set.
    m_translatedCharacters.addAll(l);
  }
  
  void clearTranslatedCharacters()
  {
    m_translatedCharacters.clear();
  }
  
  // Replace the default, not very good, log header.
  @Override
  protected String getDefaultLogHeader()
  {
    return "Totroranonsoslolatotinongog " + getSource();
  }
  
  // Create the regular expression pattern that is used to replace the text
  private Pattern createPattern()
  {
    StringBuilder sb = new StringBuilder("[");
    for (Character c : m_translatedCharacters)
    {
      sb.append(c);
    }
    // Make the pattern case-insensitive
    return Pattern.compile(
      sb.append("]").toString(),
      Pattern.CASE_INSENSITIVE);
  }
  
  // This method is called from AbstractTask when the task is run.
  @Override
  protected void runInternal(Report r)
  {
    // Interpret the source and target properties.
    // Since the strategy will only return one object, we can call get() to get
	// it.
    ReadableFile source =
      ArgumentInterpreter.getInstance().interpret(
        getSource(),
        InterpretAsReadableFileStrategy.AS_SINGLE).get();
    
    // The target is a new writable file. If there already is a file at the
    // target location, the overwrite strategy decides what to do with it.
    WritableFile target =
      ArgumentInterpreter.getInstance().
        interpret(
          getTarget(),
          new InterpretAsNewWritableFileStrategy(
            getOverwriteStrategy(),
            ArgumentInterpretationStrategy.ALLOW_ONE_AND_ONLY_ONE_RESULT_OBJECT)).
        get();
    
    Pattern pat = createPattern();
    
    // Read the source file to a String and create a Matcher on the string
    String inText = Files.readTextFile(source);
    Matcher mat = pat.matcher(inText);
    
    // The text replacement loop
    StringBuilder res = new StringBuilder();
    int lastMatch = 0;
    while(mat.find())
    {
      // Copy all characters between this match and the previous match
      res.append(inText.substring(lastMatch, mat.start()));
    	
      // The secret substitution...
      res.append(mat.group());
      res.append(m_vowel);
      res.append(mat.group());
    	
      lastMatch = mat.end();
    }
    
    // Copy all trailing characters
    res.append(inText.substring(lastMatch, inText.length()));
    
    // Write the result to the target file
    Files.writeText(target, res.toString());
      
    // The target is our produced object
    m_produced.set(target);
  }
  
  // This method is called when the task (specification) is copied by the task
  // factory. It copies all properties to the new task.
  @Override
  public void copyProperties(RobbersLanguageTranslatorTask spec)
  {
    // We must call this
    super.copyProperties(spec);
    // The character is immutable, so it can safely be copied to the spec
    spec.m_vowel = m_vowel;
    // The list is mutable, so we only copy the list contents, not the list
    // itself.
    spec.m_translatedCharacters.addAll(m_translatedCharacters);
  }
}
//


The task factory:

Example 2.2. Implementation of RobbersLanguageTranslatorTF

//
package se.rovarspraket;

public final class RobbersLanguageTranslatorTF extends
  AbstractProcessTaskFactory<
    RobbersLanguageTranslatorTF,
    RobbersLanguageTranslatorTask>
{
  // Setters. All values are set on the task specification, which happens to be
  // the task object itself.

  public RobbersLanguageTranslatorTF setVowel(char c)
  {
    getSpecification().setVowel(c);
    return this;
  }

  public RobbersLanguageTranslatorTF addTranslatedCharacter(char c)
  {
    getSpecification().addTranslatedCharacter(c);
    return this;
  }
  
  // Add a single translated character or an array or a collection of translated
  // characters. The argument will be flattened in
  // RobbersLanguageTranslatorTask.
  public RobbersLanguageTranslatorTF addTranslatedCharacters(Object o)
  {
    getSpecification().addTranslatedCharacters(o);
    return this;
  }
  
  public RobbersLanguageTranslatorTF clearTranslatedCharacters()
  {
    getSpecification().clearTranslatedCharacters();
    return this;
  }
  
  // AbstractTaskFactory wants us to implement this. It
  // is called every time that the task factory needs to create a new
  // TaskSpecification object (which happens to be the Task itself).
  @Override
  protected RobbersLanguageTranslatorTask createSpecification()
  {
    return new RobbersLanguageTranslatorTask();
  }
}
//


And this is how the task can be used from a build script:

Example 2.3. Using the Robber's language translator task

enableTaskPackage("se.rovarspraket");

// The source file
var source = new CharSequenceReadableFile("Rhododendron");

// Create the target file in a RAM directory
var target = new FutureFile(
  SchmantFileSystems.createRamFileSystem(),
  "translated.txt");

new dr.RobbersLanguageTranslatorTF().
  setSource(source).
  setTarget(target).
  run();

// Exercise: What does this print?
info(Files.readTextFile(target.getFile()));


Custom tasks and task factories can be used by just including their classes in the class path and importing the task (Java) packages in the script. However, if tasks are to be distributed, they should be packaged in a task package.

A task package has a name that should be globally unique. There are no enforced naming rules, but a good practice is to name a task package similar to Java package, for instance net.findbugs for the Findbugs task package or org.junit.junit4 for the JUnit 4 task package.

To use a task package, a build script must enable it before it can use its task. When a task package is enabled, its libraries are added to the script's classpath. (Or added to the classpath of a class loader unique to the task package if the script is run with isolated class loaders.) Enabling a task package also imports a number of Java packages so that class names in those packages don't have to be fully qualified when used by the build script.

In addition to tasks, a task package may also contain Plugin:s. Plugins adds additional functionality to tasks in other task packages or to classes in Schmant itself. Every plugin extension point is identified by a globally unique name. The name should be the fully qualified name of the plugin implementation required.

Plugins are registered in the org.schmant,plugin.PluginRegistry. A task that has a plugin extension point can use it to get all available implementations.

Schmant itself has the following plugin extension points:


Several plugins may be registered for each extension point. The first registered plugin has the highest precedence.

Task packages are distributed in Zip files. Just like a Java Jar file, the task package file can be used as-is or unpacked. When running a build script, Schmant uses the task package path to find task packages. It works just like Java's classpath.

A standard task package is a standalone module containing tasks, requiring nothing from other task packages. An extension task package is another kind of task package that extends the functionality of another task package. An example from the Schmant distribution is the com.tarsec.javadoc.pdfdoclet task package that adds an ExtJavadocTaskDecorator for creating PDF Javadocs to the Javadoc task.

An extension task package is implemented just like a standard task package, except that it has the classes of the task package to extend available to it. If a build script using an extension task package is run with isolated class loaders (a separate class loader for each task package), the extension task package uses the same class loader as the task package that it extends.

Information on which task package that an extension task package extends is put in the task package manifest file taskpackage.xml. See Example 3.2, “PDFDoclet task package manifest” for an example.

This is a schematic overview over the contents of a task package file:

/root directory       -- The task package root directory. This directory
|                        should have the same name as the task package itself,
|                        for instance net.findbugs.
+ extlib              -- (Optional) Libraries used when running external
| |                      programs.
| + jar files
+ lib                 -- Libraries that are included in the script's classpath
| |                      when the task package is enabled.
| + jar files
+ src                 -- Source files.
| + doc               -- Documentation sources.
|   + taskref         -- Task reference documentation sources.
|     + taskref files -- Task reference documentation files.
|     + overview.xml  -- Task package overview documentation.
+ taskpackage.xml     -- Task package manifest file.

This is the net.findbugs task package's taskpackage.xml file:


This is the taskpackage.xml file for the com.tarsec.javadoc.pdfdoclet extension task package:


Below is an image showing the layout of the org.at4j task package file:

This image shows the layout of the org.junit.junit4 task package file:

The JUnit4TF task runs JUnit in a separate Java process. The extlib catalog of the task package contains Jar files that are necessary for running JUnit4. The task runs a class in the launcher Jar that sets up the environment before running the unit tests.

Task package management in Schmant is performed by the TaskPackageManager class. It contains the getTaskPackage method that a task can use to access its TaskPackage.

The example below shows how a task launches a separate Java process with a classpath consisting of the Jar files in its extlib directory.

Example 3.3. Launching a process using extlib files

// First get the java command to use
// This method invocation gets a "java" or "java.exe" command from:
// a) The Java installation referenced by the JAVA_HOME environment variable, if
//    that is set.
// b) The directories of the PATH environment variable, if that is set.
File javaCmd = JdkUtil.getJdkExecutable("java", "", "exe");

// Create a classpath string from all Jar files found in the extlib directory.

// Our task package.
// The TaskPackageManager class has a static InheritableThreadLocal variable
// containing the task package manager.
TaskPackage tp = TaskPackageManager.
  get().
  getTaskPackage("org.my.task.package");

// Our task package's root directory
Directory tpRoot = tp.getRootDirectory();

Collection<EFile> jarEFiles = Directories.getAllFilesMatching(
  Directories.getDirectory(tpRoot, "extlib"),
  new Glob("*.jar"));

// Make all Jar files File-backed. This is necessary since they are to be used
// by another process.
// The makeFileBacked method copies the file to a temporary directory if it is
// not already File backed. The copy is deleted when the build script
// terminates.
StringBuilder classpath = new StringBuilder();
for (EFile jarEFile : jarEFiles)
{
  classpath.append(
    ECFileResolvableUtil.getFileObject(
      tp.makeFileBacked(jarEFile)).getAbsolutePath());
  classpath.append(File.pathSeparatorChar);
}

// Create an argument list for the process. "true" means that added strings
// containing spaces will be quoted.
ArgumentList al = new ArgumentList(true);

// The program to run
al.add(javaCmd.getAbsolutePath());

// Add the classpath
al.add("-cp").add(classpath.toString());

// Add the class to run
al.add("org.mytask.MyTaskLauncher");

// Save the output from the process to a string
SaveToStringProcessOutputStrategy sos = new SaveToStringProcessOutputStrategy();

// Create a configuration object for the process that we will launch.
ProcessSettings settings = new ProcessSettings().
  setStdoutStrategy(sos).
  setStderrStrategy(sos).
  setArgumentList(al);

// Run the program and wait until it terminates.
ProcessResult result = ProcessSupport.execAndWait(settings);

// Deal with the result...


Reference documentation for tasks and their task factories is built from the documentation sources in the task package.

The reference documentation for a task is written in an XML file formatted according to the tfdoc-1.2 DTD.

Text that is used in several task documentation pages can be put in separate files and included using XML entity references. The system id for an external file is generated from its location in the task package. For instance, if the task package org.my.task.package keeps some frequently occurring text in the org.my.task.package/src/doc/taskref/CommonText.xmlf file, it can be referenced using the system id http://www.schmant.org/org.my.task.package/src/doc/taskref/CommonText.xmlf.

There are some common external entities that can be used from all task reference documentation files to include common text:

http://www.schmant.org/taskref/taskcategories.xmlf

This document is generated automatically when the task reference is built and it contains the <category> tag with contents. It must be included by task reference files.

http://www.schmant.org/taskref/ClasspathConfigurable.xmlf

Documentation of the classpathEntries and classpathDecorators properties for tasks that are ClasspathConfigurable.

http://www.schmant.org/taskref/GeneratorTaskFactory.xmlf

Documentation of the overwriteStrategy property for generator tasks.

http://www.schmant.org/taskref/TaskFactory.xmlf

Documentation of the logHeader, logFooter, traceLogging and reportLevel properties for all Task:s.

http://www.schmant.org/taskref/TaskExecutorDependencyNote.xmlf

A note about TaskDependency consequences when using a task that can schedule subtasks in a TaskExecutor. The note should be included in the description for the task executor property.

Each property gets an anchor (<a> tag) with the name [task_factory_name]_[property_name] that can be used to link to the property. For instance, the UntarTF task factory's fileNameEncodingCharset property's anchor's name is UntarTF_fileNameEncodingCharset.

Below is the documentation file for a fictional process task that translates text file contents to the Robber Language:

Example 4.1. Task reference documentation

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE taskdoc SYSTEM "http://www.schmant.org/dtd/tfdoc-1.2.dtd" [
  <!ENTITY taskCategories SYSTEM 
    "http://www.schmant.org/taskref/taskcategories.xmlf">
  <!ENTITY taskFactory SYSTEM 
    "http://www.schmant.org/taskref/TaskFactory.xmlf">
  <!ENTITY generatorTaskFactory SYSTEM 
    "http://www.schmant.org/taskref/GeneratorTaskFactory.xmlf">
]>
<taskdoc>
<!-- Use an external entity reference to a file containing task category
     information. The included file is generated when the task documentation is
     built. -->
&taskCategories;
<!-- Place this task in the text tasks category.
     It would have been nice to be able to call the task package
     "se.rövarspråket" instead, but Java's XML parser could not locate files
     under the se.rövarspråket directory. In the future, when we're all zipping
     around in our flying cars, the whole world will use UTF-8. Sigh.
     (Robbers Language is rövarspråket in Swedish.) -->
<taskfactory 
  name="RobberLanguageTF" 
  package="se.rovarspraket"
  java-package="se.rovarspraket"
  category="catText" 
  entityfs-aware="yes">

<!-- The master detective! -->
<author>Bill Bergson (Kalle Blomkvist)</author>
<since>1.0</since>

<!-- Document this task's restriction. The restriction element is optional. -->
<restriction>This task only works with text files.</restriction>

<!-- Since the task is a process task, it is also an action and a generator
     task. -->
<implements interface="ActionTaskFactory"/>
<implements interface="GeneratorTaskFactory"/>
<implements interface="ProcessTaskFactory"/>

<!-- Use a CDATA block to be able to insert our own HTML formatting. Note the
     link to the target property. If you want to link to a class, use an
     explicit class link like |api:class:(class name)?linkClass=|. See
     ApiLinksTF documentation. -->
<produces><![CDATA[The interpreted value of the
<a href="#RobbersLanguageTF_target">target</a> property.]]></produces>

<short-description>Translate text files to the Robber
  Language.</short-description>

<!-- Note the link to the example. -->
<description><![CDATA[This task translates text files to
<a href="http://en.wikipedia.org/wiki/Rövarspråket">the Robber Language</a>.</p>

<p>The vowel to use when translating can be set in the
<a href="#RobberLanguageTF_vowel">vowel</a> property.</p>

<p>See <a href="#robberLanguageTF_ex_1">this example</a>.]]></description>

<!-- Insert common properties -->
&taskFactory;
&generatorTaskFactory;

<!-- The propertyset element is a container for properties. -->
<propertyset>
<property name="source" required="yes">
  <description>The text file to translate.</description>

  <!-- Document the property's setter method. If the property has several setter
       methods, they can be listed after each other. -->
  <setter-method name="setSource">

  <!-- The setter method description may be omitted if it is obvious what the
       method does. -->
  <description>Set one text file.</description>

  <!-- List all setter method parameters. Note how the interpretation is
       documented. See the documentation for the ArgumentInterpreterLinksTF for
       information on the format of the argument interpreter reference. -->
  <parameter
    name="o" 
    type="Object"
    interpreter="|ai:ai_readable_file;InterpretAsReadableFileStrategy|">A
  text file.</parameter>
  </setter-method>
</property>

<property name="target" required="yes">
  <description>The target location where the translated file will be
  put.</description>

  <setter-method name="setTarget">
  <parameter 
    name="o"
    type="Object"
    interpreter=
      "|ai:ai_new_writable_file;InterpretAsNewWritableFileStrategy|">The
    target file location.</parameter>
  </setter-method>
</property>

<property name="vowel">
  <description>The vowel to use when translating.</description>
  <setter-method name="setVowel">
    <parameter name="c" type="char">The vowel</parameter>
  </setter-method>

  <!-- This property has a default value. -->
  <default-value><![CDATA[<code>o</code>]]></default-value>
</property>

<property name="translatedCharacters">
  <description>The collection of characters that are translated.</description>
  <setter-method name="addTranslatedCharacter">
    <description>Add one translated character.</description>
    <parameter name="c" type="char">The character to translate.</parameter>
  </setter-method>
  <!-- This setter method flattens the argument list. -->
  <setter-method name="addTranslatedCharacters">
    <description>Add one or several translated characters.</description>
    <parameter name="o" type="Object">One character or an
    array or collection of characters.</parameter>
  </setter-method>
  <setter-method name="clearTranslatedCharacters">
    <description>Clear the list of translated characters.</description>
  </setter-method>
</property>

</propertyset>

<examplesintro>For more examples, see somewhere else.</examplesintro>

<example id="robberLanguageTF_ex_1">

  <!-- This description will be used in the examples reference. -->
  <short-description>Translate all files in a directory hierarchy to the Robber
    Language.</short-description>

  <!-- This description is displayed above the example. -->
  <description><![CDATA[Translate all files in the directory hierarchy under
  <code>src</code> to the Robber Language, putting the translated files in a
    directory hierarchy under <code>target</code>.]]></description>

  <!-- Use an include tag to include the example code. The example text will be
       inserted by the IncludeFilesTask when building the documentation.
     
       By keeping examples in separate files, they are made easier to test. -->
  <code>|include:se.rövarspråket/src/doc/taskref/robberLanguageTF_ex1.js|</code>
</example>
</taskfactory>
</taskdoc>


This chapter describes how the Emma task package is built and packaged from the source files in its development environment. This can serve as a blueprint for other task packages.