5
0
mirror of https://github.com/apache/sqoop.git synced 2025-05-08 07:11:01 +08:00

SQOOP-1441: Sqoop2: Validations: Enforce defined validations

This commit is contained in:
Jarek Jarcec Cecho 2014-08-18 10:46:19 -07:00 committed by Abraham Elmahrek
parent e2d7ac6b85
commit a53e4141ac
6 changed files with 458 additions and 4 deletions

View File

@ -37,6 +37,8 @@
/**
* Util class for transforming data from correctly annotated configuration
* 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 {
@ -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);
}
}
}

View File

@ -50,8 +50,11 @@ public enum ModelError implements ErrorCode {
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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -44,26 +44,37 @@ abstract public class Validator<T> {
*/
private List<Message> messages;
/**
* Overall status of the validation.
*/
private Status status;
public Validator() {
reset();
}
protected void addMessage(Message msg) {
status = Status.getWorstStatus(status, msg.getStatus());
messages.add(msg);
}
protected void addMessage(Status status, String msg) {
messages.add(new Message(status, msg));
addMessage(new Message(status, msg));
}
public List<Message> getMessages() {
return messages;
}
public Status getStatus() {
return status;
}
/**
* Reset validator state (all previous messages).
*/
public void reset() {
messages = new LinkedList<Message>();
status = Status.getDefault();
}
}

View File

@ -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());
}
}