Commit be0bf43f authored by Daniel Bluhm's avatar Daniel Bluhm
Browse files

Initial tests for @DataElement


Signed-off-by: Daniel Bluhm's avatarDaniel Bluhm <bluhmdj@ornl.gov>
parent 79729ffe
......@@ -59,6 +59,18 @@
<version>1.18.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package org.eclipse.ice.dev.annotations;
public interface IDataElement {
}
/*******************************************************************************
* Copyright (c) 2020- 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.dev.annotations;
import java.io.Serializable;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* This class provides a simple utility for checking that the data in objects is
* valid and meets some basic expectations. For example, it can be used to check
* that numbers lie within certain bounds or that strings are spelled correctly.
*
* All validation in this class is performed using a Javascript function that is
* injected with the function accessors. All clients are expected to configure a
* validation function by passing a Javascript function in the form of a string
* to the setFunction() operation. The function signature is of the form "var
* checkData = function (data) {return data == 'Solar Fields';}" and this class
* expects to be able to call the checkData() function by name.
*
* Clients should provide functions that, in general, perform both verification
* and validation. That is, functions should verify that data exists within
* expected parameters and insure that the values provided are accurate in a
* larger context and conform to business rules.
*
* Future ideas: 1) Can we take Javascript function objects instead of strings?
* 2) Do we need to create a script engine for *every* validator? Most likely
* not! 3) Can we get feedback from Javascript functions to identify what the
* error was? 4) Can we read functions from files too? 5) Can we inject a
* function name to call instead of defaulting to checkData()?
*
* @author Jay Jay Billings
*
*/
public class JavascriptValidator<T> implements Serializable {
/**
* Logging tool
*/
private static final Logger logger = LoggerFactory.getLogger(JavascriptValidator.class);
/**
* An id for the Serializable interface implementation.
*/
private static final long serialVersionUID = 4748960154143122573L;
/**
* A valid Javascript function, stored as a string, that can be called when the
* validate() operation is executed.
*/
private String function;
/**
* The script engine manager for executing Javascript scripts
*/
@JsonIgnore
ScriptEngineManager scriptEngineManager;
/**
* The Nashorn Javascript engine
*/
@JsonIgnore
ScriptEngine engine;
/**
* Constructor
*/
public JavascriptValidator() {
setFunction(new String());
setupScriptEngine();
}
/**
* Copy constructor
*/
public JavascriptValidator(JavascriptValidator<T> otherValidator) {
if (otherValidator != null) {
function = otherValidator.function;
}
// Still need to setup the scripting engine if we copy it.
setupScriptEngine();
}
/**
* This function sets up the Nashorn scripting engine.
*/
private void setupScriptEngine() {
scriptEngineManager = new ScriptEngineManager();
engine = scriptEngineManager.getEngineByName("JavaScript");
}
/**
* This function returns the Javascript function that will be executed as a
* string.
*
* @return the Javascript function
*/
public String getFunction() {
return function;
}
/**
* This operation sets the validation function from a Javascript function stored
* as a string.
*
* @param function a Javascript function stored as a string that can be called
* by the validate() operation.
*/
public void setFunction(String function) {
this.function = function;
}
/**
* See {@link java.lang.Object#equals(Object)}.
*/
@Override
public boolean equals(Object otherObject) {
boolean retValue = false;
// Check shallow identify and type first
if (this == otherObject) {
retValue = true;
} else if (otherObject instanceof JavascriptValidator<?>) {
JavascriptValidator<T> otherValidator = (JavascriptValidator<T>) otherObject;
retValue = this.function.equals(otherValidator.function);
}
return retValue;
}
/**
* This operation checks the data for validity.
*
* @param data the data to check
* @return true if the data is in a valid state, false otherwise
* @throws NoSuchMethodException This exception is thrown if the Javascript
* validation function cannot be found.
*/
public boolean validate(final T data) throws NoSuchMethodException {
boolean retValue = false;
Object result = null;
try {
engine.eval(function);
Invocable invocableEngine = (Invocable) engine;
result = invocableEngine.invokeFunction("checkData", data);
retValue = (boolean) result;
} catch (ScriptException e) {
logger.error("Error running validation function!", e);
}
return retValue;
}
/**
* See {@link java.lang.Object#hashCode()}.
*/
@Override
public int hashCode() {
// Using a somewhat generic and common technique for computing the hash code
// here. It matches the version in the old ICE 2.x product line, but I
// incremented the initial hash seed to 31 from 11 since this is for version 3.
int hash = 31;
// The 31 below is just coincidental and part of the original source where I
// read
// about hash codes.
hash = 31 * hash + function.hashCode();
return hash;
}
/**
* This operation clones the object. Note that it differs from the base class
* implementation in that it will return null if it cannot create the clone to
* promote fast failure. See {@link java.lang.Object#clone()};
*/
@Override
public Object clone() {
try {
// Call the copy constructor to create the clone.
return new JavascriptValidator<T>(this);
} catch (Exception e) {
logger.error("Unable to clone DataElement!", e);
return null;
}
}
}
......@@ -4,21 +4,28 @@ package $package;
import java.io.Serializable;
import java.util.UUID;
import lombok.Data;
import org.eclipse.ice.renderer.JavascriptValidator;
import org.eclipse.ice.dev.annotations.IDataElement;
import org.eclipse.ice.dev.annotations.JavascriptValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* This is an implementation of $interface that satisfies the dependencies of
* the @DataElement Annotation and was auto-generated by the ICE Framework.
*/
@Data public class ${class} implements ${interface}, Serializable {
@Data
@NoArgsConstructor
public class ${class} implements ${interface}, Serializable, IDataElement {
/**
* Logging tool
......@@ -28,7 +35,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A unique private id that identifies the data element
*/
private UUID privateId;
private UUID privateId = UUID.randomUUID();
/**
* A simple name for the data
......@@ -43,7 +50,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A unique identifier
*/
private String id = "0";
private Long id = 0L;
/**
* A comment that annotates the data in meaningful way
......@@ -74,9 +81,24 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Generated from DataField annotations
*/
#foreach($field in $fields)
#foreach($field in $fields)
protected ${field.ClassName} ${field.Name};
#end
#end
public $class($class other) throws Exception {
if (other == null) {
throw (new Exception("$class to copy cannot be null."));
}
if (!(other instanceof $class)) {
throw (new Exception("$class can copy only from other instances of $class."));
}
#foreach($prop in ["name", "description", "comment", "id", "context", "privateId", "validator", "secret", "required"])
this.$prop = other.$prop;
#end
#foreach($field in $fields)
this.${field.Name} = other.${field.Name};
#end
}
/**
* This operation serializes the data element to a string in verified JSON.
......@@ -115,12 +137,17 @@ import com.fasterxml.jackson.databind.ObjectMapper;
JsonNode rootNode = mapper.readTree(jsonDataElement);
// Static Fields
#foreach($prop in ["name", "description", "id", "comment", "context"])
#foreach($prop in ["name", "description", "comment", "context"])
// $prop
JsonNode ${prop}Node = rootNode.get("$prop");
$prop = mapper.treeToValue(${prop}Node, String.class);
#end
// id
JsonNode idNode = rootNode.get("id");
id = mapper.treeToValue(idNode, Long.class);
// Required and secret booleans
JsonNode requiredNode = rootNode.get("required");
required = mapper.treeToValue(requiredNode, Boolean.class);
......@@ -133,7 +160,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
// Validators
JsonNode validatorNode = rootNode.get("validator");
validator = mapper.treeToValue(validatorNode, validator.getClass());
if (rootNode.hasNonNull("validator")) {
validator = mapper.treeToValue(validatorNode, validator.getClass());
} else {
validator = null;
}
// Dynamic Fields
#foreach($field in $fields)
......
package org.eclipse.ice.tests.renderer;
import org.eclipse.ice.dev.annotations.*;
@DataElement
@DataField(fieldName = "testField", fieldType = String.class)
public interface GeneratedDataElement {
}
/*******************************************************************************
* Copyright (c) 2020- 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.renderer;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.ice.dev.annotations.JavascriptValidator;
import org.junit.jupiter.api.Test;
/**
* This class tests the DataElement class. With the exception of testing for
* serialization to a string, it is sufficient to test this class with T=String.
* For testing serialization, multiple types need to be tested.
*
* @author Jay Jay Billings
*
*/
class GeneratedDataElementTest {
/**
* This is a helper function for creating GeneratedDataElementImplementation values that are
* used in most of the tests.
*
* @param data the value that should be stored in the element.
* @return the data
*/
private GeneratedDataElementImplementation getStringElement(final String data) {
GeneratedDataElementImplementation element = new GeneratedDataElementImplementation();
element.setTestField(data);
return element;
}
/**
* Test method for {@link org.eclipse.ice.renderer.DataElement#getProperties()}.
*/
@Test
void testProperties() {
// Use a basic string element for this test since it is looking at content on
// the base class.
GeneratedDataElementImplementation element = getStringElement("Phutureprimitive");
// Now check the getters
assertEquals(element.getName(), "name");
assertEquals(element.getDescription(), "description");
assertEquals(element.getId(), 0);
assertEquals(element.getComment(), "no comment");
assertEquals(element.getContext(), "default");
// Check the boolean property default values
assertEquals(element.isRequired(), false);
assertEquals(element.isSecret(), false);
// Setup new values for checking setters
String name = "rock";
String description = "round garden rock";
long id = 1L;
String comment = "Brown with a pumpkin next to it";
String context = "garden";
boolean required = true;
boolean secret = true;
// Set all the properties
try {
element.setComment(comment);
element.setContext(context);
element.setId(id);
element.setName(name);
element.setDescription(description);
element.setRequired(required);
element.setSecret(secret);
} catch (Exception e) {
// Complain
e.printStackTrace();
fail();
}
// Check the getters
assertEquals(element.getName(), name);
assertEquals(element.getDescription(), description);
assertEquals(element.getId(), id);
assertEquals(element.getComment(), comment);
assertEquals(element.getContext(), context);
// Check the boolean property default values
assertEquals(element.isRequired(), required);
assertEquals(element.isSecret(), secret);
// Make sure that adding validators works superficially
JavascriptValidator<GeneratedDataElementImplementation> validator = new JavascriptValidator<GeneratedDataElementImplementation>();
element.setValidator(validator);
assertEquals(element.getValidator(), validator);
// Make sure that the UUID is not null
assertNotNull(element.getPrivateId());
return;
}
/**
* Test method for {@link org.eclipse.ice.renderer.DataElement#getData()} and
* {@link org.eclipse.ice.renderer.DataElement#setData(java.lang.Object)}.
*/
@Test
void testDataAccessors() {
// Basic intrinsic class is good for this test
GeneratedDataElementImplementation element = getStringElement("Phutureprimitive");
// No properties are configured here. Just want to make sure that the data
// behaves as expected.
// Do the straight check
assertEquals("Phutureprimitive", element.getTestField());
// Make sure that changing the value works round-trip
String data = element.getTestField();
data = "The Glitch Mob";
element.setTestField(data);
assertEquals("The Glitch Mob", element.getTestField());
return;
}
// /**
// * Test method for {@link org.eclipse.ice.renderer.DataElement#getData()} and
// * {@link org.eclipse.ice.renderer.DataElement#setData(java.lang.Object)} when
// * non-intrinsic POJOs are used.
// */
// @Test
// void testDataAccessorsForPOJOs() {
//
// // Use a test POJO for this that has members
// DataElement<TestPOJO> element = new DataElement<TestPOJO>();
// element.setData(new TestPOJO());
//
// // No properties are configured here. Just want to make sure that the data
// // behaves as expected.
//
// // Do the straight check
// TestPOJO pojo = element.getData();
// assertEquals("foo", pojo.getValue());
// assertEquals(2118.0, pojo.getDoubleValue(), 1.0e-15);
//
// // Make sure that changing the value works round-trip
// pojo.setDoubleValue(1234.0);
// pojo.setValue("bar");
// assertEquals("bar", pojo.getValue());
// assertEquals(1234.0, pojo.getDoubleValue(), 1.0e-15);
//
// return;
// }
//
/**
* Test method for {@link org.eclipse.ice.renderer.DataElement#toString()} and
* {@link org.eclipse.ice.renderer.DataElement#toString()} for intrinsic
* classes.
*/
@Test
void testStringSerialization() {
// Basic intrinsic class is good for this test
GeneratedDataElementImplementation element = getStringElement("Major Lazer & La Roux");
element.setSecret(true);
element.setRequired(true);
element.setValidator(new JavascriptValidator<GeneratedDataElementImplementation>());
// Because of the private id changing and being unique, this cannot be checked
// against a reference but can only be checked by inversion.
String output = element.toJSON();
// Change some values then read back in the original to make sure fromString()
// correctly overwrites them.
element.setTestField("Eastern Sun");
System.out.println(output);
GeneratedDataElementImplementation element2 = getStringElement("Emancipator");
element2.setValidator(new JavascriptValidator<GeneratedDataElementImplementation>());
element2.fromJSON(output);
element.fromJSON(output);
assertEquals(element,element2);
return;
}
//
// /**
// * Test method for {@link org.eclipse.ice.renderer.DataElement#toString()} and
// * {@link org.eclipse.ice.renderer.DataElement#toString()} for POJOs.
// */
// @Test
// void testPOJOSerialization() {
//
// // Use a test POJO for this that has members
// DataElement<TestPOJO> element = new DataElement<TestPOJO>();
// element.setData(new TestPOJO());
// element.setSecret(true);
// element.setRequired(true);
// element.setValidator(new JavascriptValidator<TestPOJO>());
//
// // Add a custom property to make sure they are included in serialization
// try {
// element.setProperty("sail", "awolnation");
// } catch (Exception e) {
// // Complain
// e.printStackTrace();
// fail();
// }
//
// // Because of the private id changing and being unique, this cannot be checked
// // against a reference but can only be checked by inversion.
// String output = element.toString();
//
// // Change some values then read back in the original to make sure fromString()
// // correctly overwrites them.
// DataElement<TestPOJO> element2 = new DataElement<TestPOJO>();
// TestPOJO pojo2 = new TestPOJO();
// pojo2.setDoubleValue(1.072);
// element2.setValidator(new JavascriptValidator<TestPOJO>());
// element2.setData(pojo2);
// element2.fromString(output);
//
// assertEquals(element,element2);
//
// return;