package nl.ipo.cds.etl.test; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import nl.idgis.commons.jobexecutor.Job; import nl.idgis.commons.jobexecutor.JobLogger.LogLevel; import nl.ipo.cds.domain.EtlJob; import nl.ipo.cds.domain.ImportJob; import nl.ipo.cds.etl.AbstractValidator; import nl.ipo.cds.etl.CountingFeatureOutputStream; import nl.ipo.cds.etl.Feature; import nl.ipo.cds.etl.FeatureFilter; import nl.ipo.cds.etl.PersistableFeature; import nl.ipo.cds.etl.ValidatorMessageKey; import nl.ipo.cds.etl.log.EventLogger; import nl.ipo.cds.validation.ValidatorContext; import nl.ipo.cds.validation.gml.codelists.CodeList; import nl.ipo.cds.validation.gml.codelists.CodeListFactory; import nl.ipo.cds.validation.gml.codelists.StaticCodeListFactory; import org.junit.Assert; public class ValidationRunner<T extends PersistableFeature, K extends Enum<K> & ValidatorMessageKey<K, C>, C extends ValidatorContext<K, C>> { private final AbstractValidator<T, K, C> validator; private final Class<?> featureClass; public ValidationRunner (final AbstractValidator<T, K, C> validator, final Class<T> featureClass) { assert (validator != null); assert (featureClass != null); this.validator = validator; this.featureClass = featureClass; } public Runner validation (final String validationName) { return new Runner (validationName); } public Runner validation () { return new Runner (); } public interface Result<K extends Enum<K>> { Result<K> assertNoMessages (); Result<K> assertKey (K key); Result<K> assertKey (K key, int count); Result<K> assertOnlyKey (K key); Result<K> assertOnlyKey (K key, int count); Result<K> assertMessage (K key, String ... values); } public class Runner implements Result<K> { public final String validationName; public final List<T> features; public final List<CodeList> codeLists; public Runner () { this (null, null, null); } public Runner (final String validationName) { this (validationName, null, null); } public Runner (final String validationName, final List<T> features, final List<CodeList> codeLists) { this.validationName = validationName; this.features = features != null ? new ArrayList<> (features) : Collections.<T>emptyList (); this.codeLists = codeLists != null ? new ArrayList<> (codeLists) : Collections.<CodeList>emptyList (); } public Runner withCodeList (final String codeSpace, final String ... values) { final Set<String> codes = new HashSet<> (); for (final String s: values) { codes.add (s); } final CodeList codeList = new CodeList () { @Override public boolean hasCode (final String code) { return codes.contains (code); } @Override public Set<String> getCodes () { return Collections.unmodifiableSet (codes); } @Override public String getCodeSpace () { return codeSpace; } }; final List<CodeList> lists = new ArrayList<> (codeLists); lists.add (codeList); return new Runner (validationName, features, lists); } public Runner withFeature (final T feature) { final ArrayList<T> newFeatures = new ArrayList<> (features); newFeatures.add (feature); return new Runner (validationName, newFeatures, codeLists); } public Runner withFeature (final String propertyName, final Object propertyValue) { return withFeature () .with (propertyName, propertyValue) .finish (); } public FeatureBuilder withFeature () { try { @SuppressWarnings("unchecked") final T feature = (T)featureClass.newInstance (); return new FeatureBuilder (feature); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException (e); } } public Runner with (final Object propertyValue) { assert (validationName != null); return withFeature (validationName, propertyValue); } public Result<K> run () { final EtlJob job = new ImportJob (); final List<LogLine> logLines = new ArrayList<> (); final EventLogger<K> logger = new EventLogger<K> () { @Override public String logEvent (final Job job, final K messageKey, final LogLevel logLevel, final String... messageValues) { logLines.add (new LogLine (messageKey, messageValues)); return messageKey.toString (); } @Override public String logEvent (final Job job, final K messageKey, final LogLevel logLevel, final double x, final double y, final String gmlId, final String... messageValues) { logLines.add (new LogLine (messageKey, messageValues)); return messageKey.toString (); } @Override public String logEvent(final Job job, final K messageKey, final LogLevel logLevel, final Map<String, Object> context, final String... messageValues) { logLines.add (new LogLine (messageKey, messageValues)); return messageKey.toString (); } }; final FeatureFilter<T, T> filter; final Map<String, CodeList> codeLists = new HashMap<> (); for (final CodeList list: this.codeLists) { codeLists.put (list.getCodeSpace (), list); } final CodeListFactory codeListFactory = new StaticCodeListFactory (codeLists); if (validationName != null) { filter = validator.getFilterForJob (job, codeListFactory, logger, validationName); } else { filter = validator.getFilterForJob (job, codeListFactory, logger); } final CountingFeatureOutputStream<T> outputStream = new CountingFeatureOutputStream<> (); final CountingFeatureOutputStream<Feature> errorStream = new CountingFeatureOutputStream<> (); for (final T feature: features) { filter.processFeature (feature, outputStream, errorStream); } filter.finish (); return new MaterializedResult (logLines, outputStream.getFeatureCount (), errorStream.getFeatureCount ()); } public class FeatureBuilder { public final T feature; public FeatureBuilder (final T feature) { this.feature = feature; } public FeatureBuilder with (final Object propertyValue) { assert (validationName != null); return with (validationName, propertyValue); } public FeatureBuilder with (final String propertyName, final Object propertyValue) { assert (propertyName != null && !propertyName.isEmpty ()); final String methodName = String.format ("set%s%s", propertyName.substring (0, 1).toUpperCase (), propertyName.substring (1)); final Method method = findMethod (methodName); if (method != null) { try { final MethodHandle mh = MethodHandles.lookup().unreflect (method); mh.invoke (feature, propertyValue); } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException (e); } } return this; } private Method findMethod (final String name) { for (final Method m: featureClass.getMethods ()) { if (m.getName ().equals (name) && m.getReturnType ().equals (Void.TYPE) && m.getParameterTypes ().length == 1) { return m; } } return null; } public Runner finish () { return Runner.this.withFeature (feature); } } @Override public Result<K> assertKey(K key) { return run ().assertKey (key); } @Override public Result<K> assertKey(K key, int count) { return run ().assertKey (key, count); } @Override public Result<K> assertOnlyKey(K key) { return run ().assertOnlyKey (key); } @Override public Result<K> assertOnlyKey(K key, int count) { return run ().assertOnlyKey (key, count); } @Override public Result<K> assertNoMessages() { return run ().assertNoMessages (); } @Override public Result<K> assertMessage (final K key, final String... values) { return run ().assertMessage (key, values); } } public class MaterializedResult implements Result<K> { public final List<LogLine> logLines; public final int validCount; public final int errorCount; public MaterializedResult (final List<LogLine> logLines, final int validCount, final int errorCount) { this.logLines = Collections.unmodifiableList (new ArrayList<> (logLines)); this.validCount = validCount; this.errorCount = errorCount; } private String listKeys () { final StringBuilder builder = new StringBuilder (); for (final LogLine l: logLines) { if (builder.length () > 0) { builder.append (","); } builder.append (l.messageKey); } return "[" + builder.toString () + "]"; } private String listAll () { final StringBuilder builder = new StringBuilder (); for (final LogLine l: logLines) { if (builder.length () > 0) { builder.append (","); } builder.append (String.format ("%s(%s)", l.messageKey, join (l.values))); } return "[" + builder.toString () + "]"; } private String join (final String[] values) { final StringBuilder builder = new StringBuilder (); for (final String v: values) { if (builder.length () >= 0) { builder.append (";"); } builder.append (v); } return builder.toString (); } @Override public Result<K> assertKey (final K key) { return assertKey (key, 1); } @Override public Result<K> assertKey (final K key, final int count) { int n = 0; for (final LogLine l: logLines) { if (l.messageKey.equals (key)) { ++ n; } } Assert.assertEquals (String.format ("Expected %d instances of %s, found %s", count, key, listKeys ()), count, n); return this; } @Override public Result<K> assertOnlyKey (final K key) { return assertOnlyKey (key, 1); } @Override public Result<K> assertOnlyKey(K key, int count) { int n = 0; for (final LogLine l: logLines) { if (l.messageKey.equals (key)) { ++ n; } else { Assert.fail (String.format ("Found %s while expecting only %s, %s", l.messageKey, key, listKeys ())); return this; } } Assert.assertEquals (String.format ("Expected %d instances of %s, found %s", count, key, listKeys ()), count, n); return this; } @Override public Result<K> assertNoMessages () { Assert.assertEquals (String.format ("Expected no messages, found: %s", listKeys ()), 0, logLines.size ()); return this; } @Override public Result<K> assertMessage (final K key, final String... values) { for (final LogLine l: logLines) { if (!l.messageKey.equals (key)) { continue; } if (values.length != l.values.length) { continue; } int i = 0; for (i = 0; i < values.length; ++ i) { if (values[i] == null || !values[i].equals (l.values[i])) { break; } } if (i == values.length) { return this; } } Assert.fail (String.format ("Expected %s(%s), found: %s", key, join (values), listAll ())); return this; } } public class LogLine { public final K messageKey; public final String[] values; public LogLine (final K messageKey, final String[] values) { assert (messageKey != null); assert (values != null); this.messageKey = messageKey; this.values = values; } } }