Notes on creating an executable JAR

Russell Bateman
March 2021
last update:

It's been years since I last did it, but I created an executable JAR the other day and thought I'd detail how it's done.

The Project

...is done in IntelliJ IDE Ultimate using James Agnew's HAPI FHIR library. The application accepts one or more input files, assumed to be FHIR, analyzes whether XML or JSON, and translates what's inside into the other format.

This necessarily constitutes a trivial validation of the in-coming HL7v4 (FHIR) document in that, if HAPI FHIR can't parse it, it will error out with stack crawls hinting at what's wrong. This does not, however, rise to a formal validation of the FHIR in the document which is accomplished using a richer subset of HAPI FHIR than is shown here.

The code...

See pom.xml and the Bourne shell script. The rest of the code in here is just to make this a complete (or nearly so) example.

Main.java:
package com.windofkeltia.fhir;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

import static java.util.Objects.nonNull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main
{
  private static final Logger logger = LoggerFactory.getLogger( Main.class );

  public static void main( String[] args ) throws IOException
  {
    if( args.length < 1 )
    {
      System.err.println( "No file paths were specified on the command line." );
      return;
    }

    for( String filepath : args )
    {
      String content   = getLinesInFile( filepath );
      String output    = null;
      Path   path      = Paths.get( filepath );
      String parent    = path.getParent().toString();
      String filename  = path.getFileName().toString();
      int    dot       = filename.lastIndexOf( '.' );
      String extension = ( dot > 0 ) ? filename.substring( dot+1 ) : null;
      String basename  = ( nonNull( extension ) )
                            ? filename.substring( 0, filename.length() - extension.length() - 1 )
                            : filename;

      switch( getType( content ) )
      {
        case XML :     output = new XmlToJson().translateToString( content ); basename += ".json"; break;
        case JSON :    output = new JsonToXml().translateToString( content ); basename += ".xml";  break;
        case UNKNOWN : logger.warn( "Unknown file type at " + filepath ); continue;
      }

      String target = parent + '/' + basename;
      logger.info( "Translation will be at " + target );
      File file = new File( target );

      if( file.exists() )
        file.delete();

      file.createNewFile();

      BufferedWriter writer = new BufferedWriter( new FileWriter( file ) );
      writer.write( output );
      writer.close();
    }
  }

  public static String getLinesInFile( String pathname ) throws IOException
  {
    StringBuilder sb = new StringBuilder();

    try ( BufferedReader br = new BufferedReader( new FileReader( pathname ) ) )
    {
      for( String line = br.readLine(); line != null; line = br.readLine() )
        sb.append( line ).append( '\n' );
    }

    return sb.toString();
  }

  private static Type getType( final String content )
  {
    switch( content.charAt( 0 ) )
    {
      case '<' : return Type.XML;
      case '{' : return Type.JSON;
      default :  return Type.UNKNOWN;
    }
  }

  private enum Type { XML, JSON, UNKNOWN }
}
Globals.java:
package com.windofkeltia.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;

public class Globals
{
  public static final FhirContext context = FhirContext.forR4();
  public static final IParser     jParser = context.newJsonParser();
  public static final IParser     xParser = context.newXmlParser();
}
JsonToXml.java:
package com.windofkeltia.fhir;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JsonToXml
{
  private static Logger logger = LoggerFactory.getLogger( JsonToXml.class );

  public String translateToString( final String content )
  {
    logger.trace( "JsonToXml.translateToString()..." );
    return Globals.xParser.setPrettyPrint( true )
                     .encodeResourceToString( Globals.jParser.parseResource( content ) );
  }
}
XmlToJson.java:
package com.windofkeltia.fhir;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class XmlToJson
{
  private static Logger logger = LoggerFactory.getLogger( XmlToJson.class );

  public String translateToString( final String content )
  {
    logger.trace( "XmlToJson.translateToString()..." );
    return Globals.jParser.setPrettyPrint( true )
                     .encodeResourceToString( Globals.xParser.parseResource( content ) );
  }
}
MainTest.java:
package com.windofkeltia.fhir;

import java.io.IOException;
import org.junit.Test;

public class MainTest
{
  @Test
  public void testXmlToJson() throws IOException
  {
    final String[] PATHS = { "src/test/resources/fodder/xml-samples/xml-test.xml" };
    Main.main( PATHS );
  }

  @Test
  public void testJsonToXml() throws IOException
  {
    final String[] PATHS = { "src/test/resources/fodder/json-samples/json-test.json" };
    Main.main( PATHS );
  }

  @Test
  public void testBoth() throws IOException
  {
    final String[] PATHS = { "src/test/resources/fodder/xml-samples/xml-test.xml",
                       "src/test/resources/fodder/json-samples/json-test.json"
                     };
    Main.main( PATHS );
  }
}
pom.xml

The part that is the executable JAR is build using what's in lines 102-124.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                      http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.windofkeltia</groupId>
  <artifactId>fhir-json-xml</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <name>FHIR JSON to XML or XML to JSON</name>

  <properties>
    <hapi-fhir.version>5.1.0</hapi-fhir.version>
    <woodstox-version>4.2.0</woodstox-version>
    <slf4j.version>1.7.30</slf4j.version>
    <logback-version>1.2.3</logback-version>
    <junit.version>4.12</junit.version>
    <maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
    <versions-maven-plugin.version>2.7</versions-maven-plugin.version>
    <maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>
    <maven-antrun-plugin.version>1.8</maven-antrun-plugin.version>
    <maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
    <additionalJOption>-Xdoclint:none</additionalJOption>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.release>9</maven.compiler.release>
    <maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
    <maven.install.skip>true</maven.install.skip>
    <maven.deploy.skip>true</maven.deploy.skip>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-base</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-structures-r5</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-structures-r4</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-validation-resources-r4</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <dependency>
      <groupId>ca.uhn.hapi.fhir</groupId>
      <artifactId>hapi-fhir-validation</artifactId>
      <version>${hapi-fhir.version}</version>
    </dependency>
    <dependency>
      <groupId>org.codehaus.woodstox</groupId>
      <artifactId>woodstox-core-asl</artifactId>
      <version>${woodstox-version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback-version}</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven.compiler.plugin.version}</version>
        <configuration>
          <release>8</release>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>${maven-assembly-plugin.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <archive>
                <manifest>
                  <mainClass>com.windofkeltia.fhir.Main</mainClass>
                </manifest>
              </archive>
              <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
              </descriptorRefs>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

fhir-json-xml.sh:

...and a script to run it. Put the JAR right in the same subdirectory as the script.

#!/bin/sh
if [ "$1" = "--help" -o "$1" = "-help" -o "$1" = "-h" ]; then
  echo "Specify one or more input files in FHIR, whether JSON or XML, and these will
be translated to the implied "other" format (XML or JSON) under the same name
but changed extension (.xml or .json).

Environment variable JAVA_HOME must be set up in order to use this utility.

$0 file [files]"
  exit 0
fi

if [ -z "$JAVA_HOME" ]; then
  echo "Environment variable JAVA_HOME must be set up in order to use this utility."
  exit 1
fi


$JAVA_HOME/bin/java -jar fhir-json-xml-1.0.0-jar-with-dependencies.jar $*