Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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}]"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordPortionMapping> recordPortionMappingList, Map<String, ValidationRules> validationRulesMap) {
int total = recordGormService.countByRecordCollectionId(recordCollectionId) as int
for (int offset = 0; offset < total; offset = (offset + pageSize)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordPortionMapping> recordPortionMappingList, Map<String, ValidationRules> validationRulesMap) {
DetachedCriteria<RecordGormEntity> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordPortionMapping> recordPortionMappingList, Map<String, ValidationRules> 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<RecordPortionMapping> recordPortionMappingList, Map<String, ValidationRules> 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<RecordPortionMapping> recordPortionMappingList, RecordPortionGormEntity recordPortion) {
List<String> gormUrls = []
List<String> values = []
Expand All @@ -37,24 +63,36 @@ class ValidateRecordPortionService {
}?.gormUrl
}

String failureReason(ValidationRules validationRules, List<String> gormUrls, List<String> 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<String> gormUrls, List<String> values) {

if ( !validationRules?.rules ) {
return reason
return ""
}
List<String> 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<String> gormUrls, String gormUrl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> gormUrls = recordGormUrlsAndValues.gormUrls
List<String> 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
Expand Down
41 changes: 33 additions & 8 deletions src/main/groovy/metadata/Validation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> gormUrls
List<String> values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> identifiersToGormUrls
/**
* Executable Drools Rule script
*/
String rule

@Override
public String toString() {
"""
ValidationRule[
name: ${name},
identifiersToGormUrls: ${identifiersToGormUrls.toString()},
rule: ${rule}
]"""
}
}
Loading