mirror of
https://github.com/apache/sqoop.git
synced 2025-05-11 14:30:59 +08:00
SQOOP-1441: Sqoop2: Validations: Enforce defined validations
This commit is contained in:
parent
e2d7ac6b85
commit
a53e4141ac
@ -37,6 +37,8 @@
|
|||||||
/**
|
/**
|
||||||
* Util class for transforming data from correctly annotated configuration
|
* Util class for transforming data from correctly annotated configuration
|
||||||
* objects to different structures and vice-versa.
|
* objects to different structures and vice-versa.
|
||||||
|
*
|
||||||
|
* TODO: This class should see some overhaul into more reusable code, especially expose and re-use the methods at the end.
|
||||||
*/
|
*/
|
||||||
public class FormUtils {
|
public class FormUtils {
|
||||||
|
|
||||||
@ -535,4 +537,61 @@ private static void checkForValidFormName(Set<String> existingFormNames,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public static String getName(Field input, Input annotation) {
|
||||||
|
return input.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getName(Field form, Form annotation) {
|
||||||
|
return form.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConfigurationClass getConfigurationClassAnnotation(Object object, boolean strict) {
|
||||||
|
ConfigurationClass annotation = object.getClass().getAnnotation(ConfigurationClass.class);
|
||||||
|
|
||||||
|
if(strict && annotation == null) {
|
||||||
|
throw new SqoopException(ModelError.MODEL_003, "Missing annotation ConfigurationClass on class " + object.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FormClass getFormClassAnnotation(Object object, boolean strict) {
|
||||||
|
FormClass annotation = object.getClass().getAnnotation(FormClass.class);
|
||||||
|
|
||||||
|
if(strict && annotation == null) {
|
||||||
|
throw new SqoopException(ModelError.MODEL_003, "Missing annotation ConfigurationClass on class " + object.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Form getFormAnnotation(Field field, boolean strict) {
|
||||||
|
Form annotation = field.getAnnotation(Form.class);
|
||||||
|
|
||||||
|
if(strict && annotation == null) {
|
||||||
|
throw new SqoopException(ModelError.MODEL_003, "Missing annotation Form on Field " + field.getName() + " on class " + field.getDeclaringClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Input getInputAnnotation(Field field, boolean strict) {
|
||||||
|
Input annotation = field.getAnnotation(Input.class);
|
||||||
|
|
||||||
|
if(strict && annotation == null) {
|
||||||
|
throw new SqoopException(ModelError.MODEL_003, "Missing annotation Input on Field " + field.getName() + " on class " + field.getDeclaringClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getFieldValue(Field field, Object object) {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(object);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new SqoopException(ModelError.MODEL_015, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -50,8 +50,11 @@ public enum ModelError implements ErrorCode {
|
|||||||
|
|
||||||
MODEL_013("Form name attribute should not contain unsupported characters"),
|
MODEL_013("Form name attribute should not contain unsupported characters"),
|
||||||
|
|
||||||
MODEL_014("Form name attribute cannot be more than 30 characters long")
|
MODEL_014("Form name attribute cannot be more than 30 characters long"),
|
||||||
;
|
|
||||||
|
MODEL_015("Can't get value from object")
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.sqoop.validation;
|
||||||
|
|
||||||
|
import org.apache.sqoop.validation.validators.Validator;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of validation execution.
|
||||||
|
*/
|
||||||
|
public class ValidationResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All messages for each named item.
|
||||||
|
*/
|
||||||
|
Map<String, List<Message>> messages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overall status.
|
||||||
|
*/
|
||||||
|
Status status;
|
||||||
|
|
||||||
|
public ValidationResult() {
|
||||||
|
messages = new HashMap<String, List<Message>>();
|
||||||
|
status = Status.getDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add given validator result to this instance.
|
||||||
|
*
|
||||||
|
* @param name Full name of the validated object
|
||||||
|
* @param validator Executed validator
|
||||||
|
*/
|
||||||
|
public void addValidator(String name, Validator validator) {
|
||||||
|
if(validator.getStatus() == Status.getDefault()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = Status.getWorstStatus(status, validator.getStatus());
|
||||||
|
if(messages.containsKey(name)) {
|
||||||
|
messages.get(name).addAll(validator.getMessages());
|
||||||
|
} else {
|
||||||
|
messages.put(name, validator.getMessages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge results with another validation result.
|
||||||
|
*
|
||||||
|
* @param result Other validation result
|
||||||
|
*/
|
||||||
|
public void merge(ValidationResult result) {
|
||||||
|
messages.putAll(result.messages);
|
||||||
|
status = Status.getWorstStatus(status, result.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<Message>> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.sqoop.validation;
|
||||||
|
|
||||||
|
import org.apache.sqoop.model.ConfigurationClass;
|
||||||
|
import org.apache.sqoop.model.Form;
|
||||||
|
import org.apache.sqoop.model.FormClass;
|
||||||
|
import org.apache.sqoop.model.FormUtils;
|
||||||
|
import org.apache.sqoop.model.Input;
|
||||||
|
import org.apache.sqoop.utils.ClassUtils;
|
||||||
|
import org.apache.sqoop.validation.validators.Validator;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation runner that will run validators associated with given configuration
|
||||||
|
* class or form object.
|
||||||
|
*
|
||||||
|
* Execution follows following rules:
|
||||||
|
* * Run children first (Inputs -> Form -> Class)
|
||||||
|
* * If any children is not suitable (canProceed = false), skip running parent
|
||||||
|
*
|
||||||
|
* Which means that form validator don't have to repeat it's input validators as it will
|
||||||
|
* be never called if the input's are not valid. Similarly Class validators won't be called
|
||||||
|
* unless all forms will pass validators.
|
||||||
|
*
|
||||||
|
* TODO: Cache the validators instances, so that we don't have create new instance every time
|
||||||
|
*/
|
||||||
|
public class ValidationRunner {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate given configuration instance.
|
||||||
|
*
|
||||||
|
* @param config Configuration instance
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public ValidationResult validate(Object config) {
|
||||||
|
ValidationResult result = new ValidationResult();
|
||||||
|
ConfigurationClass globalAnnotation = FormUtils.getConfigurationClassAnnotation(config, true);
|
||||||
|
|
||||||
|
// Iterate over all declared form and call their validators
|
||||||
|
for (Field field : config.getClass().getDeclaredFields()) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
|
||||||
|
Form formAnnotation = FormUtils.getFormAnnotation(field, false);
|
||||||
|
if(formAnnotation == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String formName = FormUtils.getName(field, formAnnotation);
|
||||||
|
ValidationResult r = validateForm(formName, FormUtils.getFieldValue(field, config));
|
||||||
|
result.merge(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call class validator only as long as we are in suitable state
|
||||||
|
if(result.getStatus().canProceed()) {
|
||||||
|
ValidationResult r = validateArray("", config, globalAnnotation.validators());
|
||||||
|
result.merge(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate given form instance.
|
||||||
|
*
|
||||||
|
* @param formName Form's name to build full name for all inputs.
|
||||||
|
* @param form Form instance
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public ValidationResult validateForm(String formName, Object form) {
|
||||||
|
ValidationResult result = new ValidationResult();
|
||||||
|
FormClass formAnnotation = FormUtils.getFormClassAnnotation(form, true);
|
||||||
|
|
||||||
|
// Iterate over all declared inputs and call their validators
|
||||||
|
for (Field field : form.getClass().getDeclaredFields()) {
|
||||||
|
Input inputAnnotation = FormUtils.getInputAnnotation(field, false);
|
||||||
|
if(inputAnnotation == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = formName + "." + FormUtils.getName(field, inputAnnotation);
|
||||||
|
|
||||||
|
ValidationResult r = validateArray(name, FormUtils.getFieldValue(field, form), inputAnnotation.validators());
|
||||||
|
result.merge(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call form validator only as long as we are in suitable state
|
||||||
|
if(result.getStatus().canProceed()) {
|
||||||
|
ValidationResult r = validateArray(formName, form, formAnnotation.validators());
|
||||||
|
result.merge(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute array of validators on given object (can be input/form/class).
|
||||||
|
*
|
||||||
|
* @param name Full name of the object
|
||||||
|
* @param object Input, Form or Class instance
|
||||||
|
* @param classes Validators array
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private ValidationResult validateArray(String name, Object object, Class<? extends Validator> []classes) {
|
||||||
|
ValidationResult result = new ValidationResult();
|
||||||
|
|
||||||
|
for (Class<? extends Validator> klass : classes) {
|
||||||
|
Validator v = executeValidator(object, klass);
|
||||||
|
result.addValidator(name, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute single validator.
|
||||||
|
*
|
||||||
|
* @param object Input, Form or Class instance
|
||||||
|
* @param klass Validator's clas
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private Validator executeValidator(Object object, Class<? extends Validator> klass) {
|
||||||
|
Validator instance = (Validator) ClassUtils.instantiate(klass);
|
||||||
|
instance.validate(object);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -44,26 +44,37 @@ abstract public class Validator<T> {
|
|||||||
*/
|
*/
|
||||||
private List<Message> messages;
|
private List<Message> messages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overall status of the validation.
|
||||||
|
*/
|
||||||
|
private Status status;
|
||||||
|
|
||||||
public Validator() {
|
public Validator() {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addMessage(Message msg) {
|
protected void addMessage(Message msg) {
|
||||||
|
status = Status.getWorstStatus(status, msg.getStatus());
|
||||||
messages.add(msg);
|
messages.add(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addMessage(Status status, String msg) {
|
protected void addMessage(Status status, String msg) {
|
||||||
messages.add(new Message(status, msg));
|
addMessage(new Message(status, msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Message> getMessages() {
|
public List<Message> getMessages() {
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Status getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset validator state (all previous messages).
|
* Reset validator state (all previous messages).
|
||||||
*/
|
*/
|
||||||
public void reset() {
|
public void reset() {
|
||||||
messages = new LinkedList<Message>();
|
messages = new LinkedList<Message>();
|
||||||
|
status = Status.getDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.sqoop.validation;
|
||||||
|
|
||||||
|
import org.apache.sqoop.model.ConfigurationClass;
|
||||||
|
import org.apache.sqoop.model.Form;
|
||||||
|
import org.apache.sqoop.model.FormClass;
|
||||||
|
import org.apache.sqoop.model.Input;
|
||||||
|
import org.apache.sqoop.validation.validators.NotEmpty;
|
||||||
|
import org.apache.sqoop.validation.validators.NotNull;
|
||||||
|
import org.apache.sqoop.validation.validators.Validator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public class TestValidationRunner {
|
||||||
|
|
||||||
|
@FormClass(validators = {FormA.FormValidator.class})
|
||||||
|
public static class FormA {
|
||||||
|
@Input(validators = {NotNull.class})
|
||||||
|
String notNull;
|
||||||
|
|
||||||
|
public static class FormValidator extends Validator<FormA> {
|
||||||
|
@Override
|
||||||
|
public void validate(FormA form) {
|
||||||
|
if(form.notNull == null) {
|
||||||
|
addMessage(Status.UNACCEPTABLE, "null");
|
||||||
|
}
|
||||||
|
if("error".equals(form.notNull)) {
|
||||||
|
addMessage(Status.UNACCEPTABLE, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateForm() {
|
||||||
|
FormA form = new FormA();
|
||||||
|
ValidationRunner runner = new ValidationRunner();
|
||||||
|
ValidationResult result;
|
||||||
|
|
||||||
|
// Null string should fail on Input level and should not call form level validators
|
||||||
|
form.notNull = null;
|
||||||
|
result = runner.validateForm("formName", form);
|
||||||
|
assertEquals(Status.UNACCEPTABLE, result.getStatus());
|
||||||
|
assertEquals(1, result.getMessages().size());
|
||||||
|
assertTrue(result.getMessages().containsKey("formName.notNull"));
|
||||||
|
|
||||||
|
// String "error" should trigger form level error, but not Input level
|
||||||
|
form.notNull = "error";
|
||||||
|
result = runner.validateForm("formName", form);
|
||||||
|
assertEquals(Status.UNACCEPTABLE, result.getStatus());
|
||||||
|
assertEquals(1, result.getMessages().size());
|
||||||
|
assertTrue(result.getMessages().containsKey("formName"));
|
||||||
|
|
||||||
|
// Acceptable state
|
||||||
|
form.notNull = "This is truly random string";
|
||||||
|
result = runner.validateForm("formName", form);
|
||||||
|
assertEquals(Status.FINE, result.getStatus());
|
||||||
|
assertEquals(0, result.getMessages().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormClass
|
||||||
|
public static class FormB {
|
||||||
|
@Input(validators = {NotNull.class, NotEmpty.class})
|
||||||
|
String str;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultipleValidatorsOnSingleInput() {
|
||||||
|
FormB form = new FormB();
|
||||||
|
ValidationRunner runner = new ValidationRunner();
|
||||||
|
ValidationResult result;
|
||||||
|
|
||||||
|
form.str = null;
|
||||||
|
result = runner.validateForm("formName", form);
|
||||||
|
assertEquals(Status.UNACCEPTABLE, result.getStatus());
|
||||||
|
assertEquals(1, result.getMessages().size());
|
||||||
|
assertTrue(result.getMessages().containsKey("formName.str"));
|
||||||
|
assertEquals(2, result.getMessages().get("formName.str").size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigurationClass(validators = {ConfigurationA.ClassValidator.class})
|
||||||
|
public static class ConfigurationA {
|
||||||
|
@Form FormA formA;
|
||||||
|
public ConfigurationA() {
|
||||||
|
formA = new FormA();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ClassValidator extends Validator<ConfigurationA> {
|
||||||
|
@Override
|
||||||
|
public void validate(ConfigurationA conf) {
|
||||||
|
if("error".equals(conf.formA.notNull)) {
|
||||||
|
addMessage(Status.UNACCEPTABLE, "error");
|
||||||
|
}
|
||||||
|
if("conf-error".equals(conf.formA.notNull)) {
|
||||||
|
addMessage(Status.UNACCEPTABLE, "conf-error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidate() {
|
||||||
|
ConfigurationA conf = new ConfigurationA();
|
||||||
|
ValidationRunner runner = new ValidationRunner();
|
||||||
|
ValidationResult result;
|
||||||
|
|
||||||
|
// Null string should fail on Input level and should not call form nor class level validators
|
||||||
|
conf.formA.notNull = null;
|
||||||
|
result = runner.validate(conf);
|
||||||
|
assertEquals(Status.UNACCEPTABLE, result.getStatus());
|
||||||
|
assertEquals(1, result.getMessages().size());
|
||||||
|
assertTrue(result.getMessages().containsKey("formA.notNull"));
|
||||||
|
|
||||||
|
// String "error" should trigger form level error, but not Input nor class level
|
||||||
|
conf.formA.notNull = "error";
|
||||||
|
result = runner.validate(conf);
|
||||||
|
assertEquals(Status.UNACCEPTABLE, result.getStatus());
|
||||||
|
assertEquals(1, result.getMessages().size());
|
||||||
|
assertTrue(result.getMessages().containsKey("formA"));
|
||||||
|
|
||||||
|
// String "conf-error" should trigger class level error, but not Input nor Form level
|
||||||
|
conf.formA.notNull = "conf-error";
|
||||||
|
result = runner.validate(conf);
|
||||||
|
assertEquals(Status.UNACCEPTABLE, result.getStatus());
|
||||||
|
assertEquals(1, result.getMessages().size());
|
||||||
|
assertTrue(result.getMessages().containsKey(""));
|
||||||
|
|
||||||
|
// Valid string
|
||||||
|
conf.formA.notNull = "Valid string";
|
||||||
|
result = runner.validate(conf);
|
||||||
|
assertEquals(Status.FINE, result.getStatus());
|
||||||
|
assertEquals(0, result.getMessages().size());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user