package nl.ipo.cds.etl.theme.protectedSite;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Arrays;
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.JobLogger.LogLevel;
import nl.ipo.cds.domain.EtlJob;
import nl.ipo.cds.etl.AbstractValidator;
import nl.ipo.cds.etl.ValidatorMessageKey;
import nl.ipo.cds.validation.AttributeExpression;
import nl.ipo.cds.validation.DefaultValidatorContext;
import nl.ipo.cds.validation.Expression;
import nl.ipo.cds.validation.ValidationReporter;
import nl.ipo.cds.validation.Validator;
import nl.ipo.cds.validation.callbacks.Callback;
import nl.ipo.cds.validation.callbacks.UnaryCallback;
import nl.ipo.cds.validation.execute.CompilerException;
import nl.ipo.cds.validation.gml.codelists.CodeListFactory;
import org.apache.commons.io.IOUtils;
import org.deegree.geometry.primitive.Point;
import org.deegree.geometry.standard.primitive.DefaultPoint;
public class ProtectedSiteValidator
extends AbstractValidator<ProtectedSite, ProtectedSiteValidator.MessageKey, ProtectedSiteValidator.Context> {
private static final Set<String> protectionClassification = new HashSet<String> (Arrays.asList(new String[] {
"natureConservation",
"archaeological",
"cultural",
"ecological",
"landscape",
"environment",
"geological"
}));
private static final String protectionClassificationConcat = concat (protectionClassification);
private final static Map<String, String[]> designations;
static {
final HashMap<String, String[]> designationsMap = new HashMap<String, String[]> ();
designationsMap.put("AW", new String[]{"aardkundigeWaarden"});
designationsMap.put("EHS", new String[]{"ecologischeHoofdstructuur"});
designationsMap.put("WAV", new String[]{"WAVGebieden"});
designationsMap.put("ST", new String[]{"stilteGebieden"});
designationsMap.put("PM", new String[]{"provincialeMonumenten"});
designationsMap.put("NL", new String[]{"nationaleLandschappen"});
designations = Collections.unmodifiableMap (designationsMap);
}
private static final String separator = ", ";
private static final Point defaultPoint = new DefaultPoint(null, null, null, new double[]{1.0, 2.0});
public enum MessageKey implements ValidatorMessageKey<MessageKey, Context> {
ID_NULL,
INSPIREID_NULL,
INSPIREID_DUPLICATE(LogLevel.ERROR, null, "NL.9931.ST.70CD5476-1A7F-4476-A72D-B45E515A99BA"),
INSPIREID_PARTS(LogLevel.ERROR, null, "Invalid.ID"),
INSPIREID_NL(LogLevel.ERROR, null, "DE"),
INSPIREID_BRONHOUDER(LogLevel.ERROR, null, "9940","9931"),
INSPIREID_DATASET(LogLevel.ERROR, null, "NE", "NL"),
INSPIREID_UUID(LogLevel.ERROR, null, "70CD5476-1A7F-4A99BA"),
LEGALFOUNDATIONDOCUMENT_ELEMENT_NULL,
LEGALFOUNDATIONDOCUMENT_ELEMENT_EMPTY,
LEGALFOUNDATIONDOCUMENT_ELEMENT_INVALID(LogLevel.ERROR, null, "invalid://url"),
LEGALFOUNDATIONDOCUMENT_NOT_FOUND(LogLevel.ERROR, null, "http://host.invalid/document.html"),
LEGALFOUNDATIONDOCUMENT_EMPTY(LogLevel.ERROR, null, "http://host.test/empty-document.html"),
LEGALFOUNDATIONDATE_NULL,
LEGALFOUNDATIONDATE_INVALID(LogLevel.ERROR, null, "17-DEC-10"),
SITEDESIGNATION_NULL,
SITEDESIGNATION_INVALID(LogLevel.ERROR, null, "AardkundigWaarde", protectionClassificationConcat),
SITEDESIGNATION_ILLEGAL_FORMAT(LogLevel.ERROR, null,
"stilteGebieden:stilteGebieden|overigDesignationSchema:onzeDesignation:75:ongeldig"),
SITEPROTECTIONCLASSIFICATION_NULL,
SITEPROTECTIONCLASSIFICATION_INVALID(LogLevel.ERROR, null, "naturConservation", protectionClassificationConcat),
GEOMETRY_NULL,
GEOMETRY_POINT_DUPLICATION(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_EXTERIOR_RING_CW(LogLevel.WARNING),
GEOMETRY_INTERIOR_RING_CCW(LogLevel.WARNING),
GEOMETRY_DISCONTINUITY(Integer.MAX_VALUE, true),
GEOMETRY_SELF_INTERSECTION(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_RING_NOT_CLOSED(Integer.MAX_VALUE, true),
GEOMETRY_RING_SELF_INTERSECTION(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_INTERIOR_RINGS_TOUCH(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_INTERIOR_RINGS_INTERSECT(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_INTERIOR_RINGS_WITHIN(Integer.MAX_VALUE, true),
GEOMETRY_INTERIOR_RING_TOUCHES_EXTERIOR(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_INTERIOR_RING_INTERSECTS_EXTERIOR(Integer.MAX_VALUE, true, pointToString(defaultPoint)),
GEOMETRY_INTERIOR_RING_OUTSIDE_EXTERIOR(Integer.MAX_VALUE, true),
GEOMETRY_INTERIOR_DISCONNECTED(Integer.MAX_VALUE, true),
GEOMETRY_SRS_NULL,
GEOMETRY_SRS_NOT_RD("EPSG:28992"),
HAS_MORE_EVENTS(LogLevel.WARNING);
private final String[] params;
private final LogLevel logLevel;
private final int maxMessageLog;
private final boolean addToShapeFile;
private MessageKey(LogLevel logLevel, Integer maxMessageLog, boolean addToShapeFile, String... params) {
this.maxMessageLog = maxMessageLog == null ? 10 : maxMessageLog;
this.logLevel = logLevel == null ? LogLevel.ERROR : logLevel;
this.addToShapeFile = addToShapeFile;
this.params = params;
}
private MessageKey(LogLevel logLevel, Integer maxMessageLog, String... params) {
this(logLevel, maxMessageLog, false, params);
}
private MessageKey(Integer maxMessageLog, boolean addToShapeFile, String... params) {
this(null, maxMessageLog, addToShapeFile, params);
}
private MessageKey(LogLevel logLevel) {
this(logLevel, null, false);
}
private MessageKey(String... params) {
this(null, null, false, params);
}
@Override
public int getMaxMessageLog () {
return maxMessageLog;
}
@Override
public boolean isAddToShapeFile () {
return addToShapeFile;
}
public String[] getParams () {
return params;
}
@Override
public LogLevel getLogLevel () {
return logLevel;
}
@Override
public List<Expression<MessageKey, Context, ?>> getMessageParameters () {
final List<Expression<MessageKey, Context, ?>> params = new ArrayList<Expression<MessageKey, Context, ?>> ();
params.add (new AttributeExpression<MessageKey, Context, String> ("id", String.class));
params.add (new AttributeExpression<MessageKey, Context, String> ("inspireID", String.class));
return params;
}
@Override
public MessageKey getMaxMessageKey () {
return HAS_MORE_EVENTS;
}
@Override
public boolean isBlocking() {
return getLogLevel ().equals (LogLevel.ERROR);
}
}
public ProtectedSiteValidator (final Map<Object, Object> validatorMessages) throws CompilerException {
super (Context.class, ProtectedSite.class, validatorMessages);
compile ();
}
@Override
public Context beforeJob (final EtlJob job, final CodeListFactory codeListFactory, final ValidationReporter<MessageKey, Context> reporter) {
final String datasetCode = job.getDatasetType ().getNaam ();
final String[] designation = designations.get (datasetCode);
return new Context (codeListFactory, reporter, job, designation);
}
// =========================================================================
// Validation rules:
// =========================================================================
public Validator<MessageKey, Context> getLegalFoundationDocumentValidator () {
final UnaryCallback<MessageKey, Context, Boolean, String> checkSizeCallback = new UnaryCallback<MessageKey, Context, Boolean, String> () {
@Override
public Boolean call(String input, Context context) throws Exception {
final String document = stripAnchor (input);
if (context.hasDocument (document)) {
return context.getDocumentSize (document) > 0;
}
final URL url = new URL (document);
try {
final InputStream inputStream = url.openConnection ().getInputStream ();
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream ();
IOUtils.copy (inputStream, outputStream);
context.storeDocumentSize (document, (long)outputStream.size ());
return outputStream.size () > 0;
} catch (IOException e) {
// Return true, but don't add to the checkedUrls list so that the next test fails:
return true;
}
}
};
final UnaryCallback<MessageKey, Context, Boolean, String> checkDocumentExistsCallback = new UnaryCallback<MessageKey, Context, Boolean, String> () {
@Override
public Boolean call(String input, Context context) throws Exception {
return context.hasDocument (stripAnchor (input));
}
};
return validate (
and (
// Some preconditions that terminate the validation:
validate (not (stringAttr ("legalFoundationDocument").isNull ())).message (MessageKey.LEGALFOUNDATIONDOCUMENT_ELEMENT_NULL),
validate (not (isBlank (stringAttr ("legalFoundationDocument")))).message (MessageKey.LEGALFOUNDATIONDOCUMENT_ELEMENT_EMPTY),
validate (isUrl (stringAttr ("legalFoundationDocument"))).message (MessageKey.LEGALFOUNDATIONDOCUMENT_ELEMENT_INVALID, stringAttr ("legalFoundationDocument")),
and (
// Tests that don't short-circuit:
validate (callback (Boolean.class, stringAttr ("legalFoundationDocument"), checkSizeCallback)).message (MessageKey.LEGALFOUNDATIONDOCUMENT_EMPTY, stringAttr ("legalFoundationDocument")),
validate (callback (Boolean.class, stringAttr ("legalFoundationDocument"), checkDocumentExistsCallback)).message (MessageKey.LEGALFOUNDATIONDOCUMENT_NOT_FOUND, stringAttr ("legalFoundationDocument"))
)
).shortCircuit ()
);
}
public Validator<MessageKey, Context> getLegalFoundationDateValidator () {
return validate (
not (attr ("legalFoundationDate", Date.class).isNull ())
)
.message (MessageKey.LEGALFOUNDATIONDATE_NULL);
}
public Validator<MessageKey, Context> getIdValidator () {
return validate (
not (stringAttr ("id").isNull ())
)
.message (MessageKey.ID_NULL);
}
public Validator<MessageKey, Context> getSiteDesignationValidator () {
final Callback<MessageKey, Context, Boolean> initFlagCallback = new Callback<MessageKey, Context, Boolean> () {
@Override
public Boolean call (final Context context) throws Exception {
context.setFlag (false);
return true;
}
};
final UnaryCallback<MessageKey, Context, Boolean, String[]> validateSiteDesignationCallback = new UnaryCallback<MessageKey, Context, Boolean, String[]> () {
@Override
public Boolean call (final String[] input, final Context context) throws Exception {
final boolean siteDesignationValid = context.getFlag ();
if(!siteDesignationValid) {
if(input.length == 1 || (input.length > 1 && input[0].equals(input[1]))) {
context.setFlag (contains (input[0], context.getValidDesignations ()));
}
}
return true;
}
};
final Callback<MessageKey, Context, Boolean> getFlagCallback = new Callback<MessageKey, Context, Boolean> () {
@Override
public Boolean call (final Context context) throws Exception {
return context.getFlag ();
}
};
return validate (
and (
validate (not (attr ("siteDesignation", String[].class).isNull ())).message (MessageKey.SITEDESIGNATION_NULL),
callback (Boolean.class, initFlagCallback),
forEach ("i", attr ("siteDesignation", String[].class), validate (
split (stringAttr ("i"), constant (":"), validate (
and (
validate (lte (intAttr ("length"), constant (3))).message (MessageKey.SITEDESIGNATION_ILLEGAL_FORMAT, stringAttr ("i")),
callback (Boolean.class, attr ("values", String[].class), validateSiteDesignationCallback)
).shortCircuit ()
))
)),
validate (callback (Boolean.class, getFlagCallback)).message (MessageKey.SITEDESIGNATION_INVALID, join (attr ("siteDesignation", String[].class), constant ("|")), join (attribute ("validDesignations", String[].class), constant (", ")))
).shortCircuit ()
);
}
public Validator<MessageKey, Context> getSiteProtectionClassificationValidator () {
return validate (
and (
validate (not (attr ("siteProtectionClassification", String[].class).isNull ())).message (MessageKey.SITEPROTECTIONCLASSIFICATION_NULL),
forEach (
"i",
attr ("siteProtectionClassification", String[].class),
validate (in (stringAttr ("i"), constant (protectionClassification))).message (MessageKey.SITEPROTECTIONCLASSIFICATION_INVALID, stringAttr ("i"), constant (protectionClassificationConcat))
)
).shortCircuit ()
);
}
public Validator<MessageKey, Context> getGeometryValidator () {
return validate (
and (
// The following validations short-circuit, there must be a non-null and non-empty geometry:
validate (not (geometry ("geometry").isNull ())).message (MessageKey.GEOMETRY_NULL),
validate (not (geometry ("geometry").isEmptyMultiGeometry ())).message (MessageKey.GEOMETRY_NULL),
// Non short-circuited validations:
and (
// Short circuit to prevent the interiorDisconnected validation if
// any of the other validations fail:
and (
and (
validate (not (geometry ("geometry").hasCurveDuplicatePoint ())).message (MessageKey.GEOMETRY_POINT_DUPLICATION, lastLocation ()),
validate (not (geometry ("geometry").hasCurveDiscontinuity ())).message (MessageKey.GEOMETRY_DISCONTINUITY),
validate (not (geometry ("geometry").hasCurveSelfIntersection ())).message (MessageKey.GEOMETRY_SELF_INTERSECTION, lastLocation ()),
validate (not (geometry ("geometry").hasUnclosedRing ())).message (MessageKey.GEOMETRY_RING_NOT_CLOSED),
validate (not (geometry ("geometry").hasRingSelfIntersection ())).message (MessageKey.GEOMETRY_RING_SELF_INTERSECTION, lastLocation ()),
validate (not (geometry ("geometry").hasTouchingInteriorRings ())).message(MessageKey.GEOMETRY_INTERIOR_RINGS_TOUCH, lastLocation ()),
validate (not (geometry ("geometry").hasInteriorRingsWithin ())).message (MessageKey.GEOMETRY_INTERIOR_RINGS_WITHIN)
),
validate (not (this.geometry ("geometry").isInteriorDisconnected ())).message (MessageKey.GEOMETRY_INTERIOR_DISCONNECTED)
).shortCircuit (),
// Non-blocking validations:
validate (not (geometry ("geometry").hasExteriorRingCW ())).nonBlocking ().message (MessageKey.GEOMETRY_EXTERIOR_RING_CW),
validate (not (geometry ("geometry").hasInteriorRingCCW ())).nonBlocking ().message (MessageKey.GEOMETRY_INTERIOR_RING_CCW),
validate (not (geometry ("geometry").hasInteriorRingTouchingExterior ())).nonBlocking ().message (MessageKey.GEOMETRY_INTERIOR_RING_TOUCHES_EXTERIOR, lastLocation ()),
validate (not (geometry ("geometry").hasInteriorRingOutsideExterior ())).nonBlocking ().message (MessageKey.GEOMETRY_INTERIOR_RING_OUTSIDE_EXTERIOR),
// SRS validations:
validate (this.geometry ("geometry").hasSrs ()).message (MessageKey.GEOMETRY_SRS_NULL),
validate (this.geometry ("geometry").isSrs (constant ("28992"))).message (MessageKey.GEOMETRY_SRS_NOT_RD, this.geometry ("geometry").srsName ())
)
).shortCircuit ()
);
}
public Validator<MessageKey, Context> getInspireIDValidator () {
//final String bronhouderCode = job.getBronhouder ().getCode ();
//final String datasetCode = job.getDatasetType ().getNaam ();
final UnaryCallback<MessageKey, Context, Boolean, String> isUniqueCallback = new UnaryCallback<MessageKey, Context, Boolean, String> () {
@Override
public Boolean call (final String input, final Context context) throws Exception {
final boolean result = !context.hasInspireID (input);
context.storeInspireID (input);
return result;
}
};
return validate (
and (
validate (not (stringAttr ("inspireID").isNull ())).message (MessageKey.INSPIREID_NULL),
split (stringAttr ("inspireID"), constant ("\\."), validate (
and (
validate (eq (intAttr ("length"), constant (4))).message (MessageKey.INSPIREID_PARTS, stringAttr ("inspireID")),
and (
validate (eq (stringAttr ("0"), constant ("NL"))).message (MessageKey.INSPIREID_NL, stringAttr ("0")),
validate (eq (stringAttr ("1"), stringAttr ("bronhouderCode"))).message (MessageKey.INSPIREID_BRONHOUDER, stringAttr ("1"), stringAttr ("bronhouderCode")),
validate (eq (stringAttr ("2"), stringAttr ("datasetCode"))).message (MessageKey.INSPIREID_DATASET, stringAttr ("2"), stringAttr ("datasetCode")),
validate (isUUID (stringAttr ("3"))).message (MessageKey.INSPIREID_UUID, stringAttr ("3"))
)
).shortCircuit ()
)),
validate (callback (Boolean.class, stringAttr ("inspireID"), isUniqueCallback)).message (MessageKey.INSPIREID_DUPLICATE, stringAttr ("inspireID"))
).shortCircuit ()
);
}
// =========================================================================
// Utilities:
// =========================================================================
static String concat (final String... s) {
if(s == null) {
return "";
}
StringBuilder stringBuilder = new StringBuilder();
for(int i = 0; i < s.length; i++) {
if(i != 0) {
stringBuilder.append(separator);
}
stringBuilder.append(s[i]);
}
return stringBuilder.toString();
}
static String concat (final Set<String> s) {
final ArrayList<String> strings = new ArrayList<String> (s);
Collections.sort (strings);
return concat (strings.toArray (new String[0]));
}
public static String stripAnchor(String string) {
int anchorLocation = string.indexOf("#");
if(anchorLocation != -1) {
return string.substring(0, anchorLocation);
}
return string;
}
protected static String pointToString(Point point) {
if(point == null) {
return "?";
}
StringBuilder stringBuilder = new StringBuilder("(");
stringBuilder.append(point.get0());
stringBuilder.append(separator);
stringBuilder.append(point.get1());
double p2 = point.get2();
if(!Double.isNaN(p2)) {
stringBuilder.append(separator);
stringBuilder.append(p2);
}
stringBuilder.append(")");
return stringBuilder.toString();
}
@SafeVarargs
static <T> boolean contains(T t, T... array) {
if(t == null || array == null) {
return false;
}
for(T current : array) {
if(current.equals(t)) {
return true;
}
}
return false;
}
// =========================================================================
// Context class:
// =========================================================================
public static class Context extends DefaultValidatorContext<MessageKey, Context> {
private final Set<String> inspireIDs = new HashSet<String> ();
private final HashMap<String, Long> checkedUrls = new HashMap<String, Long> ();
private final EtlJob job;
private final String[] validDesignations;
private boolean flag = false;
private int geometryErrorCount = 0;
private int errorCount = 0;
public Context (final CodeListFactory codeListFactory, final ValidationReporter<MessageKey, Context> reporter, final EtlJob job, final String[] validDesignations) {
super (codeListFactory, reporter);
this.validDesignations = validDesignations;
this.job = job;
}
public boolean getFlag () {
return flag;
}
public void setFlag (final boolean value) {
flag = value;
}
public boolean hasDocument (final String url) {
return checkedUrls.containsKey (url);
}
public long getDocumentSize (final String url) {
final Long value = checkedUrls.get (url);
return value == null ? 0 : value;
}
public void storeDocumentSize (final String url, final long size) {
checkedUrls.put (url, size);
}
public boolean hasInspireID (final String id) {
return inspireIDs.contains (id);
}
public void storeInspireID (final String id) {
inspireIDs.add (id);
}
public int getGeometryErrorCount() {
return geometryErrorCount;
}
public void setGeometryErrorCount(int geometryErrorCount) {
this.geometryErrorCount = geometryErrorCount;
}
public int getErrorCount() {
return errorCount;
}
public void setErrorCount(int errorCount) {
this.errorCount = errorCount;
}
public String[] getValidDesignations () {
return this.validDesignations;
}
public String getBronhouderCode () {
return job.getBronhouder ().getCode ();
}
public String getDatasetCode () {
return job.getDatasetType().getNaam ();
}
}
}