Skip to content

Commit 5ca8004

Browse files
Adds migration and column for doc_type_label and fixes virus scanning logic (#424)
* Adds migration and column for doc type label for user_files table. * Fixes an issue with virus scanning if using the noop scanner (would always say the files was scanned even if feature was disabled.) * Creates annotation for @DynamicField and some tests for that work. * Adds dynamic field handling for validation * Updates readme Co-Authored-By: Lauren Kemperman; [email protected]
1 parent a8626c9 commit 5ca8004

31 files changed

+536
-151
lines changed

README.md

+175-44
Large diffs are not rendered by default.

src/main/java/formflow/library/FileController.java

+22-12
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ public class FileController extends FormFlowController {
6060
private final String SESSION_USERFILES_KEY = "userFiles";
6161
private final Integer maxFiles;
6262

63+
@Value("${form-flow.uploads.default-doc-type-label:#{null}}")
64+
private String defaultDocType;
65+
66+
@Value("${form-flow.uploads.virus-scanning.enabled:false}")
67+
private boolean isVirusScanningEnabled;
68+
6369
private final ObjectMapper objectMapper = new ObjectMapper();
6470

6571
public FileController(
@@ -123,19 +129,22 @@ public ResponseEntity<?> upload(
123129
return new ResponseEntity<>(message, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
124130
}
125131

126-
boolean wasScannedForVirus = true;
127-
try {
128-
if (fileVirusScanner.virusDetected(file)) {
129-
String message = messageSource.getMessage("upload-documents.error-virus-found", null, locale);
130-
return new ResponseEntity<>(message, HttpStatus.UNPROCESSABLE_ENTITY);
131-
}
132-
} catch (WebClientResponseException | TimeoutException e) {
133-
if (blockIfClammitUnreachable) {
134-
log.error("The virus scan service could not be reached. Blocking upload.");
135-
String message = messageSource.getMessage("upload-documents.error-virus-scanner-unavailable", null, locale);
136-
return new ResponseEntity<>(message, HttpStatus.SERVICE_UNAVAILABLE);
132+
boolean wasScannedForVirus = false;
133+
if (isVirusScanningEnabled) {
134+
try {
135+
if (fileVirusScanner.virusDetected(file)) {
136+
String message = messageSource.getMessage("upload-documents.error-virus-found", null, locale);
137+
return new ResponseEntity<>(message, HttpStatus.UNPROCESSABLE_ENTITY);
138+
}
139+
wasScannedForVirus = true;
140+
} catch (WebClientResponseException | TimeoutException e) {
141+
wasScannedForVirus = false;
142+
if (blockIfClammitUnreachable) {
143+
log.error("The virus scan service could not be reached. Blocking upload.");
144+
String message = messageSource.getMessage("upload-documents.error-virus-scanner-unavailable", null, locale);
145+
return new ResponseEntity<>(message, HttpStatus.SERVICE_UNAVAILABLE);
146+
}
137147
}
138-
wasScannedForVirus = false;
139148
}
140149

141150
if (fileValidationService.isTooLarge(file)) {
@@ -174,6 +183,7 @@ public ResponseEntity<?> upload(
174183
.filesize((float) file.getSize())
175184
.mimeType(file.getContentType())
176185
.virusScanned(wasScannedForVirus)
186+
.docTypeLabel(defaultDocType)
177187
.build();
178188

179189
uploadedFile = userFileRepositoryService.save(uploadedFile);

src/main/java/formflow/library/ScreenController.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import formflow.library.data.SubmissionRepositoryService;
1515
import formflow.library.data.UserFileRepositoryService;
1616
import formflow.library.file.FileValidationService;
17-
import formflow.library.inputs.UnvalidatedField;
1817
import jakarta.servlet.http.HttpServletRequest;
1918
import jakarta.servlet.http.HttpSession;
2019
import java.io.IOException;
@@ -43,6 +42,8 @@
4342
import org.springframework.web.servlet.ModelAndView;
4443
import org.springframework.web.servlet.view.RedirectView;
4544

45+
import static formflow.library.inputs.FieldNameMarkers.UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS;
46+
4647
/**
4748
* A controller to render any screen in flows, including subflows.
4849
*/
@@ -686,7 +687,7 @@ private void handleAddressValidation(Submission submission, FormSubmission formS
686687
formSubmission.setValidatedAddress(validatedAddresses);
687688
// clear lingering address(es) from the submission stored in the database.
688689
formSubmission.getAddressValidationFields().forEach(item -> {
689-
String inputName = item.replace(UnvalidatedField.VALIDATE_ADDRESS, "");
690+
String inputName = item.replace(UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS, "");
690691
submission.clearAddressFields(inputName);
691692
});
692693
}

src/main/java/formflow/library/ValidationService.java

+39-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import jakarta.validation.constraints.NotBlank;
99
import jakarta.validation.constraints.NotEmpty;
1010
import jakarta.validation.constraints.NotNull;
11+
1112
import java.util.ArrayList;
1213
import java.util.Arrays;
1314
import java.util.Collections;
@@ -17,7 +18,9 @@
1718
import lombok.extern.slf4j.Slf4j;
1819
import org.springframework.beans.factory.annotation.Value;
1920
import org.springframework.stereotype.Service;
20-
import org.springframework.util.StringUtils;
21+
import org.apache.commons.lang3.StringUtils;
22+
23+
import static formflow.library.inputs.FieldNameMarkers.DYNAMIC_FIELD_MARKER;
2124

2225
/**
2326
* A service that validates flow inputs based on input definition.
@@ -45,8 +48,8 @@ public class ValidationService {
4548
/**
4649
* Autoconfigured constructor.
4750
*
48-
* @param validator Validator from Jakarta package.
49-
* @param actionManager the <code>ActionManager</code> that manages the logic to be run at specific points
51+
* @param validator Validator from Jakarta package.
52+
* @param actionManager the <code>ActionManager</code> that manages the logic to be run at specific points
5053
* @param inputConfigPath the package path where inputs classes are located
5154
*/
5255
public ValidationService(Validator validator, ActionManager actionManager,
@@ -95,18 +98,48 @@ private Map<String, List<String>> performFieldLevelValidation(String flowName, F
9598
}
9699

97100
formSubmission.getFormData().forEach((key, value) -> {
101+
boolean dynamicField = false;
98102
var messages = new ArrayList<String>();
99103
List<String> annotationNames = null;
100104

101105
if (key.contains("[]")) {
102106
key = key.replace("[]", "");
103107
}
104108

109+
String originalKey = key;
110+
111+
if (key.contains(DYNAMIC_FIELD_MARKER)) {
112+
dynamicField = true;
113+
key = StringUtils.substringBefore(key, DYNAMIC_FIELD_MARKER);
114+
}
115+
105116
try {
106117
annotationNames = Arrays.stream(flowClass.getDeclaredField(key).getDeclaredAnnotations())
107118
.map(annotation -> annotation.annotationType().getName()).toList();
108119
} catch (NoSuchFieldException e) {
109-
throw new RuntimeException(e);
120+
if (dynamicField) {
121+
throw new RuntimeException(
122+
String.format(
123+
"Input field '%s' has dynamic field marker '%s' in its name, but we are unable to " +
124+
"find the field in the input file. Is it a dynamic field?",
125+
originalKey, DYNAMIC_FIELD_MARKER
126+
)
127+
);
128+
} else {
129+
throw new RuntimeException(e);
130+
}
131+
}
132+
133+
// if it's acting like a dynamic field, then ensure that it is marked as one
134+
if (dynamicField) {
135+
if (!annotationNames.contains("formflow.library.data.annotations.DynamicField")) {
136+
throw new RuntimeException(
137+
String.format(
138+
"Field name '%s' (field: '%s') acts like it's a dynamic field, but the field does not contain the @DynamicField annotation",
139+
key, originalKey
140+
)
141+
);
142+
}
110143
}
111144

112145
if (Collections.disjoint(annotationNames, requiredAnnotationsList) && value.equals("")) {
@@ -118,7 +151,8 @@ private Map<String, List<String>> performFieldLevelValidation(String flowName, F
118151
.forEach(violation -> messages.add(violation.getMessage()));
119152

120153
if (!messages.isEmpty()) {
121-
validationMessages.put(key, messages);
154+
// uses original key to accommodate dynamic input names
155+
validationMessages.put(originalKey, messages);
122156
}
123157
});
124158

src/main/java/formflow/library/address_validation/ValidationRequestFactory.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import com.smartystreets.api.us_street.Batch;
66
import com.smartystreets.api.us_street.Lookup;
77
import formflow.library.data.FormSubmission;
8-
import formflow.library.inputs.UnvalidatedField;
98
import formflow.library.inputs.AddressParts;
109
import java.util.List;
1110
import lombok.extern.slf4j.Slf4j;
1211
import org.springframework.stereotype.Component;
1312

13+
import static formflow.library.inputs.FieldNameMarkers.UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS;
14+
1415
@Slf4j
1516
@Component
1617
public class ValidationRequestFactory {
@@ -21,8 +22,11 @@ public ValidationRequestFactory() {
2122
public Batch create(FormSubmission formSubmission) {
2223
Batch smartyBatch = new Batch();
2324
List<String> addressInputNames = formSubmission.getFormData().keySet().stream()
24-
.filter(key -> key.startsWith(UnvalidatedField.VALIDATE_ADDRESS) && formSubmission.getFormData().get(key).equals("true"))
25-
.map(key -> key.substring(UnvalidatedField.VALIDATE_ADDRESS.length())).toList();
25+
.filter(key ->
26+
key.startsWith(UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS.toString()) &&
27+
formSubmission.getFormData().get(key).equals("true")
28+
)
29+
.map(key -> key.substring(UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS.toString().length())).toList();
2630

2731
addressInputNames.forEach(inputName -> {
2832
Lookup lookup = new Lookup();

src/main/java/formflow/library/data/FormSubmission.java

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package formflow.library.data;
22

33
import formflow.library.address_validation.ValidatedAddress;
4-
import formflow.library.inputs.UnvalidatedField;
54
import formflow.library.inputs.AddressParts;
65
import java.util.ArrayList;
76
import java.util.List;
@@ -11,11 +10,18 @@
1110
import lombok.Data;
1211
import org.springframework.util.MultiValueMap;
1312

13+
import static formflow.library.inputs.FieldNameMarkers.UNVALIDATED_FIELD_MARKER_CSRF;
14+
import static formflow.library.inputs.FieldNameMarkers.UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS;
15+
import static formflow.library.inputs.FieldNameMarkers.UNVALIDATED_FIELD_MARKER_VALIDATED;
16+
1417
@Data
1518
public class FormSubmission {
1619

1720
public Map<String, Object> formData;
18-
private List<String> unvalidatedFields = List.of(UnvalidatedField.CSRF, UnvalidatedField.VALIDATE_ADDRESS);
21+
private List<String> unvalidatedFields = List.of(
22+
UNVALIDATED_FIELD_MARKER_CSRF,
23+
UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS
24+
);
1925

2026
public FormSubmission(MultiValueMap<String, String> formData) {
2127
this.formData = removeEmptyValuesAndFlatten(formData);
@@ -54,19 +60,19 @@ public Map<String, Object> getValidatableFields() {
5460
*/
5561
public List<String> getAddressValidationFields() {
5662
return formData.entrySet().stream()
57-
.filter(entry -> entry.getKey().startsWith(UnvalidatedField.VALIDATE_ADDRESS))
63+
.filter(entry -> entry.getKey().startsWith(UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS))
5864
.filter(entry -> entry.getValue().toString().equalsIgnoreCase("true"))
5965
.map(Entry::getKey).toList();
6066
}
6167

6268
public void setValidatedAddress(Map<String, ValidatedAddress> validatedAddresses) {
6369
validatedAddresses.forEach((key, value) -> {
6470
if (value != null) {
65-
formData.put(key + AddressParts.STREET_ADDRESS_1 + UnvalidatedField.VALIDATED, value.getStreetAddress());
66-
formData.put(key + AddressParts.STREET_ADDRESS_2 + UnvalidatedField.VALIDATED, value.getApartmentNumber());
67-
formData.put(key + AddressParts.CITY + UnvalidatedField.VALIDATED, value.getCity());
68-
formData.put(key + AddressParts.STATE + UnvalidatedField.VALIDATED, value.getState());
69-
formData.put(key + AddressParts.ZIPCODE + UnvalidatedField.VALIDATED, value.getZipCode());
71+
formData.put(key + AddressParts.STREET_ADDRESS_1 + UNVALIDATED_FIELD_MARKER_VALIDATED, value.getStreetAddress());
72+
formData.put(key + AddressParts.STREET_ADDRESS_2 + UNVALIDATED_FIELD_MARKER_VALIDATED, value.getApartmentNumber());
73+
formData.put(key + AddressParts.CITY + UNVALIDATED_FIELD_MARKER_VALIDATED, value.getCity());
74+
formData.put(key + AddressParts.STATE + UNVALIDATED_FIELD_MARKER_VALIDATED, value.getState());
75+
formData.put(key + AddressParts.ZIPCODE + UNVALIDATED_FIELD_MARKER_VALIDATED, value.getZipCode());
7076
}
7177
});
7278
}

src/main/java/formflow/library/data/Submission.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package formflow.library.data;
22

3+
import static formflow.library.inputs.FieldNameMarkers.UNVALIDATED_FIELD_MARKER_VALIDATED;
34
import static jakarta.persistence.TemporalType.TIMESTAMP;
45

56
import formflow.library.inputs.AddressParts;
6-
import formflow.library.inputs.UnvalidatedField;
77
import io.hypersistence.utils.hibernate.type.json.JsonType;
88
import jakarta.persistence.Column;
99
import jakarta.persistence.Entity;
@@ -203,11 +203,11 @@ public Boolean getIterationIsCompleteStatus(String subflow, String iterationUuid
203203
*/
204204
public void clearAddressFields(String inputName) {
205205
// we want to clear out
206-
inputData.remove(inputName + AddressParts.STREET_ADDRESS_1 + UnvalidatedField.VALIDATED);
207-
inputData.remove(inputName + AddressParts.STREET_ADDRESS_2 + UnvalidatedField.VALIDATED);
208-
inputData.remove(inputName + AddressParts.CITY + UnvalidatedField.VALIDATED);
209-
inputData.remove(inputName + AddressParts.STATE + UnvalidatedField.VALIDATED);
210-
inputData.remove(inputName + AddressParts.ZIPCODE + UnvalidatedField.VALIDATED);
206+
inputData.remove(inputName + AddressParts.STREET_ADDRESS_1 + UNVALIDATED_FIELD_MARKER_VALIDATED);
207+
inputData.remove(inputName + AddressParts.STREET_ADDRESS_2 + UNVALIDATED_FIELD_MARKER_VALIDATED);
208+
inputData.remove(inputName + AddressParts.CITY + UNVALIDATED_FIELD_MARKER_VALIDATED);
209+
inputData.remove(inputName + AddressParts.STATE + UNVALIDATED_FIELD_MARKER_VALIDATED);
210+
inputData.remove(inputName + AddressParts.ZIPCODE + UNVALIDATED_FIELD_MARKER_VALIDATED);
211211
}
212212

213213
/**

src/main/java/formflow/library/data/UserFile.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ public class UserFile {
4141

4242
@Id
4343
@GeneratedValue
44-
//@Type(type = "org.hibernate.type.UUIDCharType")
45-
//@Type(type = "pg-uuid")
4644
private UUID fileId;
4745

4846
@ManyToOne
@@ -65,10 +63,13 @@ public class UserFile {
6563

6664
@Column
6765
private Float filesize;
68-
66+
6967
@Column(name = "virus_scanned")
7068
private boolean virusScanned;
7169

70+
@Column(name = "doc_type_label")
71+
private String docTypeLabel;
72+
7273
@Override
7374
public boolean equals(Object o) {
7475
if (this == o) {
@@ -102,6 +103,7 @@ public static HashMap<String, String> createFileInfo(UserFile userFile, String t
102103
fileInfo.put("filesize", userFile.getFilesize().toString());
103104
fileInfo.put("thumbnailUrl", thumbBase64String);
104105
fileInfo.put("type", userFile.getMimeType());
106+
fileInfo.put("docTypeLabel", userFile.getDocTypeLabel());
105107
return fileInfo;
106108
}
107-
}
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package formflow.library.data.annotations;
2+
3+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
4+
import static java.lang.annotation.ElementType.TYPE;
5+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
6+
7+
import jakarta.validation.Payload;
8+
import java.lang.annotation.Documented;
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.Target;
12+
13+
@Target({ElementType.FIELD, TYPE, ANNOTATION_TYPE})
14+
@Retention(RUNTIME)
15+
@Documented
16+
public @interface DynamicField {
17+
18+
String message() default "";
19+
20+
Class<?>[] groups() default {};
21+
22+
Class<? extends Payload>[] payload() default {};
23+
24+
}

src/main/java/formflow/library/data/validators/Money.java src/main/java/formflow/library/data/annotations/Money.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
package formflow.library.data.validators;
1+
package formflow.library.data.annotations;
22

33
import static java.lang.annotation.ElementType.FIELD;
44
import static java.lang.annotation.RetentionPolicy.RUNTIME;
55

6+
import formflow.library.data.validators.MoneyValidator;
67
import java.lang.annotation.Documented;
78
import java.lang.annotation.Retention;
89
import java.lang.annotation.Target;

src/main/java/formflow/library/data/validators/Phone.java src/main/java/formflow/library/data/annotations/Phone.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
package formflow.library.data.validators;
1+
package formflow.library.data.annotations;
22

33
import static java.lang.annotation.ElementType.FIELD;
44
import static java.lang.annotation.RetentionPolicy.RUNTIME;
55

6+
import formflow.library.data.validators.PhoneValidator;
67
import jakarta.validation.Constraint;
78
import jakarta.validation.Payload;
89
import java.lang.annotation.Documented;

src/main/java/formflow/library/data/validators/MoneyValidator.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package formflow.library.data.validators;
22

3+
import formflow.library.data.annotations.Money;
34
import java.util.regex.Pattern;
45
import jakarta.validation.ConstraintValidator;
56
import jakarta.validation.ConstraintValidatorContext;
@@ -8,6 +9,7 @@
89
* This class validates that Money has been passed in the correct format - one or more digits, a dot and 2 digits after the dot.
910
*/
1011
public class MoneyValidator implements ConstraintValidator<Money, String> {
12+
1113
@Override
1214
public boolean isValid(String value, ConstraintValidatorContext context) {
1315
return Pattern.matches("(^(0|([1-9]\\d*))?(\\.\\d{1,2})?$)?", value);

src/main/java/formflow/library/data/validators/PhoneValidator.java

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package formflow.library.data.validators;
22

3+
import formflow.library.data.annotations.Phone;
34
import jakarta.validation.ConstraintValidator;
45
import jakarta.validation.ConstraintValidatorContext;
56
import java.util.regex.Pattern;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package formflow.library.inputs;
2+
3+
public class FieldNameMarkers {
4+
5+
public static final String UNVALIDATED_FIELD_MARKER_CSRF = "_csrf";
6+
public static final String UNVALIDATED_FIELD_MARKER_VALIDATE_ADDRESS = "validate_";
7+
public static final String UNVALIDATED_FIELD_MARKER_VALIDATED = "_validated";
8+
public static final String DYNAMIC_FIELD_MARKER = "_wildcard_";
9+
}

0 commit comments

Comments
 (0)