diff --git a/grails-app/controllers/uk/co/metadataconsulting/monitor/RecordCollectionController.groovy b/grails-app/controllers/uk/co/metadataconsulting/monitor/RecordCollectionController.groovy index 5964800..a9ee420 100644 --- a/grails-app/controllers/uk/co/metadataconsulting/monitor/RecordCollectionController.groovy +++ b/grails-app/controllers/uk/co/metadataconsulting/monitor/RecordCollectionController.groovy @@ -185,7 +185,7 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon } /** - * Validate a recordCollection with respect to its mapping which selects rules from the MDX. + * Validate a recordCollection with respect to its mapping, which is used to get validationRules from the MDX. * @param recordCollectionId */ def validate(Long recordCollectionId) { diff --git a/grails-app/controllers/uk/co/metadataconsulting/monitor/UrlMappings.groovy b/grails-app/controllers/uk/co/metadataconsulting/monitor/UrlMappings.groovy index 4519552..33f6893 100644 --- a/grails-app/controllers/uk/co/metadataconsulting/monitor/UrlMappings.groovy +++ b/grails-app/controllers/uk/co/metadataconsulting/monitor/UrlMappings.groovy @@ -30,9 +30,9 @@ class UrlMappings { "/records/$recordCollectionId"(controller: 'record', action: 'index') "/record/index"(controller: 'record', action: 'index') + "/records/$recordCollectionId/$recordId"(controller: 'record', action: 'show') "/record/show"(controller: 'record', action: 'show') "/record/validate"(controller: 'record', action: 'validate') - "/records/$recordCollectionId/$recordId"(controller: 'record', action: 'show') "/login/$action?/$id?(.$format)?"(controller: 'login') "/login/$action?/$id?(.$format)?"(controller: 'logout') diff --git a/grails-app/domain/uk/co/metadataconsulting/monitor/RecordPortionGormEntity.groovy b/grails-app/domain/uk/co/metadataconsulting/monitor/RecordPortionGormEntity.groovy index d9e31c9..a7499ab 100644 --- a/grails-app/domain/uk/co/metadataconsulting/monitor/RecordPortionGormEntity.groovy +++ b/grails-app/domain/uk/co/metadataconsulting/monitor/RecordPortionGormEntity.groovy @@ -11,9 +11,12 @@ class RecordPortionGormEntity { String header String name String value + + // The following fields (and the name field) get updated by a ValidationResult ValidationStatus status = ValidationStatus.NOT_VALIDATED String reason Integer numberOfRulesValidatedAgainst + Date lastUpdated static belongsTo = [record: RecordGormEntity] @@ -32,4 +35,15 @@ class RecordPortionGormEntity { reason type: 'text' sort 'header' } + + @Override + String toString() { + """ +[header: ${header}, +name: ${name}, +value: ${value}, +status: ${status}, +reason: ${reason}, +numberOfRulesValidatedAgainst: ${numberOfRulesValidatedAgainst}]""" + } } \ No newline at end of file diff --git a/grails-app/services/uk/co/metadataconsulting/monitor/RecordCollectionService.groovy b/grails-app/services/uk/co/metadataconsulting/monitor/RecordCollectionService.groovy index 025ace6..17add2d 100644 --- a/grails-app/services/uk/co/metadataconsulting/monitor/RecordCollectionService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/monitor/RecordCollectionService.groovy @@ -28,6 +28,17 @@ class RecordCollectionService implements GrailsConfigurationAware { int pageSize + /** + * Validate a record collection with respect to: + * a headers map and + * a mapping from MDX CatalogueElements (identified by GORM URL) to validation rules. + * + * Done by validating each record individually/separately. + * + * @param recordCollectionId + * @param recordPortionMappingList + * @param validationRulesMap + */ void validate(Long recordCollectionId, List recordPortionMappingList, Map validationRulesMap) { int total = recordGormService.countByRecordCollectionId(recordCollectionId) as int for (int offset = 0; offset < total; offset = (offset + pageSize)) { diff --git a/grails-app/services/uk/co/metadataconsulting/monitor/RecordService.groovy b/grails-app/services/uk/co/metadataconsulting/monitor/RecordService.groovy index c34c650..1d0a4a8 100644 --- a/grails-app/services/uk/co/metadataconsulting/monitor/RecordService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/monitor/RecordService.groovy @@ -43,11 +43,23 @@ class RecordService { @Transactional + /** + * Validate a record. + * + * Done by calling a method on each recordPortion which + * A. validates its value individually + * B. validates the whole record with validation rules relevant to that recordPortion's MDX CatalogueElement + * + * @param recordId the record being validated + * + */ void validate(Long recordId, List recordPortionMappingList, Map validationRulesMap) { DetachedCriteria query = recordGormService.queryById(recordId) query.join('portions') RecordGormEntity recordGormEntity = query.get() + log.info "ValidationRules Map: ${validationRulesMap.toString()}" for ( RecordPortionGormEntity recordPortionGormEntity : recordGormEntity.portions ) { + log.info "Validating record portion: ${recordPortionGormEntity.toString()}" ValidationResult validationResult = validateRecordPortionService.failureReason(recordPortionGormEntity, recordPortionMappingList, validationRulesMap) diff --git a/grails-app/services/uk/co/metadataconsulting/monitor/ValidateRecordPortionService.groovy b/grails-app/services/uk/co/metadataconsulting/monitor/ValidateRecordPortionService.groovy index 8735cc3..b7eac1f 100644 --- a/grails-app/services/uk/co/metadataconsulting/monitor/ValidateRecordPortionService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/monitor/ValidateRecordPortionService.groovy @@ -10,17 +10,43 @@ class ValidateRecordPortionService { DlrValidatorService dlrValidatorService ValidatorService validatorService + /** + * Uses the GORM URL of a RecordPortion, found by the recordPortionMappingList, to find the relevant set of ValidationRules + * from the validationRulesMap. + * @param recordPortion + * @param recordPortionMappingList + * @param validationRulesMap + * @return + */ ValidationRules validationRulesByRecordPortion(RecordPortionGormEntity recordPortion, List recordPortionMappingList, Map validationRulesMap) { String recordPortionGormUrl = gormUrlByRecordPortionGormEntity(recordPortionMappingList, recordPortion) validationRulesMap.get(recordPortionGormUrl) } + /** + * Method that validates a recordPortion against + * A. associated MDX CatalogueElement (found by recordPortionMappingList) + * and B. ValidationRules associated with that MDX CatalogueElement (validationRulesMap) + * + * Actually validates in context of other related recordPortion-values-cells in the same record. + * + * @param recordPortion + * @param recordPortionMappingList + * @param validationRulesMap + * @return + */ ValidationResult failureReason(RecordPortionGormEntity recordPortion, List recordPortionMappingList, Map validationRulesMap) { ValidationRules validationRules = validationRulesByRecordPortion(recordPortion, recordPortionMappingList, validationRulesMap) RecordGormUrlsAndValues recordGormUrlsAndValues = recordGormUrlsAndValuesByRecordPortion(recordPortionMappingList, recordPortion) validatorService.validate(validationRules, recordPortion.value, recordGormUrlsAndValues) } + /** + * Essentially recovers the Record from the RecordPortion... + * @param recordPortionMappingList + * @param recordPortion + * @return + */ RecordGormUrlsAndValues recordGormUrlsAndValuesByRecordPortion(List recordPortionMappingList, RecordPortionGormEntity recordPortion) { List gormUrls = [] List values = [] @@ -37,24 +63,36 @@ class ValidateRecordPortionService { }?.gormUrl } - String failureReason(ValidationRules validationRules, List gormUrls, List values) { - String reason + /** + * Method that executes the rules of "validationRules" in a Drools Rule Engine. + * The list of gormUrls and list of values should be of the same length; + * "values[i]" is the value in the cell in the column/header which is associated with the CatalogueElement identified by "gormUrls[i]". + * @param validationRules + * @param gormUrls + * @param values + * @return + */ + String executeValidationRulesWithDrools(ValidationRules validationRules, List gormUrls, List values) { + if ( !validationRules?.rules ) { - return reason + return "" } + List droolsOutputs = [] for ( ValidationRule validationRule : validationRules.rules ) { + // Create a mapping from identifiers (Drools global variables) to values to be validated. + // This makes up part of the "environment" against which the rule is executed Map m = [:] for ( String identifier : validationRule.identifiersToGormUrls.keySet() ) { m[identifier] = valuesOfGormUrl(validationRule.identifiersToGormUrls[identifier], gormUrls, values) } - reason = dlrValidatorService.validate(validationRule.name, validationRule.rule, m) - if ( reason!=null ) { - break - } + droolsOutputs.add dlrValidatorService.validate(validationRule.name, validationRule.rule, m) + } - reason + + String concatenatedOutputs = droolsOutputs.join(',\n') + return "Validation Errors from Drools: [${concatenatedOutputs}]" } int indexOfGormUrl(List gormUrls, String gormUrl) { diff --git a/grails-app/services/uk/co/metadataconsulting/monitor/ValidatorService.groovy b/grails-app/services/uk/co/metadataconsulting/monitor/ValidatorService.groovy index 0b9158e..ac270ab 100644 --- a/grails-app/services/uk/co/metadataconsulting/monitor/ValidatorService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/monitor/ValidatorService.groovy @@ -8,19 +8,50 @@ import uk.co.metadataconsulting.monitor.modelcatalogue.ValidationRules class ValidatorService { ValidateRecordPortionService validateRecordPortionService + /** + * Validates a value against a validatingImpl + * and also validates the whole record that it comes from (represented by RecordGormUrlsAndValues), + * against a set of (Drools) validationRules. + * + * @param validationRules a set of validation rules, associated with the MDX Catalogue Element, associated with the value to be validated + * @param value value to be validated + * @param recordGormUrlsAndValues + * @return + */ ValidationResult validate(ValidationRules validationRules, String value, RecordGormUrlsAndValues recordGormUrlsAndValues) { + List gormUrls = recordGormUrlsAndValues.gormUrls List values = recordGormUrlsAndValues.values - String reason = validateRecordPortionService.failureReason(validationRules, gormUrls, values) + + // Validate against Drools Validation Rules + String droolsOutput = validateRecordPortionService.executeValidationRulesWithDrools(validationRules, gormUrls, values) + String name = validationRules?.name Integer numberOfRulesValidatedAgainst = validationRules?.rules?.size() ?: 0 + String validatingImplOutput = "" + // Validate against the "ValidatingImpl" if ( validationRules?.validating ) { if ( !ValueValidator.validateRule(validationRules.validating, value) ) { - reason = reason ?: validationRules.validating.toString() + validatingImplOutput = validationRules.validating.toString() } numberOfRulesValidatedAgainst++ } + String reason = "" + if (droolsOutput) { + if (validatingImplOutput) { + reason = "${droolsOutput}, Groovy Rule: [${validatingImplOutput}]" + } + else { + reason = droolsOutput + } + + } + else if (validatingImplOutput) { + reason = validatingImplOutput + } + + ValidationStatus status = ValidationStatus.NOT_VALIDATED if ( numberOfRulesValidatedAgainst ) { status = reason ? ValidationStatus.INVALID : ValidationStatus.VALID diff --git a/src/main/groovy/metadata/Validation.java b/src/main/groovy/metadata/Validation.java index 72cd726..76b950e 100644 --- a/src/main/groovy/metadata/Validation.java +++ b/src/main/groovy/metadata/Validation.java @@ -13,15 +13,40 @@ public class Validation { - public static int yearsBetween(Date last, Date first) { - Calendar a = getCalendar(first); - Calendar b = getCalendar(last); - int diff = b.get(YEAR) - a.get(YEAR); - if (a.get(MONTH) > b.get(MONTH) || - (a.get(MONTH) == b.get(MONTH) && a.get(DATE) > b.get(DATE))) { - diff--; + /** + * Returns the difference in years between "last" and "first" (last-first), + * rounded DOWN (if negative, closer to 0) to the year. + * If the result is non-zero, then positive and negative are determined thus: + * If "last" is actually after "first", the result will be positive; + * otherwise negative. + * + * @param supposedlyLast + * @param supposedlyFirst + * @return + */ + public static int yearsBetween(Date supposedlyLast, Date supposedlyFirst) { + int lastFirstComparison = supposedlyLast.compareTo(supposedlyFirst); // 1 if last > first, 0 if last == first, -1 if last < first + Calendar earlier = null; + Calendar later = null; + + if (lastFirstComparison > 0) { + earlier = getCalendar(supposedlyFirst); + later = getCalendar(supposedlyLast); + } + else { + earlier = getCalendar(supposedlyLast); + later = getCalendar(supposedlyFirst); } - return diff; + + int diff = later.get(YEAR) - earlier.get(YEAR); + if (diff != 0) { + if (earlier.get(MONTH) > later.get(MONTH) || + (earlier.get(MONTH) == later.get(MONTH) && earlier.get(DATE) > later.get(DATE))) { + diff--; // rounding down to the nearest year + } + } + + return diff * lastFirstComparison; // multiply by comparison to get the right sign (plus or minus) } diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/DlrValidator.java b/src/main/groovy/uk/co/metadataconsulting/monitor/DlrValidator.java index ada1b11..fbb26f9 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/DlrValidator.java +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/DlrValidator.java @@ -65,7 +65,7 @@ protected KieBase loadKnowledgeBaseFromString( KnowledgeBuilderConfiguration con } if (kbuilder.hasErrors()) { - //log.error(kbuilder.getErrors().toString()); + log.error("Errors compiling rules. Rules: " + drlContentStrings.toString() + "\nErrors:" + kbuilder.getErrors().toString()); } if (kBaseConfig == null) { kBaseConfig = KnowledgeBaseFactory.newKnowledgeBaseConfiguration(); diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/RecordGormUrlsAndValues.groovy b/src/main/groovy/uk/co/metadataconsulting/monitor/RecordGormUrlsAndValues.groovy index 9d973a6..239e9c3 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/RecordGormUrlsAndValues.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/RecordGormUrlsAndValues.groovy @@ -3,6 +3,12 @@ package uk.co.metadataconsulting.monitor import groovy.transform.CompileStatic @CompileStatic +/** + * Partially represents a Record as a list of gormUrls and values. + * gormUrls[i] and values[i] should correspond to each other. + * gormUrls[i] is the GORM URL of the MDX Catalogue Element, associated with the column-header, + * under which is the cell, from which values[i] came. + */ class RecordGormUrlsAndValues { List gormUrls List values diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortion.groovy b/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortion.groovy index 2b14890..e17f940 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortion.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortion.groovy @@ -5,6 +5,9 @@ import groovy.transform.CompileStatic @Canonical @CompileStatic +/** + * non-GORM version of RecordPortionGormEntity (i.e. a "Cell" in a "Table") + */ class RecordPortion { String header String name diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortionMapping.groovy b/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortionMapping.groovy index 1009265..c931eeb 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortionMapping.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/RecordPortionMapping.groovy @@ -7,13 +7,27 @@ import uk.co.metadataconsulting.monitor.modelcatalogue.GormUrlName @ToString @CompileStatic /** - * Non-GORM representation of RecordCollectionMappingEntryGormEntity. + * Non-GORM representation (i.e. class not in the Grails domain) of RecordCollectionMappingEntryGormEntity. + * Represents a mapping from a header to a MDX CatalogueElement (identified by gormUrl). + * Actually a bit poorly named since it's more to do with a "column (header)" than individual RecordPortions (cells). */ class RecordPortionMapping { Long id + /** + * Header for the RecordCollection/DataSet + */ String header + /** + * Identifies the associated MDX CatalogueElement + */ String gormUrl + /** + * Name of the associated MDX CatalogueElement + */ String name + /** + * Combines gormURL and name. This is sort of a hack for the front-end Javascript selection/search library to work properly + */ String combinedGormUrlName static RecordPortionMapping of(RecordCollectionMappingEntryGormEntity gormEntity) { diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/ValidationResult.groovy b/src/main/groovy/uk/co/metadataconsulting/monitor/ValidationResult.groovy index 5b2d7e3..2c882f9 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/ValidationResult.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/ValidationResult.groovy @@ -3,8 +3,17 @@ package uk.co.metadataconsulting.monitor import groovy.transform.CompileStatic @CompileStatic +/** + * The result of a validation process + */ class ValidationResult { + /** + * Represents the reason validation failed + */ String reason + /** + * Name of the MDX CatalogueElement being validated + */ String name int numberOfRulesValidatedAgainst = 0 ValidationStatus status = ValidationStatus.NOT_VALIDATED diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRule.groovy b/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRule.groovy index 1bbb7ad..ba803a3 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRule.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRule.groovy @@ -5,8 +5,30 @@ import groovy.transform.CompileStatic @Canonical @CompileStatic +/** + * Representation of a Drools Rule, which are Validation Rules/Business Rules in the MDX + */ class ValidationRule { + /** + * Name of the ValidationRule + */ String name + /** + * Mapping from the identifiers (global variables used in Drools Rules) to GORM URLs (which identify CatalogueElements in the MDX) + */ Map identifiersToGormUrls + /** + * Executable Drools Rule script + */ String rule + + @Override + public String toString() { + """ +ValidationRule[ +name: ${name}, +identifiersToGormUrls: ${identifiersToGormUrls.toString()}, +rule: ${rule} +]""" + } } diff --git a/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRules.groovy b/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRules.groovy index e98e4cc..3be0168 100644 --- a/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRules.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/monitor/modelcatalogue/ValidationRules.groovy @@ -4,11 +4,42 @@ import groovy.transform.CompileStatic import org.modelcatalogue.core.scripting.ValidatingImpl @CompileStatic +/** + * An MDX CatalogueElement with A. an associated set of ValidationRules (used in Drools Engine), + * plus B. a "ValidatingImpl" (another kind of rule... Groovy script) + */ class ValidationRules { + /** + * URL of MDX CatalogueElement + */ String url + /** + * GORM URL of MDX CatalogueElement + */ String gormUrl + /** + * Name of MDX CatalogueElement + */ String name + /** + * Associated ValidationRules (Drools Rules) + */ List rules = [] + /** + * Another kind of validation rule. A Groovy Script. + */ ValidatingImpl validating + + @Override + String toString() { + String rulesString = rules.collect{it.toString()}.join(',\n') + """ +ValidationRules[url: ${url}, +gormUrl: ${gormUrl}, +name: ${name}, +rules: [${rulesString}], +validating: ${validating.toString()} +""" + } }