Article
· Nov 28, 2023 18m read

Maven Projects - Mixing Java and ObjectScript

Maven Projects mixing ObjectScript and Java

What is Maven?

Maven is a tool, hosted by the Apache Software Foundation, for building and managing any Java-based project. Apache Maven is the best source of information.

Why Maven?

Managing a project, its dependencies, and life cycle can be a tedious task. Maven automates these tasks, making project management much simpler. Maven also defines a standard directory layout and provides tools for initializing projects. These tools employ project templates called maven archetypes. There is a rich library of archetypes available. Maven also provides tools for building your own archetypes.

Maven is one of the most popular Java-based project framework and automation tools in use today. It is well supported by most IDE's. Maven also has a fully featured command line interface (CLI).

Getting Started with Maven

Start here.

Java and IRIS

There are several ways to use Java with IRIS but they all have one thing in common - JDBC. JDBC provides a mechanism for connecting from Java to IRIS. Once connected, there are two primary interfaces that can be used:
* SQL
* IRIS Native

There is also a way for IRIS ObjectScript projects to connect to an External Java Server, interacting with that server through a Dynamic Gateway.

A Simple Maven Project

Let's start with a really simple example. The first step is to make sure you have Java and Maven installed. You will also need a reachable InterSystems IRIS instance and credentials for accessing that instance.

~$ java --version
openjdk 11.0.21 2023-10-17
OpenJDK Runtime Environment Temurin-11.0.21+9 (build 11.0.21+9)
OpenJDK 64-Bit Server VM Temurin-11.0.21+9 (build 11.0.21+9, mixed mode)
~$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: /opt/maven/apache-maven-3.9.4
Java version: 11.0.21, vendor: Eclipse Adoptium, runtime: /usr/lib/jvm/temurin-11-jdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "6.2.0-36-generic", arch: "amd64", family: "unix"
~$ iris session latest
Node: mysystem, Instance: LATEST

Username: 

Rather than writing a full Maven/Java demo here, just reference this page: Maven in Five Minutes. Then come back here to see how to connect your project to InterSystems IRIS and then mix in some ObjectScript - all in one project.

Maven, Java, and IRIS using JDBC

Initialize the Project

First step is to create a new project using skills you acquired from Maven in Five Minutes.

~$ cd projects
~/projects$ mvn archetype:generate -DgroupId=demo.intersystems -DartifactId=sample-iris-mix -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=true
[INFO] Scanning for projects...
<a lot of stuff will be displayed but make sure that ends with this:>
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  26.165 s
[INFO] Finished at: 2023-11-17T06:46:14-07:00
[INFO] ------------------------------------------------------------------------

And then we'll take a look at the project structure of our new project:

~/projects$ tree sample-iris-mix
sample-iris-mix
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── demo
    │           └── intersystems
    │               └── App.java
    └── test
        └── java
            └── demo
                └── intersystems
                    └── AppTest.java

9 directories, 3 files

Without doing anything more, we can build and run this project. Again, this will generate some output, we only care that it ends with success.

~/projects$ cd sample-iris-mix/
~/projects/sample-iris-mix$ mvn clean package
[INFO] Scanning for projects...
< cutting out the extra stuff>
[INFO] Building jar: /home/me/projects/sample-iris-mix/target/sample-iris-mix-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.369 s
[INFO] Finished at: 2023-11-17T06:49:57-07:00
[INFO] ------------------------------------------------------------------------

And we can execute this project:

~/projects/sample-iris-mix$ java -cp ./target/sample-iris-mix-1.0-SNAPSHOT.jar demo.intersystems.App
Hello World!

Adding Dependencies and Making the Jar Executable

Cool. We just did what every other project boostrap tutorial in the world does. Hello? Now let's make this special. The first step is to refactor the project to give a meaningful name to our Java class. Then we'll edit the project definition file - pom.xml - to add a dependency, spice up the project and make it simpler to run. Including only the changes here (also changing the unit test dependency to JUnit 5). To do this, I'll be using a popular IDE - IntelliJ from JetBrains. There is a free community edition although I'm using the Ultimate edition. With the Ultimate edition comes source control integration, a database dashboard and console, and many other useful features. Eclipse and VS Code can also be used. I am also updating the Java version used for this project to be consistent with my installed version (currently using 11)

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.mainClass>demo.intersystems.Mix</project.build.mainClass>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.intersystems</groupId>
      <artifactId>intersystems-jdbc</artifactId>
      <version>3.8.1</version>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.8.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

and, to make the packaged project executable, add this to the build section of the pom:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.3.0</version>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <mainClass>${project.build.mainClass}</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>

Also, since we've updated the dependency from JUnit 4 to JUnit 5, we need to refactor the test class. Simply remove the old imports and replace with new:

package demo.intersystems;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Unit test for simple Mix.
 */
public class MixTest
{
    /**
     * Rigorous Test :-)
     */
    @Test
    public void shouldAnswerWithTrue()
    {
        assertTrue( true );
    }
}

We can now build the project, either from the command line or using our favorite IDE, whatever that may be:

~/projects/sample-iris-mix$ mvn compile
[INFO] Scanning for projects...
[INFO] 
...
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.182 s
[INFO] Finished at: 2023-11-17T07:37:13-07:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project sample-iris-mix: Could not resolve dependencies for project demo.intersystems:sample-iris-mix:jar:1.0-SNAPSHOT: The following artifacts could not be resolved: com.intersystems:intersystems-jdbc:jar:3.8.2 (absent): com.intersystems:intersystems-jdbc:jar:3.8.1 was not found in https://<<removed internal repository link> during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of central has elapsed or updates are forced -> [Help 1]
[ERROR] 

Ooops! This is perhaps the most frustrating part of the Java with IRIS experience. Common artifacts are not available to Maven. Why? Well, Maven resolves dependencies in a few different ways. The most common and most convenient is for a dependency to be available from a public repository such as Maven Central. The above error tells us that the dependency we have listed in our pom.xml cannot be resolved, likely because it is not available in my local repository, in Maven Central or in any other repository I may have defined as part of my project. In this specific case, I'm using a repository internal to InterSystems.

So how can we resolve this dependency without it being available from Maven Central? The simplest solution available to us now, without intersystems-jdbc version 3.8.1 available in any reachable repository, is to install that dependency from a local file. InterSystems Java client jars are all built using Maven and can be installed into your local repository using a simple command:

~$ mvn org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file -Dfile=<<IRISHOME>>/dev/java/lib/1.8/intersystems-jdbc-3.8.1.jar
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO] 
[INFO] --- install:2.5.2:install-file (default-cli) @ standalone-pom ---
[INFO] Installing <<IRISHOME>>/dev/java/lib/1.8/intersystems-jdbc-3.8.1.jar to <<HOME>.m2/repository/com/intersystems/intersystems-jdbc/3.8.1/intersystems-jdbc-3.8.1.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.313 s
[INFO] Finished at: 2023-11-17T08:39:13-07:00
[INFO] ------------------------------------------------------------------------

IRISHOME is the path to your IRIS instance and HOME is the user's home folder. Maven typically maintains the local repository in the user's home folder.

With this dependency now available, our project can now be built successfully:

~/projects/sample-iris-mix$ mvn clean package
[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< demo.intersystems:sample-iris-mix >------------------
[INFO] Building sample-iris-mix 1.0-SNAPSHOT
[INFO]   from pom.xml
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running demo.intersystems.MixTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 s - in demo.intersystems.MixTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- jar:3.3.0:jar (default-jar) @ sample-iris-mix ---
[INFO] Building jar: ~/projects/sample-iris-mix/target/sample-iris-mix-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.335 s
[INFO] Finished at: 2023-11-17T08:46:39-07:00
[INFO] ------------------------------------------------------------------------

We can now run this project more simply:

~/projects/sample-iris-mix$ java -jar ./target/sample-iris-mix-1.0-SNAPSHOT.jar
Hello World!

Now Do Something Useful

We now have a project with a dependency on intersystems-jdbc that builds and executes successfully but it doesn't really do very much. We need to connect to an IRIS Server using JDBC and execute a statement or two. So let's add a resource file named application.properties. This file will go into a new folder in our project under src/main (again - this task can be completed in most IDE's):

~/projects/sample-iris-mix$ cd src/main
~/projects/sample-iris-mix/src/main$ mkdir resources
~/projects/sample-iris-mix/src/main$ touch ./resources/application.properties
~/projects/sample-iris-mix/src/main$ tree
.
├── java
│   └── demo
│       └── intersystems
│           └── Mix.java
└── resources
    └── application.properties

4 directories, 2 files

Adding properties to that resource file that define our default connection coordinates:

iris.server=localhost
iris.port=1972
iris.namespace=user
iris.user=
iris.password=

These properties can be specified on the command line. The reason why we have to include the path to the JDBC driver when we previously installed it into our local maven repository is because Maven is a project framework, dependency resolver, packager, and so on. It is not used at runtime. Any referenced dependencies still have to be in the class path at runtime. Additionally, when a class path is specified on the command line, we cannot execute a jar. Instead, we have to include that jar in the class path and specify the main class. Maven does provide a way to make this simpler but that isn't covered in this document.

~/projects/sample-iris-mix$ java --class-path /opt/intersystems/iris/latest/dev/java/lib/1.8/intersystems-jdbc-3.8.1.jar:<HOME>/projects/sample-iris-mix/target/sample-iris-mix-1.0-SNAPSHOT.jar -Diris.user="_SYSTEM" -Diris.password=SYS demo.intersystems.Mix
+-----------------+-----+-----------+
| name            | age | home_city |
+-----------------+-----+-----------+
| Kennedy, George | 40  | Boston    |
+-----------------+-----+-----------+

Here is the source of the updated Mix class.

package demo.intersystems;

import com.intersystems.jdbc.IRISConnection;
import com.intersystems.jdbc.IRISDataSource;

import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.Set;

public class Mix
{
    public static void main( String[] args ) throws SQLException {
        IRISConnection connection = getConnection();
        createTable(connection);
        populateTable(connection);
        AsciiTable.resultSet(queryTable(connection), 10);
    }

    static void createTable(IRISConnection connection) throws SQLException {
        Statement ddl = connection.createStatement();
        ddl.execute("drop table if exists demo_person");
        ddl.execute("create table demo_person (name varchar(40), age int, home_city varchar(40))");
    }

    static void populateTable(IRISConnection connection) throws SQLException {
        PreparedStatement query = connection.prepareStatement("insert into demo_person(name, age, home_city) values (?, ?, ?)");
        query.setString(1, "Kennedy, George");
        query.setInt(2, 40 );
        query.setString(3, "Boston");
        query.executeUpdate();
    }

    static ResultSet queryTable(IRISConnection connection) throws SQLException {
        Statement query = connection.createStatement();
        return query.executeQuery("select top 10 name, age, home_city from demo_person");
    }

    static IRISConnection getConnection() {
        Properties appProperties = getProperties(Mix.class);
        IRISDataSource dataSource = new IRISDataSource();
        dataSource.setServerName(appProperties.getProperty("iris.server"));
        dataSource.setPortNumber(Integer.parseInt(appProperties.getProperty("iris.port")));
        dataSource.setDatabaseName(appProperties.getProperty("iris.namespace"));
        dataSource.setUser(appProperties.getProperty("iris.user"));
        dataSource.setPassword(appProperties.getProperty("iris.password"));
        try {
            return (IRISConnection) dataSource.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    static Properties getProperties(Class clazz) {
        Properties sysProps = System.getProperties();
        Properties resourceProperties = new Properties();
        try {
            resourceProperties.load(clazz.getClassLoader().getResourceAsStream("application.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        Set<String> propertyNames = resourceProperties.stringPropertyNames();
        for (String name : propertyNames) {
            if (sysProps.containsKey(name)) {
                String propertyValue = sysProps.getProperty(name);
                resourceProperties.replace(name, propertyValue);
            }
        }
        return resourceProperties;
    }
}

Java Unit Tests and Integration Tests

A unit test is a test that is typically run at project build time and does not require any external context in which to run. All of the sources and resources are local to the project. Unit tests also typically complete quickly. Integration tests, on the other hand, likely do require an external context, such as a database server, and can reference external resources and require a specific context in which to run. Integration tests can take longer periods of time to complete and are not run during project build. They are often run manually or prior to project deployment. Maven refers to the life cycle phase where integration tests are run as the "verify" phase.

We won't cover Java unit/integration testing here.

Mixing in ObjectScript with our Java

IRIS Native API for Java

The IRIS Native API is an interface that can be used by Java applications to directly access globals, class methods, and so on. That is well documented in Introduction to the Java Native SDK.

One very useful feature is the ability to invoke methods in ObjectScript classes. The following example is a method added to our Mix class that opens an instance of our demo_person class, yes there is such a class, and displays it on the current device.

    static void getAndLogPerson(IRISConnection connection) throws SQLException {
        IRIS iris = IRIS.createIRIS(connection);
        IRISObject person = (IRISObject) iris.classMethodObject("User.demoperson", "%OpenId", 1);
        System.out.println("\nDirect retrieval of demoperson instance: ");
        System.out.format("\nname: '%s', age: %d, home_city: '%s'\n", person.get("name"), person.get("age"), person.get("homecity"));
    }

and the output from running Mix including this method:

+-----------------+-----+-----------+
| name            | age | home_city |
+-----------------+-----+-----------+
| Kennedy, George | 40  | Boston    |
+-----------------+-----+-----------+

Direct retrieval of demoperson instance: 

name: 'Kennedy, George', age: 40, home_city: 'Boston'

Process finished with exit code 0

The IRIS Native API for Java includes many more features, this feature is presented here solely for demonstrating how Java and ObjectScript code can interact.

Multiple Language Projects

The above example is interesting but it required us to know that there was a class named User.demoperson available to be used on the IRIS Server. How did it get there? In our demo project, we created it using a JDBC DDL statement but we really didn't know the name of the class that it generated. That is generally true, there may be a number of classes that are runnable on our IRIS Server.

Wouldn't it be really nice if we could define a project that included ObjectScript sources along with our Java sources? And would it also be nice if we could include tests written in ObjectScript in our project as well?

Maven and Multiple Languages

You may have noticed that the Maven standard directory layout includes a language subfolder under both src/main and src/test. Maven actually supports multiple language projects but is primarily focused on JDK-based languages. That doesn't mean that Maven ignores other languages. We just have to tell Maven what to do with them.

So let's add some new subfolders and files to our project.. I'll use my IDE (IntelliJ) for this task just because it is simpler.

~/projects/sample-iris-mix/src$ tree
.
├── main
│   ├── java
│   │   └── demo
│   │       └── intersystems
│   │           ├── AsciiTable.java
│   │           └── Mix.java
│   ├── objectscript
│   │   └── demo
│   │       └── intersystems
│   │           └── Person.cls
│   └── resources
│       └── application.properties
└── test
    ├── java
    │   └── demo
    │       └── intersystems
    │           └── MixTest.java
    └── objectscript
        └── demo
            └── intersystems
                └── PersonTest.cls

15 directories, 6 files

ObjectScript in the Project

The ObjectScript classes added to the project, as seen in the above tree output, are exactly what you would expect. They are just classes.

class demo.intersystems.Person extends (%Persistent, %Populate) {

property name as %String(MAXLEN=40);

property age as %Integer(MINVAL=0,MAXVAL=200);

property "home_city" as %String(MAXLEN=40);

classmethod new(name as %String, age as %Integer, homeCity as %String) {
    set person = ..%New()
    set person.name = name
    set person.age = age
    set person."home_city" = homeCity
    set status = person.%Save()
    $$$ThrowOnError(status)
    return person
}
}

A Different Mix

The Mix class, our project's main class, uses JDBC and IRIS Native to interact with an IRIS Server. What if we not only get to define our Java classes but also our ObjectScript sources? Then we don't have to rely on an IRIS Server that has our project code loaded into some namespace. We get to control all of that within our own project. Let's refactor Mix and call it MixedUp:

package demo.intersystems;

import com.intersystems.jdbc.IRIS;
import com.intersystems.jdbc.IRISConnection;
import com.intersystems.jdbc.IRISObject;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class MixedUp {
    public static void main(String[] args) throws SQLException {
        IRISConnection connection = Mix.getConnection();
        IRIS iris = IRIS.createIRIS(connection);
        populateTable(iris);
        AsciiTable.resultSet(queryTable(connection), 10);
        getAndLogPerson(iris);
    }

    static void getAndLogPerson(IRIS iris) throws SQLException {
        IRISObject person = (IRISObject) iris.classMethodObject("demo.intersystems.Person", "%OpenId", 1);
        System.out.println("\nDirect retrieval of Person instance: ");
        System.out.format("\nname: '%s', age: %d, home_city: '%s'\n", person.get("name"), person.get("age"), person.get("home_city"));
    }

    static void populateTable(IRIS iris) {
        iris.classMethodVoid("demo.intersystems.Person", "Populate", 10);
    }

    static ResultSet queryTable(IRISConnection connection) throws SQLException {
        Statement query = connection.createStatement();
        return query.executeQuery("select top 10 name, age, home_city from demo_intersystems.Person");
    }
}

Loading and compiling these ObjectScript sources requires a user to run a command from the IRIS Server. Additionally, the sources must be accessible by the IRIS Server.

Running this with our ObjectScript source loaded and compiled (peek behind the curtain for a cool way to do this, well cool for now!) produces this output:

+--------------------+-----+-----------+
| name               | age | home_city |
+--------------------+-----+-----------+
| Kennedy, Archibald | 96  | Cambridge |
| Kennedy, Archibald | 96  | Cambridge |
| Kennedy, Archibald | 96  | Cambridge |
| Jenkins,Edward A.  | 158 | F1309     |
| Hills,Bill Y.      | 70  | D176      |
| Uberoth,Patrick P. | 101 | Y2440     |
| Nichols,Norbert Y. | 44  | L2678     |
| Basile,Quentin I.  | 46  | J8804     |
| Nagel,Agnes T.     | 15  | V3196     |
| Pascal,Brenda Z.   | 66  | P8988     |
+--------------------+-----+-----------+

Direct retrieval of Person instance: 

name: 'Kennedy, Archibald', age: 96, home_city: 'Cambridge'

Process finished with exit code 0

Sharing our Project

Now suppose I want to package my ObjectScript sources with the jar produced by using Maven's package lifecycle. To do that, I'll need to add some items to the build section of my pom. The following is placed at the top of the build section in our project's pom.xml file.

    <build>
        <resources>
            <resource>
                <directory>src/main</directory>
                <includes>
                    <include>objectscript/**/*</include>
                    <include>resources/**/*</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
        </resources>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <version>3.3.0</version>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            ...

So what is the big deal?

Now that we have Java and ObjectScript in the same project, what can we actually do with the objectscript code? Well, we can compile it. You can run it from our Java application. We can implement ObjectScript unit tests and we can run those unit tests. We have a single project, contained in a folder by itself, that we can develop, test, build and deploy as a single unit.

This project structure is very simple to integrate with your favorite Source Code Management system. It is also a very common structure, allowing for the inclusion of sources from multiple languages, separate main and test sources and resources, etc. With this project structure it is easy to package your project as a jar/war. Extracting, loading, compiling and running the ObjectScript sources is also a simple process although it is a manual process and it does require those sources to be available on the same system as the IRIS Server. In the future I might discuss ways to reduce the manual work needed to manage ObjectScript sources contained in a Maven Project, but this is how far this article goes.

Discussion (0)1
Log in or sign up to continue