All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.imsweb.validation.functions.TestingContextFunctions Maven / Gradle / Ivy

There is a newer version: 021-11
Show newest version
/*
 * Copyright (C) 2011 Information Management Services, Inc.
 */
package com.imsweb.validation.functions;

import com.imsweb.validation.ValidationEngine;
import com.imsweb.validation.ValidationException;
import com.imsweb.validation.entities.Rule;
import com.imsweb.validation.entities.RuleFailure;
import com.imsweb.validation.entities.RuleTest;
import com.imsweb.validation.entities.RuleTestResult;
import com.imsweb.validation.entities.SimpleMapValidatable;
import com.imsweb.validation.entities.SimpleNaaccrLinesValidatable;
import com.imsweb.validation.entities.Validatable;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

/**
 * Context available to the testing framework. The testing Groovy scripts can access the methdos of this context
 * using the "Testing." prefix. The only methods that should be accessed are the assertPass() and assertFail()
 * methods. The getTestsResults() method is used by the testing framework to gather the assertion results and
 * provide them back to the application.
 * 

* This class supports the NAACCR line notation (line.primarySite), which is pretty standard. For applications * requiring to support other data types, this class should be extended (in particular the createValidatable() * method, and the extended version should be provided to the ruleTest.executeTest() method. *

* Created on Aug 8, 2011 by depryf * @author depryf */ public class TestingContextFunctions { /** * Test assertion types. *

* Created on Oct 2, 2011 by Fabian * @author Fabian */ public enum AssertionType { /** * Use this when expecting the edit to pass */ PASS, /** * Use this when expecting the edit to fail */ FAIL } // tested rule ID protected String _ruleId; // tested rule, if not provided, it will be fetched from the referenced engine using the tested rule ID protected Rule _rule; // map of tests, key is the line number in the script, value is a list of tests (list will have more then one element in repetitions like for loops) protected Map> _tests; // referenced engine, if not provided, the default instance will be used protected ValidationEngine _engine; /** * Constructor. *

* Created on Oct 2, 2011 by Fabian */ public TestingContextFunctions(RuleTest test) { this(test, null); } /** * Constructor. *

* Created on Oct 2, 2011 by Fabian * @param test RuleTest, cannot be null * @param rule tested Rule, can be null */ public TestingContextFunctions(RuleTest test, Rule rule) { this(test, rule, ValidationEngine.getInstance()); } /** * Constructor. *

* Created on Oct 5, 2018 by depryf * @param test RuleTest, cannot be null * @param rule tested Rule, can be null * @param engine referenced ValidationEngine */ public TestingContextFunctions(RuleTest test, Rule rule, ValidationEngine engine) { _ruleId = test.getTestedRuleId(); _rule = rule; _tests = new TreeMap<>(); _engine = engine; } /** * Asserts that a given test passes. *

* Created on Jun 6, 2011 by murphyr * @param lineNumber test line number * @param dataObj test inputs */ public void assertPass(int lineNumber, Object dataObj) { assertPass(lineNumber, dataObj, null); } /** * Asserts that a given test passes. *

* Created on Jul 21, 2011 by murphyr * @param lineNumber test line number * @param dataObj test inputs * @param context extra context for the test */ public void assertPass(int lineNumber, Object dataObj, Map context) { // redirect the output OutputStream output = new OutputStream() { private StringBuilder _buf = new StringBuilder(); @Override public void write(int b) { _buf.append((char)b); } @Override public String toString() { return _buf.toString(); } }; if (context == null) context = new HashMap<>(); context.put("out", output); try { Collection results = runTest(dataObj, context); // a test runs for one edit, so there should be 0 or 1 failure (anything else is ignored) RuleFailure failure = results.isEmpty() ? null : results.iterator().next(); // it's a success if there was no failure boolean success = failure == null; // add testing result insertTestingResult(lineNumber, AssertionType.PASS, success, failure, dataObj, context, null, null, output); } catch (ValidationException e) { insertTestingResult(lineNumber, AssertionType.PASS, false, null, dataObj, context, e, null, output); } } /** * Asserts that a given test fails. *

* Created on Jun 6, 2011 by murphyr * @param lineNumber test line number * @param dataObj test inputs * @param failingProperties array of properties that needs to appear in the list of failing properties for this failure */ public void assertFail(int lineNumber, Object dataObj, String... failingProperties) { assertFail(lineNumber, dataObj, null, failingProperties); } /** * Asserts that a given test fails. *

* Created on Jul 21, 2011 by murphyr * @param lineNumber test line number * @param dataObj test inputs * @param failingProperties array of properties that needs to appear in the list of failing properties for this failure * @param context extra context for the test */ @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void assertFail(int lineNumber, Object dataObj, Map context, String... failingProperties) { Set props = new HashSet<>(); if (failingProperties != null) props.addAll(Arrays.asList(failingProperties)); // redirect the output OutputStream output = new OutputStream() { private StringBuilder _buf = new StringBuilder(); @Override public void write(int b) { _buf.append((char)b); } @Override public String toString() { return _buf.toString(); } }; if (context == null) context = new HashMap<>(); context.put("out", output); try { Collection results = runTest(dataObj, context); // a test runs for one edit, so there should be 0 or 1 failure (anything else is ignored) RuleFailure failure = results.isEmpty() ? null : results.iterator().next(); // it's a success if there was a failure, but not an exception boolean success = failure != null && failure.getGroovyException() == null && failure.getProperties().containsAll(props); // add testing result insertTestingResult(lineNumber, AssertionType.FAIL, success, failure, dataObj, context, null, props, output); } catch (ValidationException e) { insertTestingResult(lineNumber, AssertionType.FAIL, false, null, dataObj, context, e, props, output); } } /** * Creation method. *

* Created on Nov 18, 2011 by depryf * @return created entity */ public List> createLines() { return new ArrayList<>(); } /** * Creation method. *

* Created on Nov 18, 2011 by depryf * @return created entity */ public Map createLine() { return new HashMap<>(); } /** * Creation method. *

* Created on Nov 18, 2011 by depryf * @return created entity */ public Map createLine(List> lines) { Map line = createLine(); lines.add(line); return line; } /** * Creation method. *

* Created on Nov 18, 2011 by depryf * @return created entity */ public List> createUntrimmedlines() { return new ArrayList<>(); } /** * Creation method. *

* Created on Nov 18, 2011 by depryf * @return created entity */ public Map createUntrimmedline() { return new HashMap<>(); } /** * Creation method. *

* Created on Nov 18, 2011 by depryf * @return created entity */ public Map createUntrimmedline(List> untrimmedlines) { Map untrimmedline = createUntrimmedline(); untrimmedlines.add(untrimmedline); return untrimmedline; } /** * Returns the test results gathered so far. This method is not meant to be called from a testing Groovy script. *

* Created on Jun 6, 2011 by murphyr * @return test results gathered so far. */ public Map> getTestsResults() { return Collections.unmodifiableMap(_tests); } /** * Runs the tests using the provided data and extra context. *

* Created on Jun 6, 2011 by murphyr * @param data data object being validated * @param context extra context, may be null * @return the test results, a collection of RuleFailure */ protected Collection runTest(Object data, Map context) throws ValidationException { if (_rule != null) return _engine.validate(createValidatable(data, context), _rule); else return _engine.validate(createValidatable(data, context), _ruleId); } /** * Creates a validatable object from the incoming data and context. *

* Created on Oct 2, 2011 by Fabian * @param data data object being validated * @param context extra context, may be null * @return a Validatable to use in the Groovy test, never null */ @SuppressWarnings("unchecked") protected Validatable createValidatable(Object data, Map context) { Validatable result; if (data == null) throw new RuntimeException("Invalid testing data structure: cannot run edit on null object"); Rule r = _rule; if (r == null) r = _engine.getRule(_ruleId); if (r != null) { String javaPath = r.getJavaPath(); if (javaPath != null) { if (javaPath.equals("lines") || javaPath.equals("untrimmedlines") || javaPath.equals("lines.line") || javaPath.equals("untrimmedlines.untrimmedline")) { boolean useUntrimmedNotation = r.getJavaPath().startsWith("untrimmedlines."); if ("lines".equals(javaPath) || "untrimmedlines".equals(javaPath)) { if (data instanceof List) result = new SimpleNaaccrLinesValidatable((List>)data, context, useUntrimmedNotation); else throw new RuntimeException("Invalid testing data structure: expected List, got " + data.getClass().getSimpleName()); } else { if (data instanceof Map) result = new SimpleNaaccrLinesValidatable(Collections.singletonList((Map)data), context, useUntrimmedNotation); else throw new RuntimeException("Invalid testing data structure: expected Map, got " + data.getClass().getSimpleName()); } } else if (data instanceof Map) result = new SimpleMapValidatable("?", javaPath, (Map)data, context); else throw new RuntimeException("Invalid testing data structure: expected Map, got " + data.getClass().getSimpleName()); } else throw new RuntimeException("Rule '" + r.getId() + "' doesn't define a java-path"); } else throw new RuntimeException("Unable to find rule '" + _ruleId + "'"); return result; } /** * Creates a test result, adds it to the list of existing result for the provided line number. *

* Created on Jul 21, 2011 by murphyr * @param lineNum line number * @param type expected assertion type (PASS or FAIL) * @param success whether the assertion was successful or not * @param failure rule failure (non-null only if the edit actually failed) * @param values data that was validated * @param contextValues values of the extra context used for the validation, if any * @param exc validation exception (if the validation engine was unable to run the edit * @param f asserted failing properties * @param os redirected output */ protected void insertTestingResult(int lineNum, AssertionType type, boolean success, RuleFailure failure, Object values, Map contextValues, ValidationException exc, Set f, OutputStream os) { // get the list of results for this line number List list = _tests.computeIfAbsent(lineNum, k -> new ArrayList<>()); // build the log content from the output stream List log = new ArrayList<>(); try { BufferedReader reader = new BufferedReader(new StringReader(os.toString())); String line = reader.readLine(); while (line != null) { log.add(line); line = reader.readLine(); } } catch (IOException e) { throw new RuntimeException("Unable to redirect output", e); } // add a new result to that list list.add(new RuleTestResult(lineNum, list.size() + 1, type, success, failure, cloneData(values), contextValues, exc, f, log)); } /** * This methods clone the data. It currently supports only combinations of maps, lists and simple types. *

* Created on Nov 1, 2011 by depryf * @param data data to clone * @return cloned data */ @SuppressWarnings("unchecked") protected Object cloneData(Object data) { if (data == null) return null; Object result; if (data instanceof Map) result = cloneMap((Map)data); else if (data instanceof List) result = cloneList((List)data); else if (isSimpleType(data.getClass())) result = data; else throw new RuntimeException("Unsupported data type: " + data.getClass().getName()); return result; } /** * Clones the provided map. *

* Created on Nov 1, 2011 by depryf * @param data map to clone * @return cloned map */ @SuppressWarnings("unchecked") protected Map cloneMap(Map data) { Map result = new HashMap<>(); // used to remove special properties automatically added by the validation engine Set usedProperties = null; Rule r = _rule; if (r == null) r = _engine.getRule(_ruleId); if (r != null && r.getJavaPath() != null) { String javaPath = r.getJavaPath(); if (javaPath.startsWith("lines") || javaPath.startsWith("untrimmedlines")) { usedProperties = new HashSet<>(); for (String s : r.getRawProperties()) usedProperties.add(javaPath.startsWith("lines") ? s.replace("line.", "") : s.replace("untrimmedline.", "")); } } for (Entry entry : data.entrySet()) { Object obj = entry.getValue(); if (obj == null) result.put(entry.getKey(), null); else if (obj instanceof Map) result.put(entry.getKey(), cloneMap((Map)obj)); else if (obj instanceof List) result.put(entry.getKey(), cloneList((List)obj)); else if (isSimpleType(obj.getClass())) { if (!entry.getKey().startsWith("_") || (usedProperties != null && usedProperties.contains(entry.getKey()))) result.put(entry.getKey(), obj); } else throw new RuntimeException("Unsupported data type: " + obj.getClass().getName()); } return result; } /** * Clones the provided list. *

* Created on Nov 1, 2011 by depryf * @param data list to clone * @return cloned list */ @SuppressWarnings("unchecked") protected List cloneList(List data) { List result = new ArrayList<>(); for (Object obj : data) { if (obj == null) result.add(null); else if (obj instanceof Map) result.add(cloneMap((Map)obj)); else if (obj instanceof List) result.add(cloneList((List)obj)); else if (isSimpleType(obj.getClass())) { result.add(obj); } else throw new RuntimeException("Unsupported data type: " + obj.getClass().getName()); } return result; } /** * Returns true if the provided class represent a "simple" type, false otherwise. *

* Created on Nov 1, 2011 by depryf * @param clazz class * @return true if the provided class represent a "simple" type, false otherwise */ protected boolean isSimpleType(Class clazz) { return String.class.equals(clazz) || Number.class.isAssignableFrom(clazz) || Boolean.class.equals(clazz) || Date.class.equals(clazz); } /** * Returns a testing string of passed length. The generated string will be composed of * uppercase letters, in order. When Z is reached, A starts again... *

* For example getTestingString(3) = ABC *

* Created on Jul 25, 2006 by depryf * @param length requested length * @return testing string */ public static String createString(int length) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < length; i++) buf.append((char)((i % 26) + 65)); return buf.toString(); } /** * Returns current date. *

* Created on Jan 19, 2012 by murphyr * @return current date */ public static Date createDate() { return new Date(); } /** * Returns the date based on the numbers passed in. Month is (1-12) and day is (1-31). *

* Created on Jan 19, 2012 by murphyr * @param year year to set * @param month month to set * @param day day to set * @return corresponding date */ public static Date createDate(int year, int month, int day) { return new GregorianCalendar(year, month - 1, day).getTime(); } }