Chapter 2. Writing tasks

Table of Contents

Development environment
Implementation example

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.

Tasks can be developed in any Java IDE. The target Java version should be 6.0.

When a task is run from Schmant, all classes in the Jar files in the Schmant distribution's lib directory is available to it. This includes the Schmant classes and the classes in the EntityFS core, util, Jar and Zip Jars. Additional Jar files can be packaged in the task's task package. See the section called “Task package file layout”.

The tools/create_task_package_eclipse_workspace.js script in the Schmant distribution can be used to create an empty Eclipse workspace for developing a task and a task package in.

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()));