diff --git a/common/src/main/java/org/apache/sqoop/model/FormUtils.java b/common/src/main/java/org/apache/sqoop/model/FormUtils.java index a829f1e4..dfc2afa2 100644 --- a/common/src/main/java/org/apache/sqoop/model/FormUtils.java +++ b/common/src/main/java/org/apache/sqoop/model/FormUtils.java @@ -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 existingFormNames, } } -} \ No newline at end of file + 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); + } + } + +} diff --git a/common/src/main/java/org/apache/sqoop/model/ModelError.java b/common/src/main/java/org/apache/sqoop/model/ModelError.java index 86b626e3..74e924e1 100644 --- a/common/src/main/java/org/apache/sqoop/model/ModelError.java +++ b/common/src/main/java/org/apache/sqoop/model/ModelError.java @@ -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; diff --git a/common/src/main/java/org/apache/sqoop/validation/ValidationResult.java b/common/src/main/java/org/apache/sqoop/validation/ValidationResult.java new file mode 100644 index 00000000..abe5b111 --- /dev/null +++ b/common/src/main/java/org/apache/sqoop/validation/ValidationResult.java @@ -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> messages; + + /** + * Overall status. + */ + Status status; + + public ValidationResult() { + messages = new HashMap>(); + 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> getMessages() { + return messages; + } +} diff --git a/common/src/main/java/org/apache/sqoop/validation/ValidationRunner.java b/common/src/main/java/org/apache/sqoop/validation/ValidationRunner.java new file mode 100644 index 00000000..46e2d56e --- /dev/null +++ b/common/src/main/java/org/apache/sqoop/validation/ValidationRunner.java @@ -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 []classes) { + ValidationResult result = new ValidationResult(); + + for (Class 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 klass) { + Validator instance = (Validator) ClassUtils.instantiate(klass); + instance.validate(object); + return instance; + } + + +} diff --git a/common/src/main/java/org/apache/sqoop/validation/validators/Validator.java b/common/src/main/java/org/apache/sqoop/validation/validators/Validator.java index 5c24b1ad..bdb7c209 100644 --- a/common/src/main/java/org/apache/sqoop/validation/validators/Validator.java +++ b/common/src/main/java/org/apache/sqoop/validation/validators/Validator.java @@ -44,26 +44,37 @@ abstract public class Validator { */ private List 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 getMessages() { return messages; } + public Status getStatus() { + return status; + } + /** * Reset validator state (all previous messages). */ public void reset() { messages = new LinkedList(); + status = Status.getDefault(); } } diff --git a/common/src/test/java/org/apache/sqoop/validation/TestValidationRunner.java b/common/src/test/java/org/apache/sqoop/validation/TestValidationRunner.java new file mode 100644 index 00000000..19614257 --- /dev/null +++ b/common/src/test/java/org/apache/sqoop/validation/TestValidationRunner.java @@ -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 { + @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 { + @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()); + } +}