com.imsweb.validation.ValidationContextFunctions Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of validation Show documentation
Show all versions of validation Show documentation
Java implemenation of the SEER edits.
/*
* Copyright (C) 2004 Information Management Services, Inc.
*/
package com.imsweb.validation;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import groovy.lang.Binding;
import com.imsweb.validation.internal.ExtraPropertyEntityHandlerDto;
/**
* Helper methods made available to the edits.
*
* These functions need to be initialized before executing any edits in the framework. This class only provides basic methods;
* there are more advanced implementations that extends this class and can be provided in the initialize() method.
*
* To initialize the framework with these basic method, call the following:
* ValidationContextFunctions.initialize(new ValidatorContextFunction())
* As of version 1.5, the functions are lazily initialized with the default implementation; so if you don't need a special implementation,
* there is no need to initialize this class anymore.
*
* To add your own methods to the context of the edits, create a class that extends this one, add the methods, and call the following:
* ValidationContextFunctions.initialize(new MyValidatorContextFunction())
*
* A special method in this class is the "getContext" method; it allows an edit from one validator to access a context from
* another validator. In version 2.0, the engine was re-written to become non-static and allow multiple engines to run
* concurrently (if you are not planning on using that feature, you can stop reading!). The method uses the static cached
* engine (see ValidationEngine.getInstance()) and therefore it is not compatible with multiple engines. This is a known
* issue that might be fixed in the future, but since referencing contexts from other validators is very uncommon, the
* issue will be left unresolved for now.
*/
public class ValidationContextFunctions {
// unique private instance
private static ValidationContextFunctions _INSTANCE = new ValidationContextFunctions();
/**
* Initializes this class with the passed instance.
*
* If it was already initialized with another instance, the previous one will be overridden with the new one. Use the
* isInitialized() methods if you don't want this behavior.
*
* Created on Feb 11, 2008 by depryf
* @param instance a ValidationContextFunctions
instance
*/
public static void initialize(ValidationContextFunctions instance) {
_INSTANCE = instance;
}
/**
* Gets current instance of the ValidationContextFunctions
*
* Created on Feb 11, 2008 by depryf
* @return a ValidationContextFunctions
*/
public static ValidationContextFunctions getInstance() {
return _INSTANCE;
}
/**
* Returns documentation about any methods available in the context of the edits execution.
*
* Created on Mar 3, 2010 by depryf
* @return a list of ValidatorContextFunctionDto
*/
public static List getMethodsDocumentation() {
if (_INSTANCE == null)
throw new RuntimeException("Validation Context Functions have not been initialized!");
List dtos = new ArrayList<>();
for (Method m : _INSTANCE.getClass().getMethods()) {
ContextFunctionDocAnnotation annotation = m.getAnnotation(ContextFunctionDocAnnotation.class);
if (annotation != null) {
ContextFunctionDocDto dto = new ContextFunctionDocDto();
dto.setMethodName(m.getName());
if (!StringUtils.isEmpty(annotation.param1()))
dto.getParams().add(annotation.param1());
if (!StringUtils.isEmpty(annotation.param2()))
dto.getParams().add(annotation.param2());
if (!StringUtils.isEmpty(annotation.param3()))
dto.getParams().add(annotation.param3());
if (!StringUtils.isEmpty(annotation.param4()))
dto.getParams().add(annotation.param4());
if (!StringUtils.isEmpty(annotation.param5()))
dto.getParams().add(annotation.param5());
if (!StringUtils.isEmpty(annotation.param6()))
dto.getParams().add(annotation.param6());
if (!StringUtils.isEmpty(annotation.param7()))
dto.getParams().add(annotation.param7());
if (!StringUtils.isEmpty(annotation.param8()))
dto.getParams().add(annotation.param8());
if (!StringUtils.isEmpty(annotation.param9()))
dto.getParams().add(annotation.param9());
if (!StringUtils.isEmpty(annotation.param10()))
dto.getParams().add(annotation.param10());
if (!StringUtils.isEmpty(annotation.paramName1()))
dto.getParamNames().add(annotation.paramName1());
if (!StringUtils.isEmpty(annotation.paramName2()))
dto.getParamNames().add(annotation.paramName2());
if (!StringUtils.isEmpty(annotation.paramName3()))
dto.getParamNames().add(annotation.paramName3());
if (!StringUtils.isEmpty(annotation.paramName4()))
dto.getParamNames().add(annotation.paramName4());
if (!StringUtils.isEmpty(annotation.paramName5()))
dto.getParamNames().add(annotation.paramName5());
if (!StringUtils.isEmpty(annotation.paramName6()))
dto.getParamNames().add(annotation.paramName6());
if (!StringUtils.isEmpty(annotation.paramName7()))
dto.getParamNames().add(annotation.paramName7());
if (!StringUtils.isEmpty(annotation.paramName8()))
dto.getParamNames().add(annotation.paramName8());
if (!StringUtils.isEmpty(annotation.paramName9()))
dto.getParamNames().add(annotation.paramName9());
if (!StringUtils.isEmpty(annotation.paramName10()))
dto.getParamNames().add(annotation.paramName10());
dto.setDescription(annotation.desc());
dto.setExample(annotation.example());
dtos.add(dto);
}
}
return dtos;
}
// cached regular expressions
private Map _regexCache;
// maximum size of the regex cache (-1 for no limit)
private int _regexCacheSize;
// stats for the cached regular expressions
private AtomicLong _numRegexCacheHit, _numRegexCacheMiss;
/**
* Forces the given entity (corresponding to the given collection name) to report the given properties when the edit fails.
*
* There can be any number of properties. But if none are provided, then the properties gathered statically from the edit text will be used instead.
*
* Created on Apr 21, 2010 by depryf
* @param binding groovy binding (cannot be null)
* @param entity entity on which the properties need to be reported (cannot be null)
* @param properties properties to report; if none are provided, the failures will be reported for all the static properties
*/
@SuppressWarnings("unchecked")
@ContextFunctionDocAnnotation(paramName1 = "binding", param1 = "Groovy binding (always called 'binding')", paramName2 = "entity",
param2 = "entity to force the failure on; what entity means is application-dependent", paramName3 = "properties",
param3 = "optional properties (with alias prefix) to report the failure on; if none are provided, the failure will be reported for all properties used in the edit",
desc = "Forces a failure on the provided entity, this can be useful in situations where an edits iterates over sub-entities and a failure needs to be reported on some of those sub-entities.",
example = "Functions.forceFailureOnEntity(binding, line)\nFunctions.forceFailureOnEntity(binding, line, 'line.nameLast')\nFunctions.forceFailureOnEntity(binding, line, 'line.nameLast', 'line.nameFirst')")
public void forceFailureOnEntity(Binding binding, Object entity, String... properties) {
if (binding == null || entity == null)
return;
Set forcedEntities = (Set)binding.getVariable(ValidationEngine.VALIDATOR_FORCE_FAILURE_ENTITY_KEY);
if (forcedEntities == null) {
forcedEntities = new HashSet<>();
binding.setVariable(ValidationEngine.VALIDATOR_FORCE_FAILURE_ENTITY_KEY, forcedEntities);
}
forcedEntities.add(new ExtraPropertyEntityHandlerDto(entity, properties));
}
/**
* Forces the given properties to be reported if the edit fails.
*
* Created on Apr 27, 2010 by depryf
* @param binding groovy binding (cannot be null)
* @param properties properties to report (with alias prefix); if null or empty then the function does nothing
*/
@SuppressWarnings("unchecked")
@ContextFunctionDocAnnotation(paramName1 = "binding", param1 = "Groovy binding (always called 'binding')", paramName2 = "properties",
param2 = "properties (with alias prefix) to report the failure on; if none are provided, this function does nothing",
desc = "Forces the provided properties to be reported when a failure happens; the properties will be reported on the current entity being validated (what entity means is application-dependent).",
example = "Functions.forceFailureOnProperty(binding, 'line.nameLast')\nFunctions.forceFailureOnProperty(binding, 'line.nameLast', 'line.nameFirst')")
public void forceFailureOnProperty(Binding binding, String... properties) {
if (properties == null || properties.length == 0)
return;
Set forcedProperties = (Set)binding.getVariable(ValidationEngine.VALIDATOR_FORCE_FAILURE_PROPERTY_KEY);
if (forcedProperties == null) {
forcedProperties = new HashSet<>();
binding.setVariable(ValidationEngine.VALIDATOR_FORCE_FAILURE_PROPERTY_KEY, forcedProperties);
}
forcedProperties.addAll(Arrays.asList(properties));
}
/**
* Forces the given properties to be ignored if the edit fails.
*
* Created on Apr 27, 2010 by depryf
* @param binding groovy binding (cannot be null)
* @param properties properties to ignore (with alias prefix); if null or empty then the function does nothing
*/
@SuppressWarnings("unchecked")
@ContextFunctionDocAnnotation(paramName1 = "binding", param1 = "Groovy binding (always called 'binding')", paramName2 = "properties",
param2 = "properties (with alias prefix) to ignore; if none are provided, this function does nothing", desc = "Ignores the provided properties when reporting a failure for the edit.",
example = "Functions.ignoreFailureOnProperty(binding, 'line.nameLast')\nFunctions.ignoreFailureOnProperty(binding, 'line.nameLast', 'line.nameFirst')")
public void ignoreFailureOnProperty(Binding binding, String... properties) {
if (properties == null || properties.length == 0)
return;
Set ignoredProperties = (Set)binding.getVariable(ValidationEngine.VALIDATOR_IGNORE_FAILURE_PROPERTY_KEY);
if (ignoredProperties == null) {
ignoredProperties = new HashSet<>();
binding.setVariable(ValidationEngine.VALIDATOR_IGNORE_FAILURE_PROPERTY_KEY, ignoredProperties);
}
ignoredProperties.addAll(Arrays.asList(properties));
}
/**
* Gets the value defined in the context of the passed validator ID, under the passed key
*
* Created on Nov 12, 2007 by depryf
* @param validatorId validator ID
* @param contextKey context key
* @return an object, possibly null
* @throws ValidationException if provided validator ID or context key are null or invalid
*/
@ContextFunctionDocAnnotation(paramName1 = "validatorId", param1 = "validator ID", paramName2 = "contextKey", param2 = "context key",
desc = "Returns the value of the requested context key, throws an exception if the context is not found.", example = "Functions.getContext('seer', 'Birthplace_Table')")
public Object getContext(String validatorId, String contextKey) throws ValidationException {
if (validatorId == null)
throw new ValidationException("Group is required when accessing a context entry.");
if (contextKey == null)
throw new ValidationException("Context key is required when accessing a context entry.");
// this method uses the default (static) cached engine, this is a know limitation that hopefully won't cause trouble to anybody
Object context = ValidationEngine.getInstance().getContext(contextKey, validatorId);
if (context == null)
throw new ValidationException("Unknown context key '" + contextKey + "' from group '" + validatorId + "'");
return context;
}
/**
* Returns the ValidationLookup
corresponding to the passed ID, throws an exception if such a lookup doesn't exist.
*
* Created on Dec 20, 2007 by depryf
* @param id lookup ID
* @return a ValidationLookup
, never null
* @throws ValidationException if provided lookup ID is null or invalid
*/
@ContextFunctionDocAnnotation(paramName1 = "id", param1 = "Lookup ID", desc = "Returns the lookup corresponding to the requested ID.\n\n" +
"The returned object is a ValidationLookup on which the following methods are available:\n" +
" String getId()\n" +
" String getByKey(String key)\n" +
" String getByKeyWithCase(String key)\n" +
" Set getAllByKey(String key)\n" +
" Set getAllByKeyWithCase(String key)\n" +
" Set getAllKeys()\n" +
" String getByValue(String value)\n" +
" String getByValueWithCase(String value)\n" +
" Set getAllByValue(String value)\n" +
" Set getAllByValueWithCase(String value)\n" +
" Set getAllValues()\n" +
" boolean containsKey(Object key)\n" +
" boolean containsKeyWithCase(Object key)\n" +
" boolean containsValue(Object value)\n" +
" boolean containsValueWithCase(Object value)\n" +
" boolean containsPair(String key, String value)\n" +
" boolean containsPairWithCase(String key, String value)\n",
example = "Functions.fetchLookup('lookup_id').containsKey(value)")
public ValidationLookup fetchLookup(String id) throws ValidationException {
if (id == null)
throw new ValidationException("Unable to load lookup ");
ValidationLookup lookup = ValidationServices.getInstance().getLookupById(id);
if (lookup == null)
throw new ValidationException("Unable to load lookup '" + id + "'");
return lookup;
}
/**
* Returns the value corresponding to the passed ID from our configuration files. Returns null if such an ID doesn't exist.
*
* Created on Dec 20, 2007 by depryf
* @param id configuration variable ID
* @return corresponding value, null if it doesn't exist
* @throws ValidationException if provided ID is null
*/
@ContextFunctionDocAnnotation(paramName1 = "id", param1 = "Configuration variable ID", desc = "Returns the value of the requested configuration variable.",
example = "Functions.fetchConfVariable('id')")
public Object fetchConfVariable(String id) throws ValidationException {
if (id == null)
throw new ValidationException("Unable to fetch configuration variable for null value");
return ValidationServices.getInstance().getConfVariable(id);
}
/**
* Logs the message
*
* Created on Dec 20, 2007 by depryf
* @param message Message to log
*/
@ContextFunctionDocAnnotation(paramName1 = "message", param1 = "Message", desc = "Logs the given message.",
example = "Functions.log('message')")
public void log(String message) {
ValidationServices.getInstance().log(message);
}
/**
* Logs the message as a warning
*
* Created on Dec 20, 2007 by depryf
* @param message Message to log
*/
@ContextFunctionDocAnnotation(paramName1 = "message", param1 = "Message", desc = "Logs the given message as a warning.",
example = "Functions.logWarning('warning message')")
public void logWarning(String message) {
ValidationServices.getInstance().logWarning(message);
}
/**
* Logs the message as an error
*
* Created on Dec 20, 2007 by depryf
* @param message Message to log
*/
@ContextFunctionDocAnnotation(paramName1 = "message", param1 = "Message", desc = "Logs the given message as a error.",
example = "Functions.logError('error message')")
public void logError(String message) {
ValidationServices.getInstance().logError(message);
}
/**
* Utility method that attempts to convert the supplied object to an Integer.
*
* Created on Dec 27, 2007 by depryf
* @param value value
* @return corresponding Integer
value, or null if the conversion fails
*/
@ContextFunctionDocAnnotation(paramName1 = "value", param1 = "value to convert to an Integer", desc = "Converts the passed value to an Integer, returns null if it can't be converted.",
example = "Functions.asInt(record.ageAtDx)\nFunctions.asInt(25)\nFunctions.asInt('25')\nFunctions.asInt('whatever') would return null")
public Integer asInt(Object value) {
Integer result = null;
if (value != null) {
if (value instanceof Integer)
result = (Integer)value;
else if (value instanceof Number)
result = ((Number)value).intValue();
else {
String str = value instanceof String ? (String)value : value.toString();
if (NumberUtils.isDigits(str))
result = Integer.valueOf(str);
}
}
return result;
}
/**
* Checks that the passed value is between low
and high
; both inclusive.
*
* Created on Dec 27, 2007 by depryf
* @param value value to check
* @param low low limit to check against
* @param high high limit to check against
* @return true if value is between low and high, false otherwise
*/
@ContextFunctionDocAnnotation(paramName1 = "value", param1 = "value to compare", paramName2 = "low", param2 = "low limit", paramName3 = "high", param3 = "high limit",
desc = "returns true if the value is between the low and hight limit (inclusive), false it is not or cannot be determined",
example = "Functions.between(2, 1, 3) -> true\nFunctions.between('B', 'A', 'C') -> true")
public boolean between(Object value, Object low, Object high) {
if (value == null || low == null || high == null)
return false;
if (value instanceof String) {
String val = (String)value;
String l = low.toString();
String h = high.toString();
// special case, if the three params are numeric string, compare them as numeric, not strings
if (NumberUtils.isDigits(val) && NumberUtils.isDigits(l) && NumberUtils.isDigits(h)) {
long lVal = Long.parseLong(val);
long lLow = Long.parseLong(l);
long lHigh = Long.parseLong(h);
return lVal >= lLow && lVal <= lHigh;
}
else
return val.compareTo(l) >= 0 && val.compareTo(h) <= 0;
}
if (value instanceof Number && low instanceof Number && high instanceof Number) {
double val = ((Number)value).doubleValue();
double l = ((Number)low).doubleValue();
double h = ((Number)high).doubleValue();
return val >= l && val <= h;
}
return false;
}
/**
* Returns the current day
*
* Created on Dec 27, 2007 by depryf
* @return current day
*/
@ContextFunctionDocAnnotation(desc = "Returns the current day as an integer.", example = "Functions.getCurrentDay()")
public int getCurrentDay() {
return LocalDate.now().getDayOfMonth();
}
/**
* Returns the current month
*
* Created on Dec 27, 2007 by depryf
* @return current month
*/
@ContextFunctionDocAnnotation(desc = "Returns the current month as an integer.", example = "Functions.getCurrentMonth()")
public int getCurrentMonth() {
return LocalDate.now().getMonthValue();
}
/**
* Returns the current year
*
* Created on Dec 27, 2007 by depryf
* @return current year
*/
@ContextFunctionDocAnnotation(desc = "Returns the current year as an integer.", example = "Functions.getCurrentYear()")
public int getCurrentYear() {
return LocalDate.now().getYear();
}
/**
* No documentation on purpose, shouldn't be called from edits!
*
* Enables the regex caching with unlimited cache size.
*/
public void enableRegexCaching() {
enableRegexCaching(Integer.MAX_VALUE);
}
/**
* No documentation on purpose, shouldn't be called from edits!
*
* Enables the regex caching with a maximum cache size. It is recommended to you this method only if using an unlimited cache size creates real memory issues.
* @param cacheSize regex cache size, must be greater than 0.
*/
public void enableRegexCaching(int cacheSize) {
if (cacheSize < 0)
throw new RuntimeException("Cache size must be greater than 0!");
_regexCache = new ConcurrentHashMap<>();
_regexCacheSize = cacheSize;
_numRegexCacheHit = new AtomicLong();
_numRegexCacheMiss = new AtomicLong();
}
/**
* No documentation on purpose, shouldn't be called from edits!
*
* Disables the regex caching
*/
public void disableRegexCaching() {
_regexCache = null;
_regexCacheSize = Integer.MAX_VALUE;
_numRegexCacheHit = null;
_numRegexCacheMiss = null;
}
/**
* Returns true if the provided value matches according to the provided regular expression.
* @param value value to match
* @param regex regular expression (Java style) to match against
* @return true if the value matches, false otherwise.
*/
public boolean matches(Object value, Object regex) {
if (value == null || regex == null)
return false;
String val = value instanceof String ? (String)value : value.toString();
String reg = regex instanceof String ? (String)regex : regex.toString();
Pattern pattern;
if (_regexCache != null) {
pattern = _regexCache.get(reg);
if (pattern == null) {
_numRegexCacheMiss.incrementAndGet();
pattern = Pattern.compile(reg);
// in a multi-threaded environment, it's possible that the cache will add a few more values than the max cache size, and that's OK
if (_regexCache.size() < _regexCacheSize)
_regexCache.put(reg, pattern);
}
else
_numRegexCacheHit.incrementAndGet();
}
else
pattern = Pattern.compile(reg);
return pattern.matcher(val).matches();
}
/**
* Returns the number of hits in the regex cache.
*/
public long getNumRegexCacheHit() {
return _numRegexCacheHit == null ? 0L : _numRegexCacheHit.get();
}
/**
* Returns the number of misses in the regex cache.
*/
public long getNumRegexCacheMiss() {
return _numRegexCacheMiss == null ? 0L : _numRegexCacheMiss.get();
}
}