diff --git a/org.eclipse.ice.bats/.gitignore b/org.eclipse.ice.bats/.gitignore new file mode 100644 index 000000000..9291963e0 --- /dev/null +++ b/org.eclipse.ice.bats/.gitignore @@ -0,0 +1,98 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/java,eclipse +# Edit at https://www.toptal.com/developers/gitignore?templates=java,eclipse + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# End of https://www.toptal.com/developers/gitignore/api/java,eclipse + +target diff --git a/org.eclipse.ice.bats/README.md b/org.eclipse.ice.bats/README.md new file mode 100644 index 000000000..588f95340 --- /dev/null +++ b/org.eclipse.ice.bats/README.md @@ -0,0 +1,122 @@ +# BATS + +BATS, the Basic Artifact Tracking System (BATS), is a simple data management service for managing scientific data. + +BATS is a standalone maven package that can be used within or outside of ICE. + +BATS leverages [Apache Jena](https://jena.apache.org/index.html) to create [RDF Models](https://jena.apache.org/tutorials/rdf_api.html) and connects to [Apache Jena Fuseki](https://jena.apache.org/documentation/fuseki2/index.html) to publish these models. + + + +## Build Instructions + +### Prerequisites + +BATS requires a full installation of Docker for building, executing, and storing images. + + +### Using Maven + +Building the package can be performed like most other maven packages, where the end result is a jar file that can be included as a dependency +Due to the integration tests requiring a [Apache Jene Fuseki](https://jena.apache.org/documentation/fuseki2/index.html) server, +we need to first build the image using the Dockerfile located in `src/main/docker/Dockerfile`. +A test Fuseki container will be started and stopped during the tests using the [fabric8io docker-maven-plugin](https://dmp.fabric8.io/) +(GitHub repo link [here](https://github.com/fabric8io/docker-maven-plugin) + +``` +$ mvn clean docker:build install +``` + +This installs the jar file to the local repository in `~/.m2`. It is also possible to build the package without installing by running + +``` +$ mvn clean docker:build verify +``` + +In both cases one can skip the tests by including `-DskipTests` in your build. + +## BATS API + +Below are examples of a few general use cases for the BATS API + +### Upload a new DataSet + +One can create a connection to a Fuseki server to upload a [Dataset](https://jena.apache.org/documentation/javadoc/arq/org/apache/jena/query/Dataset.html), or a collection of named graphs (called [Models](https://jena.apache.org/documentation/javadoc/jena/org/apache/jena/rdf/model/Model.html) in Apache Jena). + +Here we upload a new, empty Dataset called `my-new-dataset` +to our Fuseki server `http://my-fuseki-server.org:3030`. + +The upload takes place when we issue the `.create()` method +for "creating" the data on the server. + +``` +import org.eclipse.ice.bats.DataSet; + +DataSet dataset = new DataSet(); +dataset.setName("my-new-dataset"); +dataset.setHost("http://my-fuseki-server.org"); +dataset.setPort(3030); +dataset.create(); +``` + +### Upload a new RDF Model to a DataSet + +We can also add a RDF [Model](https://jena.apache.org/documentation/javadoc/jena/org/apache/jena/rdf/model/Model.html) in Apache Jena, +to the Dataset on the Fuseki server as follows: + +``` +import org.eclipse.ice.bats.DataSet; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +DataSet dataset = new DataSet(); +dataset.setName("my-new-dataset"); +dataset.setHost("http://my-fuseki-server.org"); +dataset.setPort(3030); + +Model model = ModelFactory.createDefaultModel(); +dataset.updateModel("my-model", model); +``` + +### Pull a RDF Model from a DataSet + +Following the previous example for uploading a RDF Model, +we can pull that same model using the following: + +``` +import org.eclipse.ice.bats.DataSet; +import org.apache.jena.rdf.model.Model; + +DataSet dataset = new DataSet(); +dataset.setName("my-new-dataset"); +dataset.setHost("http://my-fuseki-server.org"); +dataset.setPort(3030); + +Model model = dataset.getModel("my-model"); +``` + +### Get "raw" Jena Dataset + +Sometimes we require the "raw" Jena dataset. +We can use the following to pull this dataset and check if it is "null" + +``` +import org.eclipse.ice.bats.DataSet; +import org.apache.jena.query.Dataset; + +DataSet dataset = new DataSet(); +dataset.setName("my-new-dataset"); +dataset.setHost("http://my-fuseki-server.org"); +dataset.setPort(3030); + +Dataset rawDataset = dataset.getJenaDataset(); +if ( rawDataset == null ) { + throw new Exception("DataSet Not Found"); +} +``` + +## How BATS got its name + +Jay Jay Bilings had a discussion with his daughter, 17 months old at the time, about her favorite animal. +She picked the moose, but since that is already taken by several projects, they settled on her second favorite animal, the bat. +The name was then back-ronymed out of it. diff --git a/org.eclipse.ice.bats/pom.xml b/org.eclipse.ice.bats/pom.xml new file mode 100644 index 000000000..dad6c9826 --- /dev/null +++ b/org.eclipse.ice.bats/pom.xml @@ -0,0 +1,115 @@ + + 4.0.0 + org.eclipse.ice + org.eclipse.ice.bats + 0.0.1-SNAPSHOT + Eclipse ICE BATS + Basic Artifact Tracking System + + + 11 + 11 + 11 + UTF-8 + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.1 + + + + integration-test + verify + + + + + + + io.fabric8 + docker-maven-plugin + 0.34.1 + + + + bats-fuseki:latest + + ${project.basedir}/src/main/docker + Dockerfile.fuseki + + + + 3030:3030 + + + + /opt/fuseki-TDB:/data/TDB + + + + host + + + http://localhost:3030 + + + + + + + + prepare-fuseki-database + pre-integration-test + + start + + + + remove-fuseki-database + post-integration-test + + stop + + + + + + + + + + + junit + junit + 4.12 + test + + + + org.apache.jena + apache-jena-libs + pom + 3.7.0 + + + + org.topbraid + shacl + 1.1.0 + + + + + org.apache.logging.log4j + log4j-core + 2.11.2 + + + + diff --git a/org.eclipse.ice.bats/src/main/docker/Dockerfile.fuseki b/org.eclipse.ice.bats/src/main/docker/Dockerfile.fuseki new file mode 100644 index 000000000..07c988e60 --- /dev/null +++ b/org.eclipse.ice.bats/src/main/docker/Dockerfile.fuseki @@ -0,0 +1,20 @@ +FROM openjdk:11.0.1-jre + +# Specify Fuseki variables +ARG FUSEKI_VERSION=3.16.0 +ARG FUSEKI_NAME=apache-jena-fuseki +ARG FUSEKI_DOWNLOAD_FILE=$FUSEKI_NAME-$FUSEKI_VERSION.tar.gz + +# Install Fuseki +RUN wget https://www-us.apache.org/dist/jena/binaries/$FUSEKI_DOWNLOAD_FILE && \ + tar -xzvf $FUSEKI_DOWNLOAD_FILE && \ + mv $FUSEKI_NAME-$FUSEKI_VERSION /opt/$FUSEKI_NAME && \ + mkdir -p /opt/$FUSEKI_NAME/run/configuration + +# Expose the Fuseki port +EXPOSE 3030 + +# Execute Fuseki from the installation directory +WORKDIR /opt/apache-jena-fuseki +ENTRYPOINT ["./fuseki-server"] + diff --git a/org.eclipse.ice.bats/src/main/java/gov/ornl/rse/bats/DataSet.java b/org.eclipse.ice.bats/src/main/java/gov/ornl/rse/bats/DataSet.java new file mode 100644 index 000000000..6d5c1017f --- /dev/null +++ b/org.eclipse.ice.bats/src/main/java/gov/ornl/rse/bats/DataSet.java @@ -0,0 +1,290 @@ +/****************************************************************************** + * Copyright (c) 2019- UT-Battelle, LLC. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0, + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Initial API and implementation and/or initial documentation - + * Jay Jay Billings + *****************************************************************************/ +package org.eclipse.ice.bats; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.apache.http.Consts; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdfconnection.RDFConnectionFuseki; +import org.apache.jena.rdfconnection.RDFConnectionRemoteBuilder; +import org.apache.jena.riot.Lang; +import org.apache.jena.update.Update; +import org.apache.jena.util.FileManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Logger; + +/** + * This class represents a set of data describing a topic or item of interest. + * In BATS, data sets are natively distributed across one or more servers. The + * initial hostname and port of an Apache Jena Fuseki server must be provided in + * order to pull the root RDF model that describes this data set, as well as an + * associated metadata models. + * + * Data sets are organized in the Apache Jena style, with each set containing + * one or more subsets called "Models." This class largely wraps those + * operations into a more convenient interface that masks Jena's HTTP-based + * transfer routines, and fits the intended use better. However, advanced users + * may retrieve the Jena dataset by calling getJenaDataset(). + * + * By default, DataSet only create Jena TDB2 persistent triple stores on the + * remote server for RDF models. Instances do not hold copies or handles to any + * data that they represent because the size of the data is not known in + * advance. Instead, this class interacts directly with the remote triple store. + * + * @author Jay Jay Billings + * + */ +public class DataSet { + + /** + * This is the default name used as the base for all unnamed instances of + * DataSet. + */ + public static final String DEFAULT_NAME = "unnamed-dataset"; + + /** + * Log utility + */ + protected static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(DataSet.class.getName()); + + /** + * The default host which holds the dataset. + */ + private String host = "http://localhost"; + + /** + * The default port of the host which holds the dataset. + */ + private int port = 3030; + + /** + * The default name for a dataset. + */ + private String name = DEFAULT_NAME; + + /** + * This operation sets the name of the data set. The name of the data set is the + * name recognized by the host, not the local machine. It must be set prior to + * calling create() or load(), but calling it after those operations does not + * change it. + * + * @param name + */ + public void setName(final String name) { + this.name = name; + } + + /** + * This operation returns the name of the data set. + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * This operation returns the host of the data set. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * This operation sets the host at which the data set should be created or from + * which it should be loaded. + * + * @param host the URI of the remote Fuseki host that hosts the data set + */ + public void setHost(final String host) { + this.host = host; + } + + /** + * This operation returns the port of the host of this data set. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * This operation sets the expected port of the host of this data set. + * + * @param port + */ + public void setPort(final int port) { + this.port = port; + } + + /** + * This operation returns the full URI identifying this data set on the remote + * server, including hostname, port, and set name. + * + * @return the full URI including all parts + */ + public String getFullURI() { + return getHost() + ":" + getPort() + "/" + getName(); + } + + /** + * This operation creates a dataset with the given name. If no name is provided + * to setName(), the default name with a UUID appended to it will be used such + * that the form of the name will be "unnamed-dataset_." Note that + * creation does not imply retrieval, and that the getRootModel() or getModel() + * functions still need to be called. Likewise (and obviously), if the model + * already exists on the remote server it can just be retrieved without calling + * create(). + * + * @throws Exception this exception is thrown if the data set cannot be created + * for any reason. + */ + public void create() throws Exception { + + // Configure the name + String dbName = DEFAULT_NAME; + if (name == DEFAULT_NAME) { + name += "_" + UUID.randomUUID().toString(); + } + dbName = name; + // Per the spec, always use tdb2. + String dbType = "tdb2"; + + // Connect the HTTP client + HttpClient client = HttpClientBuilder.create().build(); + String fusekiLocation = host + ":" + port + "/"; + String fusekiDataAPILoc = "$/datasets"; + HttpPost post = new HttpPost((fusekiLocation + fusekiDataAPILoc)); + + // Add the database parameters into the form with UTF_8 encoding. + List form = new ArrayList(); + form.add(new BasicNameValuePair("dbName", dbName)); + form.add(new BasicNameValuePair("dbType", dbType)); + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(form, Consts.UTF_8); + + // Create the data set + post.setEntity(formEntity); + HttpResponse response = client.execute(post); + logger.debug(response.toString()); + + return; + } + + /** + * This operation directs the data set to update and persist any remotely stored + * versions of this model with this version of the model. This action is a + * complete re-write of the data, with out a merge or any checks. + * + * @param modelName the name of the model that will be updated + * @param model the model that will be updated remotely + */ + public void updateModel(final String modelName, Model model) { + + RDFConnectionRemoteBuilder uploadConnBuilder = RDFConnectionFuseki.create() + .destination(getFullURI() + "/data"); + + // Open a connection to upload the ICE ontology. + try (RDFConnectionFuseki uploadConn = (RDFConnectionFuseki) uploadConnBuilder.build()) { + // Note that transactions must proceed with begin(), some operation(), and + // commit(). + uploadConn.begin(ReadWrite.WRITE); + System.out.println(model.toString()); +// uploadConn.load(modelName, model); + uploadConn.put(modelName, model); + uploadConn.commit(); + logger.debug("Committed model " + modelName + " to data set" + getName()); + } catch (Exception e) { + logger.error("Unable to update model " + modelName + " in data set " + getName() + + " on the remote Fuseki server.", e); + } + } + + /** + * This operation returns the root model in the data set, which is called the + * default graph in the Jena jargon. It is referred to as the root model here to + * denote that it is the root model in a hierarchy of models describing the same + * set. This is a convenience method identically equal to calling getModel(null) + * or getModel("default"). + * + * @return the root model if the data set exists, otherwise null + */ + public Model getRootModel() { + return getModel(null); + } + + /** + * This operation returns the model with the given name if it exists in the data + * set. + * + * @param modelName the name of the model that should be retrieved from the data + * set. Note that like Jena, calling with an argument of + * "default" or "null" will return the default graph/model. + * @return the model if it exists in the data set, otherwise null + */ + public Model getModel(final String modelName) { + Model model = null; + RDFConnectionRemoteBuilder getConnBuilder = RDFConnectionFuseki.create() + .destination(getFullURI() + "/data"); + + try (RDFConnectionFuseki getConn = (RDFConnectionFuseki) getConnBuilder.build()) { + getConn.begin(ReadWrite.READ); + model = getConn.fetch(modelName); + getConn.commit(); + logger.debug("Retrieved model " + modelName + " from data set" + getName()); + } catch (Exception e) { + logger.error("Unable to find model " + modelName + " in data set " + getName(), e); + } + + return model; + } + + /** + * This operation returns the raw Jena data set pulled from Fuseki. This could + * be a long-running operation depending on the size of the remote data. This + * operation is intended purely as a convenience to advanced users who want to + * manipulate the data set directly. + * + * @return the raw Jena data set + */ + public Dataset getJenaDataset() { + Dataset set = null; + RDFConnectionRemoteBuilder getConnBuilder = RDFConnectionFuseki.create() + .destination(getFullURI() + "/get"); + + try (RDFConnectionFuseki getConn = (RDFConnectionFuseki) getConnBuilder.build()) { + getConn.begin(ReadWrite.READ); + set = getConn.fetchDataset(); + getConn.commit(); + logger.debug("Retrieved data set" + getName()); + } catch (Exception e) { + logger.error("Unable to find data set " + getName(), e); + } + + return set; + } + +} diff --git a/org.eclipse.ice.bats/src/test/java/gov/ornl/rse/tests/bats/ITDataSet.java b/org.eclipse.ice.bats/src/test/java/gov/ornl/rse/tests/bats/ITDataSet.java new file mode 100644 index 000000000..f3045ec36 --- /dev/null +++ b/org.eclipse.ice.bats/src/test/java/gov/ornl/rse/tests/bats/ITDataSet.java @@ -0,0 +1,192 @@ +/****************************************************************************** + * Copyright (c) 2019- UT-Battelle, LLC. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0, + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Initial API and implementation and/or initial documentation - + * Jay Jay Billings + *****************************************************************************/ +package org.eclipse.ice.tests.bats; + +import static org.junit.Assert.*; + +import java.util.UUID; + +import org.apache.jena.query.Dataset; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdfconnection.RDFConnectionFuseki; +import org.apache.jena.rdfconnection.RDFConnectionRemoteBuilder; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.eclipse.ice.bats.DataSet; + +/** + * This is a simple test of the BATS Dataset class. It requires that the Fuseki + * is running locally on port 3030. The simplest way to do this is to execute + * the docker-compose.yml file that is in the directory root BATS directory. + * + * @author Jay Jay Billings + * + */ +public class ITDataSet { + + /** + * @throws java.lang.Exception + */ + @BeforeClass + public static void setUpBeforeClass() throws Exception { + } + + /** + * This is a utility operation for checking if data sets correctly created + * themselves on the remote server. + * + * @param dataSet the dataset to check + */ + private void checkDataSetCreationOnServer(final DataSet dataSet) { + // Create the dataset + try { + dataSet.create(); + } catch (Exception e) { + // Complain + e.printStackTrace(); + fail(); + } + + // Grab the dataset directy from the server + String name = dataSet.getName(); + String fusekiURI = dataSet.getHost() + ":" + dataSet.getPort() + "/" + name; + String fusekiGetURI = fusekiURI + "/get"; + RDFConnectionRemoteBuilder getConnBuilder = RDFConnectionFuseki.create().destination(fusekiGetURI); + try (RDFConnectionFuseki getConn = (RDFConnectionFuseki) getConnBuilder.build()) { + System.out.println("Pulling " + dataSet.getName()); + getConn.begin(ReadWrite.READ); + Model model = getConn.fetch(null); + getConn.commit(); + + // The only real check that exists is whether or not the exception is caught. + + } catch (Exception e) { + e.printStackTrace(); + fail("Data set not found!"); + } + } + + /** + * This operation checks data set creation. + */ + @Test + public void testCreate() { + + // Create a default, empty data set with the default name + DataSet dataSet = new DataSet(); + // Check the data set creation + checkDataSetCreationOnServer(dataSet); + + // Configure the name and some other details of a dataset and test that + // functionality + DataSet dataSet2 = new DataSet(); + String uuidString = UUID.randomUUID().toString(); + String name = "dataSetTest" + "." + uuidString; + dataSet2.setName(name); + dataSet2.setHost("http://127.0.0.1"); + dataSet2.setPort(5); + // Make sure these work OK + assertEquals(name, dataSet2.getName()); + assertEquals("http://127.0.0.1", dataSet2.getHost()); + // Just check that the port is set properly since actually testing a port switch + // is too onerous + assertEquals(5, dataSet2.getPort()); + // Reset the port to avoid an error since it has been proven that it could be + // stored correctly. + dataSet2.setPort(3030); + + // Check creating the dataset on the server with its custom args + checkDataSetCreationOnServer(dataSet2); + + return; + } + + /** + * This operation tries to pull some models from the data set + */ + @Test + public void testModels() { + // Create a new data set + DataSet dataSet = new DataSet(); + checkDataSetCreationOnServer(dataSet); + + // Put something in it + Model model = ModelFactory.createDefaultModel(); + Resource resource = model.createResource("testModelResource"); + Property property = model.createProperty("none", "g"); + resource.addProperty(property, "testProp"); + + // Update the data set + dataSet.updateModel("testModel", model); + + // Check the root/default model + Model rootModel = dataSet.getRootModel(); + assertNotNull(rootModel); + + // Check the named model + Model namedModel = dataSet.getModel("testModel"); + assertNotNull(namedModel); + // Make sure that the model matches the original model by doing a difference and + // checking the number of statements in the difference model. + Model differenceModel = namedModel.difference(model); + assertFalse(differenceModel.listStatements().hasNext()); + + // Try putting the model a second time to make sure that it doesn't get + // duplicated. + dataSet.updateModel("testModel", model); + // Make sure the number of triples didn't change with this update. + Model namedModel2 = dataSet.getModel("testModel"); + Model differenceModel2 = namedModel2.difference(model); + assertFalse(differenceModel2.listStatements().hasNext()); + + return; + } + + /** + * This operation checks loading a pre-existing data set. + */ + @Test + public void testJenaDataSetLoad() { + + // Create a new data set + DataSet referenceDataSet = new DataSet(); + checkDataSetCreationOnServer(referenceDataSet); + + // Put something in it + Model model = ModelFactory.createDefaultModel(); + Resource resource = model.createResource("testModelResource"); + Property property = model.createProperty("none", "h"); + resource.addProperty(property, "testProp"); + + // Upload it to the server + referenceDataSet.updateModel("testModel", model); + + // Load the contents from the server into a new, empty data set + DataSet loadedSet = new DataSet(); + loadedSet.setHost(referenceDataSet.getHost()); + loadedSet.setPort(referenceDataSet.getPort()); + loadedSet.setName(referenceDataSet.getName()); + Dataset jenaDataset = loadedSet.getJenaDataset(); + + // Check something! + assertEquals(referenceDataSet.getJenaDataset().getDefaultModel().toString(), + jenaDataset.getDefaultModel().toString()); + + return; + } + +} diff --git a/org.eclipse.ice.bats/src/test/resources/log4j.properties b/org.eclipse.ice.bats/src/test/resources/log4j.properties new file mode 100644 index 000000000..06538abca --- /dev/null +++ b/org.eclipse.ice.bats/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +#log4j.rootCategory=INFO,DEBUG,A1,LFS +log4j.rootLogger=DEBUG, CA + +#Set Console Appender +log4j.appender.CA=org.apache.log4j.ConsoleAppender +#CA uses PatternLayout +log4j.appender.CA.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%m%n \ No newline at end of file diff --git a/org.eclipse.ice.build/pom.xml b/org.eclipse.ice.build/pom.xml index 65439f998..3b119cb95 100644 --- a/org.eclipse.ice.build/pom.xml +++ b/org.eclipse.ice.build/pom.xml @@ -21,5 +21,6 @@ ../org.eclipse.ice.dev ../org.eclipse.ice.archetypes ../org.eclipse.ice.commands + ../org.eclipse.ice.bats