diff --git a/pom.xml b/pom.xml index f5a43b6..ef3e04a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,10 +23,28 @@ commons-lang3 3.4 + + org.slf4j + slf4j-api + 1.6.1 + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.12.0 + test + - src + src/main/java + src/test/java maven-compiler-plugin diff --git a/src/main/java/VTMain.java b/src/main/java/VTMain.java deleted file mode 100644 index 0d8695c..0000000 --- a/src/main/java/VTMain.java +++ /dev/null @@ -1,87 +0,0 @@ -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.opensaber.views.Transformer; -import io.opensaber.views.ViewTemplate; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class VTMain { - - public static void personExample() { - try { - String personJson = "{\"Person\": " + - " {\"nationalIdentifier\":\"nid823\"," + - " \"firstName\":\"Ram\"," + - " \"lastName\":\"Moorthy\"," + - " \"gender\":\"MALE\"," + - " \"dob\":\"1990-12-10\"}}"; - ObjectNode personNode = (ObjectNode) new ObjectMapper().readTree(personJson); - - // read from the ViewTemplate - String viewTemplateJson = readFileContent("personVT1.json"); - ViewTemplate viewTemplate = new ObjectMapper().readValue(viewTemplateJson, ViewTemplate.class); - - // transform action - System.out.println("Person record " + personNode); - System.out.println("Person from view template" + new Transformer().transform(viewTemplate, personNode)); - - } catch (IOException e) { - e.printStackTrace(); - } - } - - public static void simpleMathExample() { - try { - String mathProblem = "{\"Math\": " + - " {\"a\": 5," + - " \"b\": 2 }}"; - ObjectNode personNode = (ObjectNode) new ObjectMapper().readTree(mathProblem); - - // read from the ViewTemplate - String viewTemplateJson = readFileContent("mathVT1.json"); - ViewTemplate viewTemplate = new ObjectMapper().readValue(viewTemplateJson, ViewTemplate.class); - - // transform action - System.out.println("Maths record " + mathProblem); - System.out.println("Maths from view templates " + new Transformer().transform(viewTemplate, personNode)); - - } catch (IOException e) { - e.printStackTrace(); - } - } - - public static void main(String[] args) { - System.out.println("**************************"); - personExample(); - System.out.println("**************************"); - simpleMathExample(); - System.out.println("**************************"); - } - - private static String readFileContent(String fileName) { - InputStream in; - try { - in = VTMain.class.getClassLoader().getResourceAsStream(fileName); - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = in.read(buffer)) != -1) { - result.write(buffer, 0, length); - } - return result.toString(StandardCharsets.UTF_8.name()); - - } catch (FileNotFoundException e1) { - e1.printStackTrace(); - - } catch (IOException e) { - e.printStackTrace(); - - } - return null; - } - -} diff --git a/src/main/java/io/opensaber/provider/SampleViewFunctionProvider.java b/src/main/java/io/opensaber/provider/SampleViewFunctionProvider.java new file mode 100644 index 0000000..896a0d1 --- /dev/null +++ b/src/main/java/io/opensaber/provider/SampleViewFunctionProvider.java @@ -0,0 +1,31 @@ +package io.opensaber.provider; + +import io.opensaber.views.IViewFunctionProvider; +import java.util.List; +/** + * This class is a sample implementation class of IViewFunctionProvider + * + */ +public class SampleViewFunctionProvider implements IViewFunctionProvider { + + @Override + public String doAction(List values) { + // doing a simple concat for the values + return concat(values); + } + + /** + * simple concat for the values as string and comma(',') as seperator + * + * @param args + * @return + */ + public String concat(List args) { + String res = ""; + for (Object arg : args) { + res = res.toString().isEmpty() ? arg.toString() : (res + ", " + arg.toString()); + } + return res; + } + +} diff --git a/src/main/java/io/opensaber/views/EvaluatorFactory.java b/src/main/java/io/opensaber/views/EvaluatorFactory.java new file mode 100644 index 0000000..9be3e60 --- /dev/null +++ b/src/main/java/io/opensaber/views/EvaluatorFactory.java @@ -0,0 +1,34 @@ +package io.opensaber.views; + +public class EvaluatorFactory { + + public static final String concat = "concat"; + public static final String userDefinedConcat = "userDefinedConcat"; + public static final String customDefinedConcat = "customDefinedConcat"; + /** + * returns the instance of IEvaluator implementations (like:FunctionEvaluator, ProviderEvaluator + * @param functionName + * @param function + * @return + */ + public static IEvaluator getInstance(String functionName, FieldFunction function){ + IEvaluator evaluator = null; + switch(functionName){ + + case concat : + evaluator = new FunctionEvaluator(function); + break; + case userDefinedConcat : + evaluator = new ProviderEvaluator(function); + break; + case customDefinedConcat : + break; + default : + evaluator = new FunctionEvaluator(function); + break; + } + + return evaluator; + } + +} diff --git a/src/main/java/io/opensaber/views/Field.java b/src/main/java/io/opensaber/views/Field.java index 01c4fc9..b14ffa0 100644 --- a/src/main/java/io/opensaber/views/Field.java +++ b/src/main/java/io/opensaber/views/Field.java @@ -1,9 +1,13 @@ package io.opensaber.views; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @JsonIgnoreProperties(ignoreUnknown = true) public class Field { + private static Logger logger = LoggerFactory.getLogger(Field.class); private String name; private String title; @@ -63,5 +67,26 @@ public String getTitle() { return this.title; } } + /** + * parse function to get the function name + * + * @return function name(like: concat) + */ + public String getFunctioName(){ + String fdName = StringUtils.substring(this.function, this.function.lastIndexOf("/")+1, this.function.indexOf("(")); + if(fdName.isEmpty()){ + logger.error("$function reference is not valid! for " + this.function); + throw new IllegalArgumentException("$function reference is not valid! "); + } + return fdName; + } + /** + * + * @return array of args name + */ + public String[] getArgNames(){ + String argNames = this.function.substring(this.function.indexOf("(") + 1, this.function.lastIndexOf(")")); + return argNames.split(", "); + } } diff --git a/src/main/java/io/opensaber/views/FunctionEvaluator.java b/src/main/java/io/opensaber/views/FunctionEvaluator.java index 526ec3c..e57b794 100644 --- a/src/main/java/io/opensaber/views/FunctionEvaluator.java +++ b/src/main/java/io/opensaber/views/FunctionEvaluator.java @@ -1,19 +1,17 @@ package io.opensaber.views; - - import org.apache.commons.jexl2.Expression; import org.apache.commons.jexl2.JexlContext; import org.apache.commons.jexl2.JexlEngine; import org.apache.commons.jexl2.MapContext; -import java.util.List; - -public class FunctionEvaluator { +public class FunctionEvaluator implements IEvaluator{ private static final JexlEngine jexl = new JexlEngine(); private JexlContext jexlContext = new MapContext(); private FieldFunction function; private Expression jexlExpression; + + private static final String ARG = "arg"; public FunctionEvaluator(FieldFunction function) { this.function = function; @@ -22,7 +20,7 @@ public FunctionEvaluator(FieldFunction function) { public void setContextArgs() { int itr = 1; for(Object val : function.getArgValues()) { - String arg = "arg" + itr++; + String arg = ARG + itr++; jexlContext.set(arg, val); } } @@ -32,19 +30,12 @@ private void prepare() { setContextArgs(); } - public T evaluate() { + @Override + public Object evaluate() { prepare(); - T result = (T) jexlExpression.evaluate(jexlContext); + Object result = jexlExpression.evaluate(jexlContext); return result; } - public String concat(List args) { - String res = ""; - for (String arg : args) { - res = res.isEmpty() ? arg : (res + ":" + arg); - } - return res; - } - } diff --git a/src/main/java/io/opensaber/views/IEvaluator.java b/src/main/java/io/opensaber/views/IEvaluator.java new file mode 100644 index 0000000..c5ff8ba --- /dev/null +++ b/src/main/java/io/opensaber/views/IEvaluator.java @@ -0,0 +1,10 @@ +package io.opensaber.views; + +public interface IEvaluator { + /** + * evaluates to provide result + * From a given expression, a provider class, a reference template of a sview template + * @return + */ + public T evaluate(); +} diff --git a/src/main/java/io/opensaber/views/ProviderEvaluator.java b/src/main/java/io/opensaber/views/ProviderEvaluator.java new file mode 100644 index 0000000..fdc98f7 --- /dev/null +++ b/src/main/java/io/opensaber/views/ProviderEvaluator.java @@ -0,0 +1,47 @@ +package io.opensaber.views; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProviderEvaluator implements IEvaluator { + + private static Logger logger = LoggerFactory.getLogger(ProviderEvaluator.class); + + private FieldFunction function; + + public ProviderEvaluator(FieldFunction function) { + this.function = function; + } + + @Override + public Object evaluate() { + IViewFunctionProvider viewFuntionProvider = getInstance(function.getExpression()); + Object result = viewFuntionProvider.doAction(function.getArgValues()); + return result; + } + + /** + * invokes instance for given providerName + * @param providerName full qualified name of class + * @return + */ + public IViewFunctionProvider getInstance(String providerName) { + + IViewFunctionProvider viewFunctionProvider = null; + try { + if (providerName == null || providerName.isEmpty()) { + throw new IllegalArgumentException( + "view function provider class {} cannot be instantiate with empty value"); + } + Class advisorClass = Class.forName(providerName); + viewFunctionProvider = (IViewFunctionProvider) advisorClass.newInstance(); + logger.info("Invoked view function provider class with classname: " + providerName); + + } catch (ClassNotFoundException | SecurityException | InstantiationException | IllegalAccessException + | IllegalArgumentException e) { + logger.error("view function provider class {} cannot be instantiate with exception:", providerName, e); + } + return viewFunctionProvider; + } + +} diff --git a/src/main/java/io/opensaber/views/SampleViewFunctionProvider.java b/src/main/java/io/opensaber/views/SampleViewFunctionProvider.java deleted file mode 100644 index a2b7c50..0000000 --- a/src/main/java/io/opensaber/views/SampleViewFunctionProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.opensaber.views; - -import java.util.List; - -public class SampleViewFunctionProvider implements IViewFunctionProvider { - - @Override - public String doAction(List values) { - // TODO Auto-generated method stub - return null; - } - -} diff --git a/src/main/java/io/opensaber/views/Transformer.java b/src/main/java/io/opensaber/views/Transformer.java index fc80e7d..2088339 100644 --- a/src/main/java/io/opensaber/views/Transformer.java +++ b/src/main/java/io/opensaber/views/Transformer.java @@ -3,53 +3,62 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Transformer { + + private static Logger logger = LoggerFactory.getLogger(Transformer.class); + /** + * transforms a given JsonNode to representation of view templates + * view template indicates any new field or mask fields for transformation + * + * @param viewTemplate + * @param node + * @return + */ public JsonNode transform(ViewTemplate viewTemplate, ObjectNode node) { + logger.debug("transformation on input node " + node); ObjectNode result = JsonNodeFactory.instance.objectNode(); String subjectType = node.fieldNames().next(); - ObjectNode nodeAttrs = (ObjectNode) node.get(subjectType); - // for each field array item + for (Field field : viewTemplate.getFields()) { - // - if function is specified - /// - call inline function expression evaluation - if (field.getFunction() != null) { - // TODO - remove hardcoded function reference - String expression = viewTemplate.getFunctionDefinitions().get(0).getResult(); - String functionStr = field.getFunction(); - String argNames = field.getFunction().substring(functionStr.indexOf("(") + 1, - functionStr.lastIndexOf(")")); - String[] functionArgs = argNames.split(", "); + + String functionStr = field.getFunction(); + if (functionStr != null) { + + String fdName = field.getFunctioName(); + String expression = viewTemplate.getExpression(fdName); FieldFunction function = new FieldFunction(expression); List actualValues = new ArrayList<>(); - for(String oneArg: functionArgs) { + for (String oneArg : field.getArgNames()) { // Cut off the $ - //System.out.println("Added value "+ oneArg.substring(1) + " -- " + nodeAttrs.get(oneArg.substring(1))); actualValues.add(ValueType.getValue(nodeAttrs.get(oneArg.substring(1)))); } function.setArgValues(actualValues); - FunctionEvaluator evaluator = new FunctionEvaluator(function); + IEvaluator evaluator = EvaluatorFactory.getInstance(fdName, function); if (field.getDisplay()) { - result.put(field.getTitle(), evaluator.evaluate()); + Object evaluatedValue = evaluator.evaluate(); + if(evaluatedValue instanceof String){ + result.put(field.getTitle(), evaluatedValue.toString()); + } else { + result.set(field.getTitle(), JsonNodeFactory.instance.pojoNode(evaluatedValue)); + } + } } else if (field.getDisplay()) { result.set(field.getTitle(), nodeAttrs.get(field.getName())); } - //TODO: - // - if provider is specified - // - instantiate the provider and do an invoke on provider.doOperation - // - else: nothing } - + logger.debug("Node transformation result: " + result); return JsonNodeFactory.instance.objectNode().set(subjectType, result); } diff --git a/src/main/java/io/opensaber/views/ViewTemplate.java b/src/main/java/io/opensaber/views/ViewTemplate.java index 5a764b8..bce0926 100644 --- a/src/main/java/io/opensaber/views/ViewTemplate.java +++ b/src/main/java/io/opensaber/views/ViewTemplate.java @@ -1,11 +1,13 @@ package io.opensaber.views; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @JsonIgnoreProperties(ignoreUnknown = true) public class ViewTemplate { + private static Logger logger = LoggerFactory.getLogger(ViewTemplate.class); private String id; private String subject; @@ -36,4 +38,24 @@ public List getFields() { public void setFields(List fields) { this.fields = fields; } + /** + * return the result for a given function name + * Example: "arg1 + \" : \" + arg2" + * + * @param name function name (like concat) + * @return result + */ + public String getExpression(String name) { + String expression = ""; + for (FunctionDefinition fd : this.getFunctionDefinitions()) { + if (fd.getName().compareTo(name) == 0) { + expression = fd.getResult() != null ? fd.getResult() : fd.getProvider(); + } + } + if (expression.isEmpty()) { + logger.error("No function definition specified for function - " + name); + throw new IllegalArgumentException("No function definition specified for function - " + name); + } + return expression; + } } diff --git a/src/test/java/io/opensaber/views/FieldTest.java b/src/test/java/io/opensaber/views/FieldTest.java new file mode 100644 index 0000000..38304e1 --- /dev/null +++ b/src/test/java/io/opensaber/views/FieldTest.java @@ -0,0 +1,24 @@ +package io.opensaber.views; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FieldTest { + + private Field field = new Field(); + + @Test + public void testGetFunctioName(){ + field.setFunction("#/functionDefinitions/concat($lastName, $firstName)"); + String name = field.getFunctioName(); + assertEquals("concat", name); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetFunctioNameEmpty(){ + field.setFunction("#/functionDefinitions/($lastName, $firstName)"); + field.getFunctioName(); + } + +} diff --git a/src/test/java/io/opensaber/views/TransformerTest.java b/src/test/java/io/opensaber/views/TransformerTest.java new file mode 100644 index 0000000..f741d88 --- /dev/null +++ b/src/test/java/io/opensaber/views/TransformerTest.java @@ -0,0 +1,78 @@ +package io.opensaber.views; + +import static org.junit.Assert.assertEquals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Ignore; +import org.junit.Test; + +public class TransformerTest { + + private Transformer transformer = new Transformer(); + + @Test + public void testTransformForPersonVT() throws JsonProcessingException, IOException{ + String personJson = "{\"Person\": " + + " {\"nationalIdentifier\":\"nid823\"," + + " \"firstName\":\"Ram\"," + + " \"lastName\":\"Moorthy\"," + + " \"gender\":\"MALE\"," + + " \"dob\":\"1990-12-10\"}}"; + ObjectNode personNode = (ObjectNode) new ObjectMapper().readTree(personJson); + String viewTemplateJson = readFileContent("personVT1.json"); + ViewTemplate viewTemplate = new ObjectMapper().readValue(viewTemplateJson, ViewTemplate.class); + + JsonNode actualnode = transformer.transform(viewTemplate, personNode); + System.out.println("actualnode = "+actualnode); + JsonNode expectedNode = new ObjectMapper().readTree("{\"Person\":{\"NAME\":\"Ram\",\"lastName\":\"Moorthy\",\"Name in passport\":\"Moorthy, Ram\"}}"); + System.out.println("expectedNode = "+expectedNode); + assertEquals(expectedNode, actualnode); + + } + + @Test@Ignore + public void testTransformForMathVT() throws JsonProcessingException, IOException{ + String mathProblem = "{\"Math\": " + + " {\"a\": 5," + + " \"b\": 2 }}"; + ObjectNode node = (ObjectNode) new ObjectMapper().readTree(mathProblem); + + String viewTemplateJson = readFileContent("mathVT1.json"); + ViewTemplate viewTemplate = new ObjectMapper().readValue(viewTemplateJson, ViewTemplate.class); + + JsonNode actualnode = transformer.transform(viewTemplate, node); + JsonNode expectedNode = new ObjectMapper().readTree("{\"Math\":{\"addend_A\":5,\"addend_B\":2,\"SUM\":7}}"); + assertEquals(expectedNode.toString(), actualnode.toString()); + + } + + private static String readFileContent(String fileName) { + InputStream in; + try { + in = Transformer.class.getClassLoader().getResourceAsStream(fileName); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = in.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString(StandardCharsets.UTF_8.name()); + + } catch (FileNotFoundException e1) { + e1.printStackTrace(); + + } catch (IOException e) { + e.printStackTrace(); + + } + return null; + } +} diff --git a/src/test/java/io/opensaber/views/ViewTemplateTest.java b/src/test/java/io/opensaber/views/ViewTemplateTest.java new file mode 100644 index 0000000..7bba99b --- /dev/null +++ b/src/test/java/io/opensaber/views/ViewTemplateTest.java @@ -0,0 +1,43 @@ +package io.opensaber.views; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +public class ViewTemplateTest { + + private ViewTemplate vt; + + @Before + public void init() { + vt = new ViewTemplate(); + FunctionDefinition fd = new FunctionDefinition(); + fd.setName("name"); + fd.setResult("expression"); + List fds = new ArrayList<>(); + fds.add(fd); + vt.setFunctionDefinitions(fds); + } + + @Test + public void testGetExpression() { + + String result = vt.getExpression("name"); + + assertEquals(vt.getFunctionDefinitions().get(0).getResult(), result); + assertNotEquals("unexpected", result); + + } + + @Test(expected = IllegalArgumentException.class) + public void testGetExpressionException() { + + vt.getExpression("invalid_name"); + + } + +} diff --git a/src/main/resources/mathVT1.json b/src/test/resources/mathVT1.json similarity index 100% rename from src/main/resources/mathVT1.json rename to src/test/resources/mathVT1.json diff --git a/src/main/resources/personVT1.json b/src/test/resources/personVT1.json similarity index 89% rename from src/main/resources/personVT1.json rename to src/test/resources/personVT1.json index cdf99d8..539fb1c 100644 --- a/src/main/resources/personVT1.json +++ b/src/test/resources/personVT1.json @@ -18,7 +18,7 @@ }, { "title": "Name in passport", - "function": "#/functionDefinitions/concat($lastName, $firstName)", + "function": "#/functionDefinitions/userDefinedConcat($lastName, $firstName)", "$comment": "This is a virtual field not defined in the schema" }, { @@ -37,7 +37,7 @@ }, { "name" : "userDefinedConcat", - "provider": "user.opensaber.registry.utils.MySplConcatenator", + "provider": "io.opensaber.provider.SampleViewFunctionProvider", "$comment" : "Complex operations that cannot be expressed easily in an in-line function definition can be implemented as a class. " }, {