package ca.uhn.fhir.tinder.ant;
/*
* #%L
* HAPI Tinder Plugin
* %%
* Copyright (C) 2014 - 2016 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.apache.commons.lang.WordUtils;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.tools.generic.EscapeTool;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.tinder.AbstractGenerator;
import ca.uhn.fhir.tinder.AbstractGenerator.ExecutionException;
import ca.uhn.fhir.tinder.AbstractGenerator.FailureException;
import ca.uhn.fhir.tinder.GeneratorContext;
import ca.uhn.fhir.tinder.GeneratorContext.ProfileFileDefinition;
import ca.uhn.fhir.tinder.TinderStructuresMojo.ValueSetFileDefinition;
import ca.uhn.fhir.tinder.ValueSetGenerator;
import ca.uhn.fhir.tinder.VelocityHelper;
import ca.uhn.fhir.tinder.parser.BaseStructureSpreadsheetParser;
import ca.uhn.fhir.tinder.parser.DatatypeGeneratorUsingSpreadsheet;
import ca.uhn.fhir.tinder.parser.ProfileParser;
import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingSpreadsheet;
import ca.uhn.fhir.tinder.parser.TargetType;
/**
/**
* Generate files from FHIR resource/composite metadata using Velocity templates.
* <p>
* Generates either source or resource files for each selected resource or
* composite data type. One file is generated for each selected entity. The
* files are generated using a Velocity template that can be taken from
* inside the hapi-timder-plugin project or can be located in other projects
* <p>
* The following Ant task properties are used
* <p>
* <table border="1" cellpadding="2" cellspacing="0">
* <tr>
* <td valign="top"><b>Attribute</b></td>
* <td valign="top"><b>Description</b></td>
* <td align="center" valign="top"><b>Required</b></td>
* </tr>
* <tr>
* <td valign="top">version</td>
* <td valign="top">The FHIR version whose resource metadata
* is to be used to generate the files<br>
* Valid values: <code><b>dstu</b></code> | <code><b>dstu2</b></code> | <code><b>dstu3</b></code></td>
* <td valign="top" align="center">Yes</td>
* </tr>
* <tr>
* <td valign="top">projectHome</td>
* <td valign="top">The project's base directory. This is used to
* possibly locate other assets within the project used in file generation.</td>
* <td valign="top" align="center">No. Defaults to: <code>${basedir}/..</code></td>
* </tr>
* <tr>
* <td valign="top">generateResources</td>
* <td valign="top">Should files be generated from FHIR resource metadata?<br>
* Valid values: <code><b>true</b></code> | <code><b>false</b></code></td>
* <td valign="top" align="center" rowspan="4">At least one of these four options must be specified</td>
* </tr>
* <tr>
* <td valign="top">generateDataTypes</td>
* <td valign="top">Should files be generated from FHIR composite data type metadata?<br>
* Valid values: <code><b>true</b></code> | <code><b>false</b></code></td>
* </tr>
* <tr>
* <td valign="top">generateValueSets</td>
* <td valign="top">Should files be generated from FHIR value set metadata?<br>
* This option can only be used if generating multiple files (one file per value-set.)<br>
* Valid values: <code><b>true</b></code> | <code><b>false</b></code></td>
* </tr>
* <tr>
* <td valign="top">generateProfiles</td>
* <td valign="top">Should files be generated from FHIR profile metadata?<br>
* This option can only be used if generating multiple files (one file per profile.)<br>
* Valid values: <code><b>true</b></code> | <code><b>false</b></code></td>
* </tr>
* <tr>
* <td colspan="3" />
* </tr>
* <tr>
* <td valign="top" colspan="3">Java source files can be generated
* for FHIR resources or composite data types. Source files can be
* generated for each selected entity or a single source file can
* be generated containing all of the selected entity. The following configuration
* properties control the naming of the generated source files:
* <p>The following properties will be used when generating multiple source files:<br>
* <targetSourceDirectory>/<targetPackage>/<filenamePrefix><i>element-name</i><filenameSuffix><br>
* where: <i>element-name</i> is the "title-case" name of the selected resource or composite data type.
* <p>The following properties will be used when generating a single source file:<br>
* <targetSourceDirectory>/<targetPackage>/<targetFile>
* <p>
* Note that all dots in the targetPackage will be replaced by the path separator character when building the
* actual source file location. Also note that <code>.java</code> will be added to the filenameSuffix or targetFile if it is not already included.
* </td>
* </tr>
* <tr>
* <td valign="top">targetSourceDirectory</td>
* <td valign="top">The source directory to contain the generated Java packages and classes.</td>
* <td valign="top" align="center">Yes when Java source files are to be generated</td>
* </tr>
* <tr>
* <td valign="top">targetPackage</td>
* <td valign="top">The Java package that will contain the generated classes.
* This package is generated in the <targetSourceDirectory> if needed.</td>
* <td valign="top" align="center">Yes when <i>targetSourceDirectory</i> is specified</td>
* </tr>
* <tr>
* <td valign="top">packageBase</td>
* <td valign="top">The base Java package for related classes. This property
* can be used to reference class in other places in a folder structure.</td>
* <td valign="top" align="center">No</td>
* </tr>
* <tr>
* <td valign="top">targetFile</td>
* <td valign="top">The name of the file to be generated</td>
* <td valign="top" align="center">Yes when generating a single file containing all selected elements</td>
* </tr>
* <tr>
* <td valign="top">filenamePrefix</td>
* <td valign="top">The prefix string that is to be added onto the
* beginning of the resource or composite data type name to become
* the Java class name or resource file name.</td>
* <td valign="top" align="center">No</td>
* </tr>
* <tr>
* <td valign="top">filenameSuffix</td>
* <td valign="top">Suffix that will be added onto the end of the resource
* or composite data type name to become the Java class name or resource file name.</td>
* <td valign="top" align="center">No.</code></td>
* </tr>
* <tr>
* <td colspan="3" />
* </tr>
* <tr>
* <td valign="top" colspan="3">Resource (non-Java) files can also be generated
* for FHIR resources or composite data types. a file can be
* generated for each selected entity or a single file can
* be generated containing all of the selected entity. The following configuration
* properties control the naming of the generated files:
* <p>The following properties will be used when generating multiple files (one for each selected element):<br>
* <targetResourceDirectory>/<targetFolder>/<filenamePrefix><i>element-name</i><filenameSuffix><br>
* where: <i>element-name</i> is the "title-case" name of the selected resource or composite data type.
* <p>The following properties will be used when generating a single file containing all selected elements:<br>
* <targetResourceDirectory>/<targetFolder>/<targetFile>
* </td>
* </tr>
* <tr>
* <td valign="top">targetResourceDirectory</td>
* <td valign="top">The resource directory to contain the generated files.</td>
* <td valign="top" align="center">Yes when resource files are to be generated</td>
* </tr>
* <tr>
* <td valign="top">targetFolder</td>
* <td valign="top">The folder within the targetResourceDirectory where the generated files will be placed.
* This folder is generated in the <targetResourceDirectory> if needed.</td>
* <td valign="top" align="center">No</td>
* </tr>
* <tr>
* <td colspan="3" />
* </tr>
* <tr>
* <td valign="top">template</td>
* <td valign="top">The path of one of the <i>Velocity</i> templates
* contained within the <code>hapi-tinder-plugin</code> Maven plug-in
* classpath that will be used to generate the files.</td>
* <td valign="top" align="center" rowspan="2">One of these two options must be configured</td>
* </tr>
* <tr>
* <td valign="top">templateFile</td>
* <td valign="top">The full path to the <i>Velocity</i> template that is
* to be used to generate the files.</td>
* </tr>
* <tr>
* <td valign="top">velocityPath</td>
* <td valign="top">When using the <code>templateFile</code> option, this property
* can be used to specify where Velocity macros and other resources are located.</td>
* <td valign="top" align="center">No. Defaults to same directory as the template file.</td>
* </tr>
* <tr>
* <td valign="top">velocityProperties</td>
* <td valign="top">Specifies the full path to a java properties file
* containing Velocity configuration properties</td>
* <td valign="top" align="center">No.</td>
* </tr>
* <tr>
* <td valign="top">includeResources</td>
* <td valign="top">A list of the names of the resources or composite data types that should
* be used in the file generation</td>
* <td valign="top" align="center">No. Defaults to all defined resources except for DSTU2,
* the <code>Binary</code> resource is excluded and
* for DSTU3, the <code>Conformance</code> resource is excluded.</td>
* </tr>
* <tr>
* <td valign="top">excludeResources</td>
* <td valign="top">A list of the names of the resources or composite data types that should
* excluded from the file generation</td>
* <td valign="top" align="center">No.</td>
* </tr>
* <tr>
* <td valign="top">valueSetFiles</td>
* <td valign="top">A list of files containing value-set resource definitions
* to be used.</td>
* <td valign="top" align="center">No. Defaults to all defined value-sets that
* are referenced from the selected resources.</td>
* </tr>
* <tr>
* <td valign="top">profileFiles</td>
* <td valign="top">A list of files containing profile definitions
* to be used.</td>
* <td valign="top" align="center">No. Defaults to the default profile
* for each selected resource</td>
* </tr>
* </table>
*
*
*
* @author Bill Denton
*
*/
public class TinderGeneratorTask extends Task {
private String version;
private String projectHome;
private boolean generateResources;
private boolean generateDatatypes;
private boolean generateValueSets;
private boolean generateProfiles;
private File targetSourceDirectory;
private String targetPackage;
private String packageBase;
private String targetFile;
private String filenamePrefix;
private String filenameSuffix;
private File targetResourceDirectory;
private String targetFolder;
// one of these two is required
private String template;
private File templateFile;
private String velocityPath;
private String velocityProperties;
private List<String> includeResources;
private List<String> excludeResources;
private List<ValueSetFileDefinition> valueSetFiles;
private List<ProfileFileDefinition> profileFiles;
private boolean verbose;
private FhirContext fhirContext; // set from version in validateAttributes
/**
*
*/
public TinderGeneratorTask () {
super();
}
@Override
public void execute () throws BuildException {
validateAttributes();
GeneratorContext context = new GeneratorContext();
context.setVersion(version);
context.setBaseDir(projectHome);
context.setIncludeResources(includeResources);
context.setExcludeResources(excludeResources);
context.setValueSetFiles(valueSetFiles);
context.setProfileFiles(profileFiles);
Generator generator = new Generator();
try {
generator.prepare(context);
} catch (ExecutionException e) {
throw new BuildException(e.getMessage(), e.getCause());
} catch (FailureException e) {
throw new BuildException(e.getMessage(), e.getCause());
}
/*
* Deal with the generation target
*/
TargetType targetType = null;
File targetDirectory = null;
if (targetSourceDirectory != null) {
if (targetResourceDirectory != null) {
throw new BuildException("Both [targetSourceDirectory] and [targetResourceDirectory] are specified. Please choose just one.");
}
targetType = TargetType.SOURCE;
if (null == targetPackage) {
throw new BuildException("The [targetPackage] property must be specified when generating Java source code.");
}
targetDirectory = new File(targetSourceDirectory, targetPackage.replace('.', File.separatorChar));
} else
if (targetResourceDirectory != null) {
if (targetSourceDirectory != null) {
throw new BuildException("Both [targetSourceDirectory] and [targetResourceDirectory] are specified. Please choose just one.");
}
targetType = TargetType.RESOURCE;
if (targetFolder != null) {
targetDirectory = new File(targetResourceDirectory, targetFolder);
} else {
targetDirectory = targetResourceDirectory;
}
if (null == targetPackage) {
targetPackage = "";
}
} else {
throw new BuildException("Either [targetSourceDirectory] or [targetResourceDirectory] must be specified.");
}
targetDirectory.mkdirs();
log(" * Output ["+targetType.toString()+"] Directory: " + targetDirectory.getAbsolutePath());
try {
/*
* Single file with all elements
*/
if (targetFile != null) {
if (targetType == TargetType.SOURCE) {
if (!targetFile.endsWith(".java")) {
targetFile += ".java";
}
}
File target = new File(targetDirectory, targetFile);
OutputStreamWriter targetWriter = new OutputStreamWriter(new FileOutputStream(target, false), "UTF-8");
/*
* Next, deal with the template and initialize velocity
*/
VelocityEngine v = VelocityHelper.configureVelocityEngine(templateFile, velocityPath, velocityProperties);
InputStream templateIs = null;
if (templateFile != null) {
templateIs = new FileInputStream(templateFile);
} else {
templateIs = this.getClass().getResourceAsStream(template);
}
InputStreamReader templateReader = new InputStreamReader(templateIs);
/*
* build new Velocity Context
*/
VelocityContext ctx = new VelocityContext();
if (packageBase != null) {
ctx.put("packageBase", packageBase);
} else
if (targetPackage != null) {
int ix = targetPackage.lastIndexOf('.');
if (ix > 0) {
ctx.put("packageBase", targetPackage.subSequence(0, ix));
} else {
ctx.put("packageBase", targetPackage);
}
}
ctx.put("targetPackage", targetPackage);
ctx.put("targetFolder", targetFolder);
ctx.put("version", version);
ctx.put("isRi", BaseStructureSpreadsheetParser.determineVersionEnum(version).isRi());
ctx.put("hash", "#");
ctx.put("esc", new EscapeTool());
if (BaseStructureSpreadsheetParser.determineVersionEnum(version).isRi()) {
ctx.put("resourcePackage", "org.hl7.fhir." + version + ".model");
} else {
ctx.put("resourcePackage", "ca.uhn.fhir.model." + version + ".resource");
}
String capitalize = WordUtils.capitalize(version);
if ("Dstu".equals(capitalize)) {
capitalize="Dstu1";
}
ctx.put("versionCapitalized", capitalize);
/*
* Write resources if selected
*/
ResourceGeneratorUsingSpreadsheet rp = context.getResourceGenerator();
if (generateResources && rp != null) {
log("Writing Resources...");
ctx.put("resources", rp.getResources());
v.evaluate(ctx, targetWriter, "", templateReader);
targetWriter.close();
} else {
DatatypeGeneratorUsingSpreadsheet dtp = context.getDatatypeGenerator();
if (generateDatatypes && dtp != null) {
log("Writing DataTypes...");
ctx.put("datatypes", dtp.getResources());
v.evaluate(ctx, targetWriter, "", templateReader);
targetWriter.close();
}
}
/*
* Multiple files.. one for each element
*/
} else {
/*
* Write resources if selected
*/
ResourceGeneratorUsingSpreadsheet rp = context.getResourceGenerator();
if (generateResources && rp != null) {
log("Writing Resources...");
rp.setFilenamePrefix(filenamePrefix);
rp.setFilenameSuffix(filenameSuffix);
rp.setTemplate(template);
rp.setTemplateFile(templateFile);
rp.setVelocityPath(velocityPath);
rp.setVelocityProperties(velocityProperties);
rp.writeAll(targetType, targetDirectory, null, targetPackage);
}
/*
* Write composite datatypes
*/
DatatypeGeneratorUsingSpreadsheet dtp = context.getDatatypeGenerator();
if (generateDatatypes && dtp != null) {
log("Writing Composite Datatypes...");
dtp.setFilenamePrefix(filenamePrefix);
dtp.setFilenameSuffix(filenameSuffix);
dtp.setTemplate(template);
dtp.setTemplateFile(templateFile);
dtp.setVelocityPath(velocityPath);
dtp.setVelocityProperties(velocityProperties);
dtp.writeAll(targetType, targetDirectory, null, targetPackage);
}
/*
* Write valuesets
*/
ValueSetGenerator vsp = context.getValueSetGenerator();
if (generateValueSets && vsp != null) {
log("Writing ValueSet Enums...");
vsp.setFilenamePrefix(filenamePrefix);
vsp.setFilenameSuffix(filenameSuffix);
vsp.setTemplate(template);
vsp.setTemplateFile(templateFile);
vsp.setVelocityPath(velocityPath);
vsp.setVelocityProperties(velocityProperties);
vsp.writeMarkedValueSets(targetType, targetDirectory, targetPackage);
}
/*
* Write profiles
*/
ProfileParser pp = context.getProfileParser();
if (generateProfiles && pp != null) {
log("Writing Profiles...");
pp.setFilenamePrefix(filenamePrefix);
pp.setFilenameSuffix(filenameSuffix);
pp.setTemplate(template);
pp.setTemplateFile(templateFile);
pp.setVelocityPath(velocityPath);
pp.setVelocityProperties(velocityProperties);
pp.writeAll(targetType, targetDirectory, null, targetPackage);
}
}
} catch (Exception e) {
if (e instanceof BuildException) {
throw (BuildException)e;
}
log("Caught exception: "+e.getClass().getName()+" ["+e.getMessage()+"]", 1);
e.printStackTrace();
throw new BuildException("Error processing "+getTaskName()+" task.", e);
} finally {
cleanup();
}
}
protected void validateAttributes () throws BuildException {
if (null == version) {
throw new BuildException("The "+this.getTaskName()+" task requires \"version\" attribute.");
}
if (null == template) {
if (null == templateFile) {
throw new BuildException("The "+this.getTaskName()+" task requires \"template\" or \"templateFile\" attribute.");
}
if (!templateFile.exists()) {
throw new BuildException("The Velocity template file ["+templateFile.getAbsolutePath()+"] does not exist.");
}
if (!templateFile.canRead()) {
throw new BuildException("The Velocity template file ["+templateFile.getAbsolutePath()+"] cannot be read.");
}
if (!templateFile.isFile()) {
throw new BuildException("The Velocity template file ["+templateFile.getAbsolutePath()+"] is not a file.");
}
}
if (null == projectHome) {
throw new BuildException("The "+this.getTaskName()+" task requires \"projectHome\" attribute.");
}
}
protected void cleanup () {
}
public void setVersion(String version) {
this.version = version;
}
public void setProjectHome(String projectHome) {
this.projectHome = projectHome;
}
public void setGenerateResources(boolean generateResources) {
this.generateResources = generateResources;
}
public void setGenerateDatatypes(boolean generateDatatypes) {
this.generateDatatypes = generateDatatypes;
}
public void setGenerateValueSets(boolean generateValueSets) {
this.generateValueSets = generateValueSets;
}
public void setGenerateProfiles(boolean generateProfiles) {
this.generateProfiles = generateProfiles;
}
public void setTargetSourceDirectory(File targetSourceDirectory) {
this.targetSourceDirectory = targetSourceDirectory;
}
public void setTargetPackage(String targetPackage) {
this.targetPackage = targetPackage;
}
public void setPackageBase(String packageBase) {
this.packageBase = packageBase;
}
public void setTargetFile(String targetFile) {
this.targetFile = targetFile;
}
public void setFilenamePrefix(String filenamePrefix) {
this.filenamePrefix = filenamePrefix;
}
public void setFilenameSuffix(String filenameSuffix) {
this.filenameSuffix = filenameSuffix;
}
public void setTargetResourceDirectory(File targetResourceDirectory) {
this.targetResourceDirectory = targetResourceDirectory;
}
public void setTargetFolder(String targetFolder) {
this.targetFolder = targetFolder;
}
public void setTemplate(String template) {
this.template = template;
}
public void setTemplateFile(File templateFile) {
this.templateFile = templateFile;
}
public void setVelocityPath(String velocityPath) {
this.velocityPath = velocityPath;
}
public void setVelocityProperties(String velocityProperties) {
this.velocityProperties = velocityProperties;
}
public void setIncludeResources(String names) {
if (null == this.includeResources) {
this.includeResources = new ArrayList<String>();
}
if (names != null) {
StringTokenizer tokens = new StringTokenizer(names, ", \t\r\n");
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
this.includeResources.add(token.trim());
}
}
}
public void setExcludeResources(String names) {
if (null == this.excludeResources) {
this.excludeResources = new ArrayList<String>();
}
if (names != null) {
StringTokenizer tokens = new StringTokenizer(names, ", \t\r\n");
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
this.excludeResources.add(token.trim());
}
}
}
public void setValueSetFiles(String names) {
if (null == this.valueSetFiles) {
this.valueSetFiles = new ArrayList<ValueSetFileDefinition>();
}
if (names != null) {
StringTokenizer tokens = new StringTokenizer(names, ", \t\r\n");
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
ValueSetFileDefinition def = new ValueSetFileDefinition();
def.setValueSetFile(token.trim());
this.valueSetFiles.add(def);
}
}
}
public void setProfileFiles(String names) {
if (null == this.profileFiles) {
this.profileFiles = new ArrayList<ProfileFileDefinition>();
}
if (names != null) {
StringTokenizer tokens = new StringTokenizer(names, ", \t\r\n");
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
ProfileFileDefinition def = new ProfileFileDefinition();
int ix = token.indexOf('{');
if (ix >= 0) {
int end = token.indexOf('}');
String url = token.substring(ix+1, end);
token = token.substring(end+1);
def.setProfileSourceUrl(url);
}
def.setProfileFile(token.trim());
this.profileFiles.add(def);
}
}
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
class Generator extends AbstractGenerator {
@Override
protected void logInfo(String message) {
log(message);
}
@Override
protected void logDebug(String message) {
if (verbose) {
log(message);
}
}
}
}