Skip to content

[NAE-1770] Validation register frontend #293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: release/8.0.0
Choose a base branch
from
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 @@ -35,6 +35,7 @@ import com.netgrif.application.engine.rules.domain.RuleRepository
import com.netgrif.application.engine.startup.DefaultFiltersRunner
import com.netgrif.application.engine.startup.FilterRunner
import com.netgrif.application.engine.utils.FullPageRequest
import com.netgrif.application.engine.validations.interfaces.IValidationService
import com.netgrif.application.engine.workflow.domain.*
import com.netgrif.application.engine.workflow.domain.eventoutcomes.EventOutcome
import com.netgrif.application.engine.workflow.domain.eventoutcomes.caseoutcomes.CreateCaseEventOutcome
Expand Down Expand Up @@ -177,6 +178,9 @@ class ActionDelegate /*TODO: release/8.0.0: implements ActionAPI*/ {
@Autowired
PublicViewProperties publicViewProperties

@Autowired
IValidationService validationService

FrontendActionOutcome Frontend

/**
Expand Down Expand Up @@ -1084,8 +1088,8 @@ class ActionDelegate /*TODO: release/8.0.0: implements ActionAPI*/ {
}
}

Validation validation(String rule, I18nString message) {
return new Validation(rule, message)
Validation validation(String name, Arguments clientArguments, Arguments serverArguments, I18nString message) {
return new Validation(name, clientArguments, serverArguments, message)
}

// TODO: release/8.0.0 remove?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.netgrif.application.engine.petrinet.domain.dataset.logic.action


import com.netgrif.application.engine.petrinet.domain.dataset.*
import groovy.util.logging.Slf4j

import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

@Slf4j
class ValidationDelegate {

public static final String FUTURE = 'future'
public static final String TODAY = 'today'
public static final String PAST = 'past'
public static final String NOW = 'now'
public static final String INF = 'inf'
public static final String TEL_NUMBER_REGEX = '^(?:\\+?(\\d{1,3}))?([-. (]*(\\d{3})[-. )]*)?((\\d{3})[-. ]*(\\d{2,4})(?:[-.x ]*(\\d+))?)$'
public static final String EMAIL_REGEX = '^[a-zA-Z0-9\\._\\%\\+\\-]+@[a-zA-Z0-9\\.\\-]+\\.[a-zA-Z]{2,}$'

// todo NAE-1788: thisField keyword
Field<?> thisField

Boolean notEmpty() { return thisField.rawValue != null }

// boolean field validations
Boolean requiredTrue() { return thisField instanceof BooleanField && notEmpty() && thisField.rawValue == true }

// date field validations
Boolean between(def from, def to) {
if (!(thisField instanceof DateField || thisField instanceof DateTimeField)) {
return false
}

LocalDateTime updateDate_TODAY = thisField instanceof DateField ? LocalDate.now().atStartOfDay() : LocalDateTime.now()
LocalDateTime thisFieldValue = thisField.rawValue instanceof LocalDateTime ? thisField.rawValue : thisField.rawValue.atStartOfDay()

def fromDate = from
if (from instanceof String) {
LocalDate parsedDate = parseStringToLocalDate(from)
fromDate = parsedDate ? parsedDate.atStartOfDay() : from
}

def toDate = to
if (to instanceof String) {
LocalDate parsedDate = parseStringToLocalDate(to)
toDate = parsedDate ? parsedDate.atStartOfDay() : to
}

if ((fromDate == TODAY || fromDate == NOW) && toDate == FUTURE) {
if (thisFieldValue < updateDate_TODAY) {
return false
}
} else if (fromDate == PAST && (toDate == TODAY || toDate == NOW)) {
if (thisFieldValue > updateDate_TODAY) {
return false
}
} else if (fromDate == PAST && (toDate instanceof LocalDateTime)) {
if (thisFieldValue > toDate) {
return false
}
} else if (fromDate == TODAY && (toDate instanceof LocalDateTime)) {
if (thisFieldValue > toDate || thisFieldValue < updateDate_TODAY) {
return false
}
} else if ((fromDate instanceof LocalDateTime) && toDate == TODAY) {
if (thisFieldValue < fromDate || thisFieldValue > updateDate_TODAY) {
return false
}
} else if (toDate == FUTURE && (fromDate instanceof LocalDateTime)) {
if (thisFieldValue < fromDate) {
return false
}
} else if ((fromDate instanceof LocalDateTime) && (toDate instanceof LocalDateTime)) {
if (thisFieldValue > toDate || thisFieldValue < fromDate) {
return false
}
}
return true
}

Boolean workday() { return (thisField instanceof DateField || thisField instanceof DateTimeField) && notEmpty() && !thisField.rawValue.dayOfWeek.isWeekend() }


Boolean weekend() { return (thisField instanceof DateField || thisField instanceof DateTimeField) && notEmpty() && thisField.rawValue.dayOfWeek.isWeekend() }

protected static LocalDate parseStringToLocalDate(String stringDate) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? Can be used DateUtils.parseDate() instead?

if (stringDate == null) {
return null
}
List<String> patterns = Arrays.asList("dd.MM.yyyy", "")
try {
return LocalDate.parse(stringDate, DateTimeFormatter.BASIC_ISO_DATE)
} catch (DateTimeParseException ignored) {
try {
return LocalDate.parse(stringDate, DateTimeFormatter.ISO_DATE)
} catch (DateTimeParseException ignored2) {
for (String pattern : patterns) {
try {
return LocalDateTime.parse(stringDate, DateTimeFormatter.ofPattern(pattern))
} catch (DateTimeParseException | IllegalArgumentException ignored3) {
}
}
}
}
return null
}

// number field validations
Boolean odd() { return thisField instanceof NumberField && notEmpty() && thisField.rawValue as Double % 2 != 0 }

Boolean even() { return thisField instanceof NumberField && notEmpty() && thisField.rawValue as Double % 2 == 0 }

Boolean positive() { return thisField instanceof NumberField && notEmpty() && thisField.rawValue >= 0 }

Boolean negative() { return thisField instanceof NumberField && notEmpty() && thisField.rawValue <= 0 }

Boolean decimal() { return thisField instanceof NumberField && notEmpty() && thisField.rawValue as Double % 1 == 0 }

Boolean inrange(def from, def to) {

if (from instanceof String && from.toLowerCase() == INF) {
from = Double.MIN_VALUE
}

if (to instanceof String && to.toLowerCase() == INF) {
to = Double.MAX_VALUE
}
return thisField instanceof NumberField && notEmpty() && thisField.rawValue >= from as Double && thisField.rawValue <= to as Double
}

// text field validations
Boolean regex(String pattern) { return thisField instanceof TextField && notEmpty() && thisField.rawValue ==~ pattern }

Boolean minLength(Integer minLength) { return thisField instanceof TextField && notEmpty() && (thisField.rawValue as String).length() >= minLength }

Boolean maxLength(Integer maxLength) { return thisField instanceof TextField && notEmpty() && (thisField.rawValue as String).length() <= maxLength }

Boolean telNumber() { return regex(TEL_NUMBER_REGEX) }

Boolean email() { return regex(EMAIL_REGEX) }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.netgrif.application.engine.petrinet.domain.dataset.logic.action

import com.netgrif.application.engine.event.IGroovyShellFactory
import com.netgrif.application.engine.petrinet.domain.dataset.Field
import com.netgrif.application.engine.petrinet.domain.dataset.Validation
import com.netgrif.application.engine.validations.ValidationRegistry
import com.netgrif.application.engine.workflow.domain.Case
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Lookup
import org.springframework.stereotype.Component

@Slf4j
@Component
abstract class ValidationExecutioner {

@Lookup("validationDelegate")
abstract ValidationDelegate getValidationDelegate()

@Autowired
private ValidationRegistry registry;

@Autowired
private IGroovyShellFactory shellFactory

void execute(Case useCase, Field<?> field, List<Validation> validations) {
if (!validations) {
return
}

log.info("Validations: ${validations.collect { it.name }}")

ValidationDelegate delegate = initDelegate(useCase, field, validations.collect { it.name })
validations.each { validation ->
runValidation(field, validation, delegate)
}
}

protected void runValidation(Field<?> field, Validation validation, ValidationDelegate delegate) {
if (field.rawValue == null) {
return
}

Closure<Boolean> code = initCode(validation, delegate)
if (!code()) {
throw new IllegalArgumentException(validation.message.toString())
}
}

protected Closure<Boolean> getValidationCode(String validationName) {
return this.registry.getValidation(validationName)
}

protected static String escapeSpecialCharacters(String s){
return s.replace('\\', '\\\\')
.replace('\'', '\\\'')
}

protected Closure<Boolean> initCode(Validation validation, ValidationDelegate delegate) {
List<String> argumentList = []
if (validation.serverArguments != null) {
argumentList = validation.serverArguments.argument.collect { it.isDynamic ? it.value : "'${escapeSpecialCharacters(it.value)}'" }
}
String validationCall = "${validation.name}(${argumentList.join(", ")})"
Closure<Boolean> code = this.shellFactory.getGroovyShell().evaluate("{ -> return " + validationCall + " }") as Closure<Boolean>
return code.rehydrate(delegate, code.owner, code.thisObject)
}

protected ValidationDelegate initDelegate(Case useCase, Field<?> thisField, List<String> validationNames) {
ValidationDelegate delegate = getValidationDelegate()
delegate.metaClass.useCase = useCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't useCase as proper attribute of ValidationDelegate?

useCase.dataSet.fields.values().forEach { Field<?> field ->
delegate.metaClass."$field.importId" = field
}

validationNames = filterConflictedValidationNames(useCase, validationNames)
validationNames.each { validationName ->
delegate.metaClass."$validationName" = getValidationCode(validationName)
}

delegate.thisField = thisField
return delegate
}

private static List<String> filterConflictedValidationNames(Case useCase, List<String> validationNames) {
List<String> fieldNames = useCase.dataSet.fields.keySet() as List<String>
fieldNames.retainAll(validationNames)
if (!fieldNames.isEmpty()) {
log.warn("Ignoring validations {} for case [{}]: field names are identical with validation names", fieldNames, useCase.stringId)
validationNames -= fieldNames
}
return validationNames
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can reduce this to:

List<String> fieldNames = useCase.dataSet.fields.keySet() as List<String>
return validationNames.removeAll(fieldNames)

Disadvantage is, we cannot have the same log message anymore. So the question is how important is the log message.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class RunnerController {
MailRunner,
DemoRunner,
QuartzSchedulerRunner,
ValidationRunner,
FinisherRunnerSuperCreator,
FinisherRunner,
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.netgrif.application.engine.startup

import com.netgrif.application.engine.auth.service.interfaces.IUserService
import com.netgrif.application.engine.validations.interfaces.IValidationService
import com.netgrif.application.engine.workflow.domain.Case
import com.netgrif.application.engine.workflow.domain.QCase
import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository
import com.querydsl.core.types.Predicate
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Component

@Slf4j
@Component
class ValidationRunner extends AbstractOrderedCommandLineRunner {

private static final int PAGE_SIZE = 100
public static final String VALIDATION_FILE_NAME = "engine-processes/validations/validation.xml"
public static final String VALIDATION_PETRI_NET_IDENTIFIER = "validation"
public static final String VALIDATION_ACTIVE_PLACE_ID = "active"
public static final String VALIDATION_NAME_FIELD_ID = "name"
public static final String VALIDATION_GROOVY_DEFINITION_FIELD_ID = "validation_definition_groovy"

@Autowired
private ImportHelper helper

@Autowired
private IUserService userService

@Autowired
private IValidationService validationService

@Autowired
private CaseRepository caseRepository
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use IWorkflowService's bean instead of direct interaction with repository


@Override
void run(String... strings) throws Exception {
log.info("Starting validation runner")

helper.upsertNet(VALIDATION_FILE_NAME, VALIDATION_PETRI_NET_IDENTIFIER)
Predicate predicate = QCase.case$.processIdentifier.eq(VALIDATION_PETRI_NET_IDENTIFIER) & QCase.case$.activePlaces.get(VALIDATION_ACTIVE_PLACE_ID).isNotNull()
long numberActiveValidations = caseRepository.count(predicate)
int pageCount = (int) (numberActiveValidations / PAGE_SIZE) + 1
pageCount.times { pageNum ->
caseRepository.findAll(predicate, PageRequest.of(pageNum, PAGE_SIZE))
.getContent()
.each { Case validationCase ->
validationService.registerValidation(
validationCase.getDataSet().get(VALIDATION_NAME_FIELD_ID).rawValue as String,
validationCase.getDataSet().get(VALIDATION_GROOVY_DEFINITION_FIELD_ID).rawValue as String
)
}
}

log.info("Validation runner finished, [{}] validations successfully imported", numberActiveValidations)
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading