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
void testArtifactPath(TestExpansion<DataElementComposite> expansion) {
expansion
.assertThatExpander()
.hasArtifactPath("C:/MODULE_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:

@TestModel(select = "testComp::City")
Spec baseModel() {
return component("testComp",
dataElement("City",
set("packageName", "net.palver.test")));
}

This will build a Component with a single DataElement City. The TestModel annotation defines which part of the model we need in the test (testComp::City).

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

@Test
public void test_artifactPath() {
assertThat(tester.artifactPath(baseModel()),
isPath("C:/MODULE_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:/MODULE_ROOT/logic/gen/common/src/net/palver/test/CityFoo.java"
but: was "
C:/MODULE_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>$module.rootDirectory$/$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)
  • program.rootDirectory: The root directory for the application.
  • module.rootDirectory: The root directory for the component.
  • 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
Harvest directory

The location of the harvest files is also based on the artifact path. When harvesting, the expander framework will render the artifact path, but insert different values for the variables. E.g. program.rootDirectory will now point to the harvest directory of the Application, module.rootDirectory will point to the harvest directory of the Component and artifactSubFolders will not contain /gen/.

Program and Modules

Standard application models are build with Applications and Components, but other models might use other elements with similar roles. Program and Module are more general names for the same concepts.

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 the regular directories

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