Mapping Imports
Many generated artifacts have some form of import or include statements that need to be expanded. This guide explains how to manage imports for some of the standard file types. After that, it explains how to add support for other import strategies.
The syntax used here is introduced in version 2023.1.0
of the mapping xsd, so make sure the xmlns
attribute point to
this version or one more recent:
<?xml version="1.0" encoding="UTF-8" ?>
<mapping xmlns="https://schemas.normalizedsystems.org/xsd/expanders/2023/1/0/mapping">
Next, we need to add the artifact definition to the head of the mapping file. This definition defines the importStrategy (see below) and the location of the artifact currently being generated.
<mapping xmlns="https://schemas.normalizedsystems.org/xsd/expanders/2023/1/0/mapping">
<artifact this="'{artifact}'" importStrategy="{strategy}"/>
The this
attribute is an expression, so you will need to use quotes when defining a String value.
Java Imports
For java artifacts, define the importStrategy java
and the fully qualified class name of the artifact.
<artifact this="dataElement.qualifiedName + 'Bean'" importStrategy="java"/>
Then, add define classes in uses
statements.
These statements declare that the defined class is being used and thus needs to be imported.
<uses eval="'jakarta.ejb.Stateless'"/>
You can also define them in a group
statement.
This can be useful if the imports are only added conditionally.
<group name="importMethod" if="dataElement.getOption('io.import').defined">
<uses eval="'net.democritus.upload.ImportFile'"/>
<uses eval="'net.democritus.upload.ImportResult'"/>
</group>
Or, you can define them in a list
statement.
This can be useful for classes used by LinkFields or ValueFields.
<list name="linkVariables" eval="dataElement.fields" param="field"
filter="field.linkField neq null"
unique="field.linkField.targetElement">
<value name="class" eval="classBuilder.from(field.linkField.targetElement)"/>
<uses eval="field.linkField.targetElement.qualifiedName + 'Local'"/>
</list>
Finally, add @imports
to the template.
The classes will be de-duplicated and filtered to include only the necessary imports.
base() ::= <<
package <packageName>;
@imports
@Stateless
public class <dataElement.name>Bean {}
>>
package net.demo;
import jakarta.ejb.Stateless;
import net.democritus.upload.ImportFile;
import net.democritus.upload.ImportResult;
import net.demo.people.SpacePortLocal;
@Stateless
public class StarshipThingBean {}
Static imports can be defined by adding type="static"
to the uses
statement.
<uses type="static" eval="'net.democritus.sys.NullDataRef.EMPTY_DATA_REF'"/>
CommonJS/AMD imports
For js artifacts, define the importStrategy amd
or commonjs
.
<artifact
this="component.name + '/' + dataElement.name.firstToLower
+ '/' + dataElement.name.firstToLower + '-std-edit.js'"
importStrategy="commonjs"/>
AMD dependencies are part of the module header. This is how requirejs modules are intended to be written, but you might run into issues when mixing it with the commonjs notation.
base() ::= <<
@imports
function defineEditDialog() {
}
})
>>
define([
'require',
'knockout',
'nsx/triggers'
], function (
require,
ko,
Trigger
) {
function defineEditDialog() {
}
})
CommonJS dependencies are imported by calling the require()
function.
base() ::= <<
define(function (require) {
@imports
function defineEditDialog() {
}
})
>>
define(function (require) {
const ko = require('knockout');
const Trigger = require('nsx/triggers');
function defineEditDialog() {
}
})
Define imported artifacts with uses statements.
<!-- Add ` as {var}` to define the library name. -->
<uses eval="'knockout as ko'"/>
<!-- Optionally place `{` and `}` around the module path. -->
<uses eval="'{nsx/triggers} as ko'"/>
<!-- If no variable name is provided, a name is picked based on the filename. -->
<uses eval="'nsx/nsx-application'"/>
Technology-dependent Imports
In some cases, you may need to be able to change imports when switching technologies. E.g. some libraries may change packages with a new version. For this, you can add a TechnologyLexicon. A TechnologyLexicon is linked to a technology and will only be activated if that technology is present in the Application (Program).
It defines a number of CoordinateRedirections, which are used to map usages to the correct coordinate.
<dataResource type="expansionControl::TechnologyLexicon">
<technologyLexicon>
<technology name="JAVA_EE"/>
<coordinateRedirections>
<coordinateRedirection>
<coordinate>jakarta.ejb.EJB</coordinate>
<target>javax.ejb.EJB</target>
</coordinateRedirection>
<coordinateRedirection>
<coordinate>jakarta.ejb.Stateless</coordinate>
<target>javax.ejb.Stateless</target>
</coordinateRedirection>
</coordinateRedirections>
</technologyLexicon>
</dataResource>
Providing new ArtifactImportStrategies
It is possible to provide more import strategy options. You will need to:
- Add a DataResource with the new ArtifactImportStrategy
- Implement the
ArtifactImportResolver
interface to process the ArtifactUsages - Implement the conversion to output for the template in an
ArtifactImportsDefinition
implementation
- artifactImportStrategies.xml
- HelloWorldImportResolver.java
- HelloWorldImportsDefinition.java
<dataResource type="expansionControl::ArtifactImportStrategy">
<artifactImportStrategy name="helloWorld">
<implementation>net.democritus.expander.hello.HelloWorldImportResolver</implementation>
</artifactImportStrategy>
</dataResource>
package net.democritus.expander.hello;
import net.democritus.expansion.CoordinateRedirectionComposite;
import net.democritus.expansion.TechnologyLexiconComposite;
import net.democritus.expansion.imports.*;
import java.util.*;
import java.util.stream.Collectors;
public class HelloWorldImportResolver implements ArtifactImportResolver {
private String me;
@Override
public HelloWorldImportsDefinition resolve(List<ArtifactUsage> artifactUsages) {
List<String> names = artifactUsages.stream()
.map(name -> name.equals(me) ? "me" : name)
.distinct()
.collect(Collectors.toList());
return new HelloWorldImportsDefinition(names);
}
@Override
public void setArtifactDescription(ArtifactDescription artifactDescription) {
// Retrieve any useful information from the ArtifactDescription, like the `this` reference.
this.me = artifactDescription.getLocation()
}
@Override
public void setTechnologyLexicons(List<TechnologyLexiconComposite> lexicons) {
// Handle TechnologyLexicons to support Technology-dependent imports
}
@Override
public void setImportsAnchor(ImportsAnchor importsAnchor) {
// Handle imports anchor definition in case of parameters (see below)
}
}
package net.democritus.expander.hello;
import net.democritus.expansion.imports.ArtifactImportsDefinition;
import java.util.*;
import java.util.stream.Collectors;
public class HelloWorldImportsDefinition implements ArtifactImportsDefinition {
private final List<String> names;
public HelloWorldImportsDefinition(List<String> names) {
this.names = names;
}
@Override
public List<String> getImportStatements() {
return names.stream()
.map(name -> "Hello " + name + "!")
.collect(Collectors.toList());
}
}
Then, in the artifact definitions, set the importStrategy to the name of the ArtifactImportStrategy.
<?xml version="1.0" encoding="UTF-8" ?>
<mapping xmlns="https://schemas.normalizedsystems.org/xsd/expanders/2023/1/0/mapping">
<artifact this="'Mark'" importStrategy="helloWorld"/>
<uses eval="'Ben'"/>
<uses eval="neighbour"/>
</mapping>
Import Parameters
It is possible to define parameters in an imports anchor:
@imports(param1, param2=value)
These parameters are passed to the ArtifactImportResolver with the setImportsAnchor()
method.
This can be used if you need imports in multiple locations in your template. E.g. in Angular Component files, there are imports for other file modules, and imports in the Component declaration.
// file imports
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
@Component({
selector: 'my-component',
standalone: true,
imports: [
// component imports
CommonModule
]
})
export class MyComponent {}
The template uses a parameter to keep the imports separate:
base() ::= <<
@imports
@Component({
selector: 'my-component',
standalone: true,
imports: [
@imports(Component)
]
})
export class MyComponent {}
>>
The ArtifactImportResolver then implements the import resolution depending on the provided parameters.
public ArtifactImportsDefinition resolve(List<ArtifactUsage> usages) {
if (importsAnchor.hasParam("Component")) {
return resolveComponentImports(usages);
} else {
return resolveEsModuleImports(usages);
}
}