This chapter continues from where Creating the workspace definition left us.

Writing a custom Target

When the existing functionality is not enough, you can implement your own target.

Target with ingredients

Let's define a custom target that sums the sizes of (the cached contents of) given paths. First we will write a naive implementation that just uses the cached contents of other paths without declaring them ingredients.

~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/IwanttutorialWorkspace.java"
package com.example.wsdef;
import java.util.Arrays;
import java.util.List;
import org.fluentjava.iwant.api.core.HelloTarget;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.SideEffect;
import org.fluentjava.iwant.api.model.Target;
import org.fluentjava.iwant.api.wsdef.SideEffectDefinitionContext;
import org.fluentjava.iwant.api.wsdef.TargetDefinitionContext;
import org.fluentjava.iwant.api.wsdef.Workspace;
import org.fluentjava.iwant.eclipsesettings.EclipseSettings;
public class IwanttutorialWorkspace implements Workspace {
private final Path ingredient1 = new HelloTarget("ingredient1", "12");
private final Path ingredient2 = new HelloTarget("ingredient2", "345");
private final Target myTarget = new FileSizeSum("file-size-sum",
Arrays.asList(ingredient1, ingredient2));
@Override
public List<? extends Target> targets(TargetDefinitionContext ctx) {
return Arrays.asList(new HelloTarget("hello", "hello from iwant\n"));
return Arrays.asList(new HelloTarget("hello", "hello from iwant\n"),
myTarget);
}
@Override
public List<? extends SideEffect> sideEffects(
SideEffectDefinitionContext ctx) {
return Arrays.asList(EclipseSettings.with().name("eclipse-settings")
.modules(ctx.wsdefdefJavaModule(), ctx.wsdefJavaModule())
.end());
}
}
~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/FileSizeSum.java"
package com.example.wsdef;
import java.io.File;
import java.util.List;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.TargetEvaluationContext;
import org.fluentjava.iwant.api.target.TargetBase;
import org.fluentjava.iwant.coreservices.FileUtil;
class FileSizeSum extends TargetBase {
private final List<Path> pathsToSum;
public FileSizeSum(String name, List<Path> pathsToSum) {
super(name);
this.pathsToSum = pathsToSum;
}
@Override
protected IngredientsAndParametersDefined ingredientsAndParameters(
IngredientsAndParametersPlease iUse) {
return iUse.nothingElse();
}
@Override
public void path(TargetEvaluationContext ctx) throws Exception {
File dest = ctx.cached(this);
System.err.println("Refreshing " + dest);
int pathSizeSum = 0;
for (Path path : pathsToSum) {
File pathFile = ctx.cached(path);
int pathSize = FileUtil.contentAsBytes(pathFile).length;
pathSizeSum += pathSize;
}
FileUtil.newTextFile(dest, pathSizeSum + "\n");
}
}
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/list-of/targets
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
hello
file-size-sum
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r find
(0/1 D! com.example.wsdef.FileSizeSum file-size-sum)
Refreshing /home/hacker/iwant-tutorial/as-iwant-tutorial-developer/.i-cached/target/file-size-sum
(FAILED com.example.wsdef.FileSizeSum file-size-sum)
Target file-size-sum referred to ingredient1 without declaring it an ingredient.
Output asserted

The context iwant passes to our target allows us to resolve a path to a file that contains its cached content. But if we haven't declared the referred path an ingredient, iwant will throw an exception.

We can fix this by declaring the ingredients:

~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/FileSizeSum.java"
package com.example.wsdef;
import java.io.File;
import java.util.List;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.TargetEvaluationContext;
import org.fluentjava.iwant.api.target.TargetBase;
import org.fluentjava.iwant.coreservices.FileUtil;
class FileSizeSum extends TargetBase {
private final List<Path> pathsToSum;
public FileSizeSum(String name, List<Path> pathsToSum) {
super(name);
this.pathsToSum = pathsToSum;
}
@Override
protected IngredientsAndParametersDefined ingredientsAndParameters(
IngredientsAndParametersPlease iUse) {
return iUse.nothingElse();
return iUse.ingredients("pathsToSum", pathsToSum).nothingElse();
}
@Override
public void path(TargetEvaluationContext ctx) throws Exception {
File dest = ctx.cached(this);
System.err.println("Refreshing " + dest);
int pathSizeSum = 0;
for (Path path : pathsToSum) {
File pathFile = ctx.cached(path);
int pathSize = FileUtil.contentAsBytes(pathFile).length;
pathSizeSum += pathSize;
}
FileUtil.newTextFile(dest, pathSizeSum + "\n");
}
}
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
(0/1 D! org.fluentjava.iwant.api.core.HelloTarget ingredient1)
(0/1 D! org.fluentjava.iwant.api.core.HelloTarget ingredient2)
(0/1 D! com.example.wsdef.FileSizeSum file-size-sum)
Refreshing /home/hacker/iwant-tutorial/as-iwant-tutorial-developer/.i-cached/target/file-size-sum
5
Output asserted

Now that we declared the ingredients, iwant automatically told them to refresh their cached contents before telling our target to refresh its. We can see this from the output. The D! before a target's type and name means the target is refreshed because its cached content descriptor (and thus also its content) is missing.

(We can also see that the workspace definition classes were updated. The S~ in the output means they were updated because of modified source ingredients. We just modified the source of our target, a part of the workspace definition.)

iwant uses a target's cached content descriptor and its timestamp to determine whether the target is dirty (needs refreshing) or up-to-date. We as target authors just defined the content descriptor in the method ingredientsAndParameters.

This is what our cached content descriptor looks like:

~/iwant-tutorial $ cat as-iwant-tutorial-developer/.i-cached/descriptor/file-size-sum
com.example.wsdef.FileSizeSum
i:pathsToSum:
ingredient1
ingredient2
Output asserted

Target with parameters

Next we will make our target accept static input, a parameter. We will add a header line above the size sum line. Again, we first try what happens if we don't declare this change to our content definition.

~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/IwanttutorialWorkspace.java"
package com.example.wsdef;
import java.util.Arrays;
import java.util.List;
import org.fluentjava.iwant.api.core.HelloTarget;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.SideEffect;
import org.fluentjava.iwant.api.model.Target;
import org.fluentjava.iwant.api.wsdef.SideEffectDefinitionContext;
import org.fluentjava.iwant.api.wsdef.TargetDefinitionContext;
import org.fluentjava.iwant.api.wsdef.Workspace;
import org.fluentjava.iwant.eclipsesettings.EclipseSettings;
public class IwanttutorialWorkspace implements Workspace {
private final Path ingredient1 = new HelloTarget("ingredient1", "12");
private final Path ingredient2 = new HelloTarget("ingredient2", "345");
private final Target myTarget = new FileSizeSum("file-size-sum",
Arrays.asList(ingredient1, ingredient2));
Arrays.asList(ingredient1, ingredient2), "The sum");
@Override
public List<? extends Target> targets(TargetDefinitionContext ctx) {
return Arrays.asList(new HelloTarget("hello", "hello from iwant\n"),
myTarget);
}
@Override
public List<? extends SideEffect> sideEffects(
SideEffectDefinitionContext ctx) {
return Arrays.asList(EclipseSettings.with().name("eclipse-settings")
.modules(ctx.wsdefdefJavaModule(), ctx.wsdefJavaModule())
.end());
}
}
~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/FileSizeSum.java"
package com.example.wsdef;
import java.io.File;
import java.util.List;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.TargetEvaluationContext;
import org.fluentjava.iwant.api.target.TargetBase;
import org.fluentjava.iwant.coreservices.FileUtil;
class FileSizeSum extends TargetBase {
private final List<Path> pathsToSum;
private final String headerLineContent;
public FileSizeSum(String name, List<Path> pathsToSum) {
public FileSizeSum(String name, List<Path> pathsToSum,
String headerLineContent) {
super(name);
this.pathsToSum = pathsToSum;
this.headerLineContent = headerLineContent;
}
@Override
protected IngredientsAndParametersDefined ingredientsAndParameters(
IngredientsAndParametersPlease iUse) {
return iUse.ingredients("pathsToSum", pathsToSum).nothingElse();
}
@Override
public void path(TargetEvaluationContext ctx) throws Exception {
File dest = ctx.cached(this);
System.err.println("Refreshing " + dest);
int pathSizeSum = 0;
for (Path path : pathsToSum) {
File pathFile = ctx.cached(path);
int pathSize = FileUtil.contentAsBytes(pathFile).length;
pathSizeSum += pathSize;
}
FileUtil.newTextFile(dest, pathSizeSum + "\n");
FileUtil.newTextFile(dest,
headerLineContent + "\n" + pathSizeSum + "\n");
}
}
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
5
Output asserted

Whoops, cache invalidation! Unfortunately, at the moment iwant cannot help, if the target author forgets to declare a parameter i.e. input that is not a reference to another path, an ingredient.

We can fix this by declaring the header line a parameter:

~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/FileSizeSum.java"
package com.example.wsdef;
import java.io.File;
import java.util.List;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.TargetEvaluationContext;
import org.fluentjava.iwant.api.target.TargetBase;
import org.fluentjava.iwant.coreservices.FileUtil;
class FileSizeSum extends TargetBase {
private final List<Path> pathsToSum;
private final String headerLineContent;
public FileSizeSum(String name, List<Path> pathsToSum,
String headerLineContent) {
super(name);
this.pathsToSum = pathsToSum;
this.headerLineContent = headerLineContent;
}
@Override
protected IngredientsAndParametersDefined ingredientsAndParameters(
IngredientsAndParametersPlease iUse) {
return iUse.ingredients("pathsToSum", pathsToSum).nothingElse();
return iUse.ingredients("pathsToSum", pathsToSum)
.parameter("headerLineContent", headerLineContent)
.nothingElse();
}
@Override
public void path(TargetEvaluationContext ctx) throws Exception {
File dest = ctx.cached(this);
System.err.println("Refreshing " + dest);
int pathSizeSum = 0;
for (Path path : pathsToSum) {
File pathFile = ctx.cached(path);
int pathSize = FileUtil.contentAsBytes(pathFile).length;
pathSizeSum += pathSize;
}
FileUtil.newTextFile(dest,
headerLineContent + "\n" + pathSizeSum + "\n");
}
}
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
(0/1 D~ com.example.wsdef.FileSizeSum file-size-sum)
Refreshing /home/hacker/iwant-tutorial/as-iwant-tutorial-developer/.i-cached/target/file-size-sum
The sum
5
Output asserted

Now, after the last wish, our target is up-to-date:

~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
The sum
5
Output asserted

But if we change the value of the parameter, the target will become dirty and be refreshed.

~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/IwanttutorialWorkspace.java"
package com.example.wsdef;
import java.util.Arrays;
import java.util.List;
import org.fluentjava.iwant.api.core.HelloTarget;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.SideEffect;
import org.fluentjava.iwant.api.model.Target;
import org.fluentjava.iwant.api.wsdef.SideEffectDefinitionContext;
import org.fluentjava.iwant.api.wsdef.TargetDefinitionContext;
import org.fluentjava.iwant.api.wsdef.Workspace;
import org.fluentjava.iwant.eclipsesettings.EclipseSettings;
public class IwanttutorialWorkspace implements Workspace {
private final Path ingredient1 = new HelloTarget("ingredient1", "12");
private final Path ingredient2 = new HelloTarget("ingredient2", "345");
private final Target myTarget = new FileSizeSum("file-size-sum",
Arrays.asList(ingredient1, ingredient2), "The sum");
Arrays.asList(ingredient1, ingredient2), "The sum of file sizes");
@Override
public List<? extends Target> targets(TargetDefinitionContext ctx) {
return Arrays.asList(new HelloTarget("hello", "hello from iwant\n"),
myTarget);
}
@Override
public List<? extends SideEffect> sideEffects(
SideEffectDefinitionContext ctx) {
return Arrays.asList(EclipseSettings.with().name("eclipse-settings")
.modules(ctx.wsdefdefJavaModule(), ctx.wsdefJavaModule())
.end());
}
}
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
(0/1 D~ com.example.wsdef.FileSizeSum file-size-sum)
Refreshing /home/hacker/iwant-tutorial/as-iwant-tutorial-developer/.i-cached/target/file-size-sum
The sum of file sizes
5
Output asserted

The D~ in the output means the target was refreshed because of a changed content descriptor. Out of curiosity, let's see what our content descriptor now looks like.

~/iwant-tutorial $ cat as-iwant-tutorial-developer/.i-cached/descriptor/file-size-sum
com.example.wsdef.FileSizeSum
i:pathsToSum:
ingredient1
ingredient2
p:headerLineContent:
The sum of file sizes
Output asserted

Target under construction

When developing a target, it is convenient to declare its own source an ingredient. This way any change to its source makes it dirty. If the target uses other classes, they or their classes also need to be declared.

Let's try it for our target.

~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/FileSizeSum.java"
package com.example.wsdef;
import java.io.File;
import java.util.List;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.Source;
import org.fluentjava.iwant.api.model.TargetEvaluationContext;
import org.fluentjava.iwant.api.target.TargetBase;
import org.fluentjava.iwant.core.javafinder.WsdefJavaOf;
import org.fluentjava.iwant.coreservices.FileUtil;
class FileSizeSum extends TargetBase {
private final List<Path> pathsToSum;
private final String headerLineContent;
private final Source me;
public FileSizeSum(String name, List<Path> pathsToSum,
String headerLineContent) {
String headerLineContent, WsdefJavaOf wsdefJavaOf) {
super(name);
this.pathsToSum = pathsToSum;
this.headerLineContent = headerLineContent;
this.me = wsdefJavaOf.classUnderSrcMainJava(getClass());
}
@Override
protected IngredientsAndParametersDefined ingredientsAndParameters(
IngredientsAndParametersPlease iUse) {
return iUse.ingredients("pathsToSum", pathsToSum)
.parameter("headerLineContent", headerLineContent)
.nothingElse();
.ingredients("me", me).nothingElse();
}
@Override
public void path(TargetEvaluationContext ctx) throws Exception {
File dest = ctx.cached(this);
System.err.println("Refreshing " + dest);
int pathSizeSum = 0;
for (Path path : pathsToSum) {
File pathFile = ctx.cached(path);
int pathSize = FileUtil.contentAsBytes(pathFile).length;
pathSizeSum += pathSize;
}
FileUtil.newTextFile(dest,
headerLineContent + "\n" + pathSizeSum + "\n");
}
}
~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/IwanttutorialWorkspace.java"
package com.example.wsdef;
import java.util.Arrays;
import java.util.List;
import org.fluentjava.iwant.api.core.HelloTarget;
import org.fluentjava.iwant.api.model.Path;
import org.fluentjava.iwant.api.model.SideEffect;
import org.fluentjava.iwant.api.model.Target;
import org.fluentjava.iwant.api.wsdef.SideEffectDefinitionContext;
import org.fluentjava.iwant.api.wsdef.TargetDefinitionContext;
import org.fluentjava.iwant.api.wsdef.Workspace;
import org.fluentjava.iwant.api.wsdef.WorkspaceContext;
import org.fluentjava.iwant.core.javafinder.WsdefJavaOf;
import org.fluentjava.iwant.eclipsesettings.EclipseSettings;
public class IwanttutorialWorkspace implements Workspace {
private final Path ingredient1 = new HelloTarget("ingredient1", "12");
private final Path ingredient2 = new HelloTarget("ingredient2", "345");
private final Target myTarget = new FileSizeSum("file-size-sum",
Arrays.asList(ingredient1, ingredient2), "The sum of file sizes");
private final Target myTarget;
public IwanttutorialWorkspace(WorkspaceContext ctx) {
myTarget = new FileSizeSum("file-size-sum",
Arrays.asList(ingredient1, ingredient2),
"The sum of file sizes", new WsdefJavaOf(ctx));
}
@Override
public List<? extends Target> targets(TargetDefinitionContext ctx) {
return Arrays.asList(new HelloTarget("hello", "hello from iwant\n"),
myTarget);
}
@Override
public List<? extends SideEffect> sideEffects(
SideEffectDefinitionContext ctx) {
return Arrays.asList(EclipseSettings.with().name("eclipse-settings")
.modules(ctx.wsdefdefJavaModule(), ctx.wsdefJavaModule())
.end());
}
}
~/iwant-tutorial $ $EDITOR "as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/IwanttutorialWorkspaceFactory.java"
package com.example.wsdef;
import org.fluentjava.iwant.api.wsdef.Workspace;
import org.fluentjava.iwant.api.wsdef.WorkspaceContext;
import org.fluentjava.iwant.api.wsdef.WorkspaceFactory;
public class IwanttutorialWorkspaceFactory implements WorkspaceFactory {
@Override
public Workspace workspace(WorkspaceContext ctx) {
return new IwanttutorialWorkspace();
return new IwanttutorialWorkspace(ctx);
}
}
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
(0/1 D~ com.example.wsdef.FileSizeSum file-size-sum)
Refreshing /home/hacker/iwant-tutorial/as-iwant-tutorial-developer/.i-cached/target/file-size-sum
The sum of file sizes
5
Output asserted

Our target is now up-to-date, but if we touch its source, it will be refreshed.

~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
The sum of file sizes
5
Output asserted
~/iwant-tutorial $ touch as-iwant-tutorial-developer/i-have/wsdef/src/main/java/com/example/wsdef/FileSizeSum.java
~/iwant-tutorial $ as-iwant-tutorial-developer/with/bash/iwant/target/file-size-sum/as-path | xargs -r cat
(0/1 S~ org.fluentjava.iwant.api.javamodules.JavaClasses iwant-tutorial-wsdef-main-classes)
(0/1 S~ com.example.wsdef.FileSizeSum file-size-sum)
Refreshing /home/hacker/iwant-tutorial/as-iwant-tutorial-developer/.i-cached/target/file-size-sum
The sum of file sizes
5
Output asserted