package nl.ipo.cds.etl;
import nl.idgis.commons.jobexecutor.JobLogger;
import nl.idgis.commons.jobexecutor.JobLogger.LogLevel;
import nl.ipo.cds.domain.EtlJob;
import nl.ipo.cds.etl.log.EventLogger;
import nl.ipo.cds.etl.log.LogStringBuilder;
import nl.ipo.cds.validation.Expression;
import nl.ipo.cds.validation.Validation;
import nl.ipo.cds.validation.ValidationReporter;
import nl.ipo.cds.validation.ValidatorContext;
import nl.ipo.cds.validation.execute.Compiler;
import nl.ipo.cds.validation.execute.CompilerException;
import nl.ipo.cds.validation.gml.codelists.CodeListFactory;
import nl.ipo.cds.validation.logical.AndExpression;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.deegree.geometry.primitive.Point;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
public abstract class AbstractValidator<T extends PersistableFeature, Keys extends Enum<Keys> & ValidatorMessageKey<Keys, Context>, Context extends ValidatorContext<Keys, Context>>
extends Validation<Keys, Context>
implements Validator<T> {
private static final Log log = LogFactory.getLog (AbstractValidator.class);
private final Class<Context> contextClass;
private final Class<T> featureClass;
private final Map<Object, Object> validatorMessages;
private SortedMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> validators = null;
private Map<String, ValidatorExecutor<T, Keys, Context>> executors = null;
private ValidatorExecutor<T, Keys, Context> executor = null;
private SortedMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> preValidators = null;
private Map<String, GlobalValidatorExecutor<Keys, Context>> preExecutors = null;
private GlobalValidatorExecutor<Keys, Context> preExecutor = null;
private SortedMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> postValidators = null;
private Map<String, GlobalValidatorExecutor<Keys, Context>> postExecutors = null;
private GlobalValidatorExecutor<Keys, Context> postExecutor = null;
@Retention (RetentionPolicy.RUNTIME)
public @interface Precondition {
}
@Retention (RetentionPolicy.RUNTIME)
public @interface Postcondition {
}
public AbstractValidator (final Class<Context> contextClass, final Class<T> featureClass, final Map<Object, Object> validatorMessages) {
if (contextClass == null) {
throw new NullPointerException ("contextClass cannot be null");
}
this.contextClass = contextClass;
this.featureClass = featureClass;
this.validatorMessages = new HashMap<Object, Object> (validatorMessages);
}
public final void compile () throws CompilerException {
this.validators = new TreeMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> (findValidators (null));
this.preValidators = new TreeMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> (findValidators (Precondition.class));
this.postValidators = new TreeMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> (findValidators (Postcondition.class));
this.executors = createExecutors (contextClass, validators);
this.preExecutors = createGlobalExecutors (contextClass, preValidators);
this.postExecutors = createGlobalExecutors (contextClass, postValidators);
this.executor = createExecutor (contextClass, validators);
this.preExecutor = createGlobalExecutor (contextClass, preValidators);
this.postExecutor = createGlobalExecutor (contextClass, postValidators);
}
public Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> getValidators () {
if (validators == null) {
throw new IllegalStateException ("Validator is not compiled");
}
return Collections.unmodifiableMap (validators);
}
public nl.ipo.cds.validation.Validator<Keys, Context> getValidator (final String name) {
if (validators == null) {
throw new IllegalStateException ("Validator is not compiled");
}
return validators.get (name);
}
private Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> findValidators (final Class<? extends Annotation> annotationClass) {
final Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> validators = new HashMap<String, nl.ipo.cds.validation.Validator<Keys, Context>> ();
for (final Method method: getClass ().getMethods ()) {
if (Modifier.isStatic (method.getModifiers ())
|| !nl.ipo.cds.validation.Validator.class.isAssignableFrom (method.getReturnType ())
|| method.getParameterTypes ().length > 0) {
continue;
}
if (annotationClass == null && (method.getAnnotation (Precondition.class) != null || method.getAnnotation (Postcondition.class) != null)) {
continue;
}
if (annotationClass != null && method.getAnnotation (annotationClass) == null) {
continue;
}
method.setAccessible (true);
try {
@SuppressWarnings("unchecked")
final nl.ipo.cds.validation.Validator<Keys, Context> validator = (nl.ipo.cds.validation.Validator<Keys, Context>)method.invoke (this);
validators.put (demangleMethodName (method.getName ()), validator);
} catch (IllegalAccessException e) {
throw new RuntimeException (e);
} catch (IllegalArgumentException e) {
throw new RuntimeException (e);
} catch (InvocationTargetException e) {
throw new RuntimeException (e);
}
}
return validators;
}
private static String demangleMethodName (final String input) {
final String nameWithoutGet;
if (input.startsWith ("get")) {
nameWithoutGet = input.substring (3, 4).toLowerCase () + input.substring (4);
} else {
nameWithoutGet = input;
}
if (nameWithoutGet.endsWith ("Validator")) {
return nameWithoutGet.substring (0, nameWithoutGet.length () - 9);
}
return nameWithoutGet;
}
private Map<String, ValidatorExecutor<T, Keys, Context>> createExecutors (final Class<Context> contextClass, final Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> validators) throws CompilerException {
final Map<String, ValidatorExecutor<T, Keys, Context>> executors = new HashMap<String, ValidatorExecutor<T, Keys, Context>> ();
final Compiler<Context> compiler = new Compiler<Context> (contextClass).addBean ("feature", featureClass);
@SuppressWarnings("unchecked")
final Class<ValidatorExecutor<T, Keys, Context>> executorClass = (Class<ValidatorExecutor<T, Keys, Context>>)((Class<?>)ValidatorExecutor.class);
for (final Map.Entry<String, nl.ipo.cds.validation.Validator<Keys, Context>> entry: validators.entrySet ()) {
executors.put (entry.getKey (), compiler.compile (entry.getValue (), executorClass));
}
return executors;
}
private Map<String, GlobalValidatorExecutor<Keys, Context>> createGlobalExecutors (final Class<Context> contextClass, final Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> validators) throws CompilerException {
final Map<String, GlobalValidatorExecutor<Keys, Context>> executors = new HashMap<String, GlobalValidatorExecutor<Keys, Context>> ();
final Compiler<Context> compiler = new Compiler<Context> (contextClass);
@SuppressWarnings("unchecked")
final Class<GlobalValidatorExecutor<Keys, Context>> executorClass = (Class<GlobalValidatorExecutor<Keys, Context>>)((Class<?>)GlobalValidatorExecutor.class);
for (final Map.Entry<String, nl.ipo.cds.validation.Validator<Keys, Context>> entry: validators.entrySet ()) {
executors.put (entry.getKey (), compiler.compile (entry.getValue (), executorClass));
}
return executors;
}
private ValidatorExecutor<T, Keys, Context> createExecutor (final Class<Context> contextClass, final Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> validators) throws CompilerException {
// Create a single validator that combines all validation rules:
final List<Expression<Keys, Context, Boolean>> inputs = new ArrayList<Expression<Keys, Context, Boolean>> ();
for (final Map.Entry<String, nl.ipo.cds.validation.Validator<Keys, Context>> entry: validators.entrySet ()) {
inputs.add (entry.getValue ());
}
final AndExpression<Keys, Context> andExpression = new AndExpression<Keys, Context> (inputs);
// Compile a validator that contains the and expression:
@SuppressWarnings("unchecked")
final Class<ValidatorExecutor<T, Keys, Context>> executorClass = (Class<ValidatorExecutor<T, Keys, Context>>)((Class<?>)ValidatorExecutor.class);
final Compiler<Context> compiler = new Compiler<Context> (contextClass).addBean ("feature", featureClass);
return compiler.compile (this.validate (andExpression), executorClass);
}
private GlobalValidatorExecutor<Keys, Context> createGlobalExecutor (final Class<Context> contextClass, final Map<String, nl.ipo.cds.validation.Validator<Keys, Context>> validators) throws CompilerException {
// Create a single validator that combines all validation rules:
final List<Expression<Keys, Context, Boolean>> inputs = new ArrayList<Expression<Keys, Context, Boolean>> ();
for (final Map.Entry<String, nl.ipo.cds.validation.Validator<Keys, Context>> entry: validators.entrySet ()) {
inputs.add (entry.getValue ());
}
if (inputs.size () == 0) {
return null;
}
final Expression<Keys, Context, Boolean> andExpression;
if (inputs.size () == 1) {
andExpression = inputs.get (0);
} else {
andExpression = new AndExpression<Keys, Context> (inputs);
}
// Compile a validator that contains the and expression:
@SuppressWarnings("unchecked")
final Class<GlobalValidatorExecutor<Keys, Context>> executorClass = (Class<GlobalValidatorExecutor<Keys, Context>>)((Class<?>)GlobalValidatorExecutor.class);
final Compiler<Context> compiler = new Compiler<Context> (contextClass);
return compiler.compile (this.validate (andExpression), executorClass);
}
public Context beforeJob (final EtlJob job, final CodeListFactory codeListFactory, final ValidationReporter<Keys, Context> reporter) {
try {
return contextClass.newInstance ();
} catch (InstantiationException e) {
throw new RuntimeException (e);
} catch (IllegalAccessException e) {
throw new RuntimeException (e);
}
}
/**
* Allows to perform post-job validation.
* @param job The job.
* @param reporter Use this to report additional validation errors.
* @param context The context.
*/
public void afterJob (final EtlJob job, final Reporter reporter, final Context context) {
}
/**
* Cleanup method to clean resources.
* @param job The job that was running.
* @param context The context.
*/
public void afterJobCleanup (final EtlJob job, final Context context) {
}
public void beforeFeature (final EtlJob job, final EventLogger<Keys> logger, final Context context, final T feature) {
}
public void afterFeature (final EtlJob job, final EventLogger<Keys> logger, final Context context, final T feature) {
}
private boolean validateFeature (final EtlJob job, final EventLogger<Keys> logger, final T feature, final Context context, ValidatorExecutor<T, Keys, Context> executor) {
final Boolean result = executor.validate (context, feature);
return result != null && result;
}
@Override
public FeatureFilter<T, T> getFilterForJob (final EtlJob etlJob, final CodeListFactory codeListFactory, final JobLogger logger) {
return getFilterForJob (etlJob, codeListFactory, logger, executor, preExecutor, postExecutor);
}
public FeatureFilter<T, T> getFilterForJob (final EtlJob etlJob, final CodeListFactory codeListFactory, final JobLogger logger, final String validationName) {
// Locate the executor:
final ValidatorExecutor<T, Keys, Context> executor = executors.get (validationName);
if (executor == null) {
throw new IllegalArgumentException (String.format ("No validation named %s exists.", validationName));
}
return getFilterForJob (etlJob, codeListFactory, logger, executor, null, null);
}
private FeatureFilter<T, T> getFilterForJob (
final EtlJob etlJob,
final CodeListFactory codeListFactory,
final JobLogger logger,
final ValidatorExecutor<T, Keys, Context> executor,
final GlobalValidatorExecutor<Keys, Context> preExecutor,
final GlobalValidatorExecutor<Keys, Context> postExecutor) {
// Create a log string builder for this job:
final LogStringBuilder<Keys> logStringBuilder = new LogStringBuilder<Keys> ();
logStringBuilder.setProperties (validatorMessages);
logStringBuilder.setJobLogger (logger);
return getFilterForJob (etlJob, codeListFactory, logStringBuilder, executor, preExecutor, postExecutor);
}
/**
* Create a filter for the given job with an existing event logger. Usefull mostly for use in
* unit-tests where logging needs to be intercepted.
*
* @param etlJob
* @param logger
* @return
*/
public FeatureFilter<T, T> getFilterForJob (final EtlJob etlJob, final CodeListFactory codeListFactory, final EventLogger<Keys> logger) {
return getFilterForJob (etlJob, codeListFactory, logger, executor, preExecutor, postExecutor);
}
public FeatureFilter<T, T> getFilterForJob (final EtlJob etlJob, final CodeListFactory codeListFactory, final EventLogger<Keys> logger, final String validationName) {
// Locate the executor:
final ValidatorExecutor<T, Keys, Context> executor = executors.get (validationName);
if (executor == null) {
throw new IllegalArgumentException (String.format ("No validation named %s exists.", validationName));
}
return getFilterForJob (etlJob, codeListFactory, logger, executor, null, null);
}
private FeatureFilter<T, T> getFilterForJob (
final EtlJob etlJob,
final CodeListFactory codeListFactory,
final EventLogger<Keys> logger,
final ValidatorExecutor<T, Keys, Context> executor,
final GlobalValidatorExecutor<Keys, Context> preExecutor,
final GlobalValidatorExecutor<Keys, Context> postExecutor) {
final Reporter reporter = new Reporter (etlJob, logger);
final Context context = beforeJob (etlJob, codeListFactory, reporter);
if (preExecutor != null) {
preExecutor.validate (context);
}
return new FeatureFilter<T, T> () {
@Override
public void processFeature (final T feature, final FeatureOutputStream<T> outputStream,
final FeatureOutputStream<Feature> errorOutputStream) {
log.debug("validating feature: " + feature.getId());
beforeFeature (etlJob, logger, context, feature);
if (validateFeature (etlJob, logger, feature, context, executor)) {
outputStream.writeFeature (feature);
} else {
errorOutputStream.writeFeature (feature);
}
afterFeature (etlJob, logger, context, feature);
}
@Override
public void finish() {
afterJobCleanup(etlJob, context);
}
@Override
public boolean postProcess() {
if (postExecutor != null) {
postExecutor.validate (context);
}
afterJob (etlJob, reporter, context);
if (reporter.hasErrors ()){
etlJob.setGeometryErrorCount (reporter.getGeometryErrorCount ());
return false;
}
return true;
}
};
}
public static interface ValidatorExecutor<T extends PersistableFeature, Keys extends Enum<Keys> & ValidatorMessageKey<Keys, Context>, Context extends ValidatorContext<Keys, Context>> {
Boolean validate (final Context context, final T feature);
}
public static interface GlobalValidatorExecutor<Keys extends Enum<Keys> & ValidatorMessageKey<Keys, Context>, Context extends ValidatorContext<Keys, Context>> {
Boolean validate (Context context);
}
public class Reporter implements ValidationReporter<Keys, Context> {
private int geometryErrorCount = 0;
private int errorCount = 0;
private final HashMap<Keys, Integer> eventCounters = new HashMap<Keys, Integer> ();
private final EtlJob job;
private final EventLogger<Keys> logger;
public Reporter (final EtlJob job, final EventLogger<Keys> logger) {
this.job = job;
this.logger = logger;
}
@Override
public void reportValidationError (
final nl.ipo.cds.validation.Validator<Keys, Context> validator,
final Context context,
final Keys messageKey,
final Object[] parameters) {
// Convert message values to string:
final String[] messageValues;
if (parameters != null) {
messageValues = new String[parameters.length];
for (int i = 0; i < parameters.length; ++ i) {
messageValues[i] = parameters[i] == null ? "" : parameters[i].toString ();
}
} else {
messageValues = new String[0];
}
// Get the current inspire ID:
final String currentInspireId = messageValues.length >= 2 ? messageValues[1] : "";
if (messageKey.isAddToShapeFile ()) {
logEvent (context, messageKey, context.getLastLocation (), currentInspireId, true, messageValues);
} else {
logEvent (context, messageKey, null, null, true, messageValues);
}
}
public void logEvent (final Context context, Keys messageKey, String... messageValues) {
this.logEvent(context, messageKey, null, null, false, messageValues);
}
private void logEvent (final Context context, Keys messageKey, Point point, String inspireId, boolean removeDuplicateId, String... messageValues) {
final String currentId = messageValues.length >= 1 && messageValues[0] != null && messageValues[0].length () > 0 ? messageValues[0] : "[onbekend]";
// final String currentInspireId = messageValues.length >= 2 ? messageValues[1] : "";
int copyPosition = removeDuplicateId ? 2 : 1;
messageValues = messageValues.length > copyPosition ? Arrays.copyOfRange (messageValues, copyPosition, messageValues.length) : new String[0];
if(messageKey.isAddToShapeFile()){
++ geometryErrorCount;
}
if(messageKey.getLogLevel () == LogLevel.ERROR) {
++ errorCount;
}
Integer counter = eventCounters.get(messageKey);
if(counter == null) {
log.debug("new counter for message: " + messageKey);
counter = 1;
eventCounters.put(messageKey, counter);
} else {
log.debug("incrementing counter for message: " + messageKey + " current value: " + counter);
eventCounters.put(messageKey, counter + 1);
}
if(counter < messageKey.getMaxMessageLog()) {
String[] idMessageValues = new String[messageValues.length + 1];
idMessageValues[0] = currentId;
System.arraycopy(messageValues, 0, idMessageValues, 1, messageValues.length);
log.debug("event logged: " + messageKey);
if(point != null){
logger.logEvent(job, messageKey, messageKey.getLogLevel (), point.get0(), point.get1(), inspireId, idMessageValues);
} else{
logger.logEvent(job, messageKey, messageKey.getLogLevel (), idMessageValues);
}
} else if(counter == messageKey.getMaxMessageLog()) {
log.debug("max message log reached for message: " + messageKey);
logger.logEvent(job, messageKey.getMaxMessageKey (), messageKey.getMaxMessageKey ().getLogLevel (),
messageKey.toString());
} else {
log.debug("event ignored: " + messageKey);
}
}
public boolean hasErrors () {
return errorCount > 0;
}
public int getGeometryErrorCount () {
return geometryErrorCount;
}
}
}