Skip to main content

Defining the Artifact Path

For an expander, the artifact path and name define where the generated artifact will be located and how it will be named.

Testing the artifact path

To start, look at the {Expander}Test.java file. By default, it will have a test method called test_artifactPath:

@Test
public void test_artifactPath() {
assertThat(tester.artifactPath(baseModel()),
isPath("C:/COMPONENT_ROOT/some/file.ext"));
}

The test uses a baseModel method to construct a model to test against. Let's say we have a dataElement expander, we can then start by defining a data element:

private DataElementComposite baseModel() {
return specBuilder.buildAndFind(
component("testComp",
dataElement("City",
set("packageName", "net.palver.test"))),
dataElement("testComp::City"));
}

The buildAndFind() method takes 2 parameters; a specification of the model (here component("testComp", ...)) and a reference to the element you wish to extract from it (dataElement("testComp::City")).

Next, we modify the test to test a relevant path:

@Test
public void test_artifactPath() {
assertThat(tester.artifactPath(baseModel()),
isPath("C:/COMPONENT_ROOT/shared/gen/common/src/net/palver/test/CityFoo.java"));
}

If everything is done correctly, the test should fail on a mismatch:

java.lang.AssertionError:
Expected: "a string
C:/COMPONENT_ROOT/logic/gen/common/src/net/palver/test/CityFoo.java"
but: was "
C:/COMPONENT_ROOT/logic/gen/common/src//"

Defining the artifact path

To define the artifact path and name we edit the {Expander}.xml file.

E.g. we can fix the path to match the test above with the following values:

<artifactName>$dataElement.name$Foo.java</artifactName>
<artifactPath>$componentRoot.directory$/$artifactSubFolders$/$dataElement.packageName;format="toPath"$</artifactPath>

The artifactName and artifactPath are small embedded templates similar to the other templates. They can make use of the following variables:

  • dataElement: Which is the composite of the target element, the name of this variable is based on the element type. (E.g. for a TaskElement expander, this would be taskElement)
  • dataElementContext/expansionContext: A context object of type DataElementExpansionContext with extra information (generally not needed for regular expanders)
  • applicationRoot.directory: The root directory of the application, which resolves to {expansionDirectory}/{application.shortName}. Note that using this in your artifactPath means that files are harvested to the application's source directory. See below how you can fix this.
  • componentRoot.directory: The root directory of the component, which resolves to {applicationRoot.directory}/components/{component.name}
  • artifactSubFolders: The sub-folders based on the layer-technology-sourceType settings (E.g. logic/gen/common/src)

Furthermore, you can use formats in the template as follows:

$dataElement.name;format="firstToLower"$

The formats available are:

  • firstToLower: converts the first character to lowerCase
  • firstToUpper: converts the first character to upperCase
  • lower: converts all characters to lowerCase
  • upper converts all characters to upperCase
  • toPath: converts all . characters to /
  • kebab: converts camelCase to dashed lowercase

Using OGNL for more complex logic

caution

Using artifactModifiers is considered an advanced topic and should be used with care and only when absolutely necessary.

Sometimes, there are paths that cannot be represented with the resources available in the template. In that case, there is the possibility to define <artifactModifiers> in the xml. This contains an ognl definition of a map with key-value pairs which can be used in the artifactPath and artifactName.

E.g. to remove all vowels from the dataElement name:

<artifactModifiers>#{
'dataElementNoVowels' : dataElement.name.replaceAll('[aeiouy]', '')
}
</artifactModifiers>
<artifactName>$dataElementNoVowels$Foo.java</artifactName>
<artifactPath>$componentRoot.directory$/$artifactSubFolders$/$dataElement.packageName;format="toPath"$</artifactPath>

The artifactModifiers can use the same variables as the artifactPath.

Defining artifactPaths for locations outside of the regular directories

caution

You will need at least expanders 4.15.0 to use these features.

In some cases you may need to define an artifact path that is different from the standard path. Important to know is that when using applicationRoot.directory in your artifact path, harvest files will end up in the application's source directory. To prevent this, you can define the directory for harvest and expansion separately.

First, create a separate class to define the new componentRoot.

package net.demo.foo;

import net.democritus.elements.ApplicationComposite;
import net.democritus.elements.DataElementComposite;

import java.util.Map;
import java.util.Objects;

public class MyCustomDirectories {

public static String getComponentRoot(Map<String, Object> data) {
boolean isHarvest = getProperty("isHarvest", data);
String applicationDirectory = getProperty("directory", getProperty("applicationRoot", data));
String componentDirectory = getProperty("directory", getProperty("componentRoot", data));
ApplicationComposite application = getProperty("application", data);
DataElementComposite dataElement = getProperty("dataElement", data);

return isHarvest ?
componentDirectory + "/foo" :
applicationDirectory + "/foo/src/app/" + application.getName().toLowerCase() + '/' + dataElement.getComponent().getName();
}

@SuppressWarnings("unchecked")
private static <T> T getProperty(String key, Map<String, Object> data) {
return (T) Objects.requireNonNull(data.get(key), "Property '" + key + "' not found. Available properties: " + data.keySet());
}

}

The class receives a map with the data from the artifactPathBuilder. This contains the variables mentioned above. In addition to those we also use isHarvest, which is a boolean that is true only when deciding the harvest location. We can use this value to decide to return either the component root directory for the expansion or the harvest files.

Next, we use this class in artifactModifiers to insert it into the artifactPath:

<artifactModifiers>#{
'myComponentRoot' : @net.demo.foo.MyCustomDirectories@getComponentRoot(#this)
}
</artifactModifiers>
<artifactName>$dataElement.name$Foo.java</artifactName>
<artifactPath>$myComponentRoot$/$artifactSubFolders$/$dataElement.packageName;format="toPath"$</artifactPath>

In addition to this, there are also some other variables to give fine-grained control in defining an artifactPath.

  • layerDir: The directory name for the layerType. (E.g logic)
  • technologyDir: The directory name for the technology. (E.g common or ejb3)
  • sourceDir: The directory name for the sourceType. (E.g src or js)
  • genDir: Resolves to gen/ for the expansion path, and an empty string for the harvest path
  • expansionRoot.directory: The root directory of the expansion
  • scriptsRoot.directory: A directory for scripts which resolves to {expansionDirectory}/scripts