diff --git a/src/main/java/com/github/joschi/jadconfig/JadConfig.java b/src/main/java/com/github/joschi/jadconfig/JadConfig.java index 05e199d..54ca7ff 100644 --- a/src/main/java/com/github/joschi/jadconfig/JadConfig.java +++ b/src/main/java/com/github/joschi/jadconfig/JadConfig.java @@ -2,6 +2,8 @@ import com.github.joschi.jadconfig.converters.NoConverter; import com.github.joschi.jadconfig.converters.StringConverter; +import com.github.joschi.jadconfig.response.ProcessingOutcome; +import com.github.joschi.jadconfig.response.ProcessingResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,10 +20,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; -import static com.github.joschi.jadconfig.ReflectionUtils.getAllFields; -import static com.github.joschi.jadconfig.ReflectionUtils.getAllMethods; -import static com.github.joschi.jadconfig.ReflectionUtils.invokeMethodsWithAnnotation; /** * The main class for JadConfig. It's responsible for parsing the configuration bean(s) that contain(s) the annotated @@ -85,6 +85,8 @@ public JadConfig(Collection repositories, Object... configurationBea * Processes the configuration provided by the configured {@link Repository} and filling the provided configuration * beans. * + * Stops processing on first encountered exception. + * * @throws RepositoryException If an error occurred while reading from the configured {@link Repository} * @throws ValidationException If any parameter couldn't be successfully validated */ @@ -98,58 +100,106 @@ public void process() throws RepositoryException, ValidationException { for (Object configurationBean : configurationBeans) { LOG.debug("Processing configuration bean {}", configurationBean); - processClassFields(configurationBean, getAllFields(configurationBean.getClass())); - invokeValidatorMethods(configurationBean, getAllMethods(configurationBean.getClass())); + processClassFields(configurationBean, ReflectionUtils.getAllFields(configurationBean.getClass())); + invokeValidatorMethods(configurationBean, ReflectionUtils.getAllMethods(configurationBean.getClass())); + } + } + + /** + * Processes the configuration provided by the configured {@link Repository} and filling the provided configuration + * beans. + *

+ * Instead of stopping processing on first encountered exception, tries to collect all validation problems and in + * case of any problems aggregate them all into single exception, listing all the field and validation issues. + */ + public void processFailingLazily() throws RepositoryException, LazyValidationException { + final ProcessingResponse result = doProcessFailingLazily(); + if (!result.isSuccess()) { + throw new LazyValidationException(result); + } + } + + ProcessingResponse doProcessFailingLazily() throws RepositoryException { + for (Repository repository : repositories) { + LOG.debug("Opening repository {}", repository); + repository.open(); } + + return configurationBeans.stream() + .peek(bean -> LOG.debug("Processing configuration bean {}", bean)) + .map(this::processBean) + .collect(Collectors.collectingAndThen(Collectors.toList(), ProcessingResponse::new)); + } + + private ProcessingOutcome processBean(Object bean) { + final Map fieldProcessingProblems = processClassFieldsFailingLazily(bean, ReflectionUtils.getAllFields(bean.getClass())); + final Map validationMethodsProblems = invokeValidatorMethodsFailingLazily(bean, ReflectionUtils.getAllMethods(bean.getClass())); + return new ProcessingOutcome(bean, fieldProcessingProblems, validationMethodsProblems); } private void processClassFields(Object configurationBean, Field[] fields) throws ValidationException { for (Field field : fields) { - Parameter parameter = field.getAnnotation(Parameter.class); + processClassField(configurationBean, field); + } + } - if (parameter != null) { - LOG.debug("Processing field {}", parameter); + private Map processClassFieldsFailingLazily(Object configurationBean, Field[] fields) { + final Map fieldProcessingProblems = new HashMap<>(); + for (Field field : fields) { + try { + processClassField(configurationBean, field); + } catch (Exception ex) { + fieldProcessingProblems.put(field.getAnnotation(Parameter.class).value(), ex); + } + } + return fieldProcessingProblems; + } - Object fieldValue = getFieldValue(field, configurationBean); + private void processClassField(Object configurationBean, Field field) throws ValidationException { + Parameter parameter = field.getAnnotation(Parameter.class); - String parameterName = parameter.value(); - String parameterValue = lookupParameter(parameterName) - .orElseGet(() -> lookupFallbackParameter(parameter)); + if (parameter != null) { + LOG.debug("Processing field {}", parameter); + Object fieldValue = getFieldValue(field, configurationBean); - if (parameterValue == null && fieldValue == null && parameter.required()) { - throw new ParameterException("Required parameter \"" + parameterName + "\" not found."); - } + String parameterName = parameter.value(); + String parameterValue = lookupParameter(parameterName) + .orElseGet(() -> lookupFallbackParameter(parameter)); - if (parameterValue != null) { - if (parameter.trim()) { - LOG.debug("Trimmed parameter value {}", parameterName); - parameterValue = Strings.trim(parameterValue); - } + if (parameterValue == null && fieldValue == null && parameter.required()) { + throw new ParameterException("Required parameter \"" + parameterName + "\" not found."); + } - LOG.debug("Converting parameter value {}", parameterName); - try { - fieldValue = convertStringValue(field.getType(), parameter.converter(), parameterValue); - } catch (ParameterException e) { - throw new ParameterException("Couldn't convert value for parameter \"" + parameterName + "\"", e); - } + if (parameterValue != null) { - LOG.debug("Validating parameter {}", parameterName); - final List>> validators = - new ArrayList<>(Collections.>>singleton(parameter.validator())); - validators.addAll(Arrays.asList(parameter.validators())); - validateParameter(validators, parameterName, fieldValue); + if (parameter.trim()) { + LOG.debug("Trimmed parameter value {}", parameterName); + parameterValue = Strings.trim(parameterValue); } - LOG.debug("Setting parameter {} to {}", parameterName, fieldValue); - + LOG.debug("Converting parameter value {}", parameterName); try { - field.set(configurationBean, fieldValue); - } catch (Exception e) { - throw new ParameterException("Couldn't set field " + field.getName(), e); + fieldValue = convertStringValue(field.getType(), parameter.converter(), parameterValue); + } catch (ParameterException e) { + throw new ParameterException("Couldn't convert value for parameter \"" + parameterName + "\"", e); } + + LOG.debug("Validating parameter {}", parameterName); + final List>> validators = + new ArrayList<>(Collections.>>singleton(parameter.validator())); + validators.addAll(Arrays.asList(parameter.validators())); + validateParameter(validators, parameterName, fieldValue); + } + + LOG.debug("Setting parameter {} to {}", parameterName, fieldValue); + + try { + field.set(configurationBean, fieldValue); + } catch (Exception e) { + throw new ParameterException("Couldn't set field " + field.getName(), e); } } } @@ -230,7 +280,7 @@ private void validateParameter(Collection>> validat private void invokeValidatorMethods(Object configurationBean, Method[] methods) throws ValidationException { try { - invokeMethodsWithAnnotation(configurationBean, ValidatorMethod.class, methods); + ReflectionUtils.invokeMethodsWithAnnotation(configurationBean, ValidatorMethod.class, methods); } catch (InvocationTargetException e) { if (e.getTargetException() instanceof ValidationException) { throw (ValidationException)e.getTargetException(); @@ -242,6 +292,27 @@ private void invokeValidatorMethods(Object configurationBean, Method[] methods) } } + private Map invokeValidatorMethodsFailingLazily(Object configurationBean, Method[] methods) { + final Map problems = new HashMap<>(); + + for (Method method : methods) { + if (method.isAnnotationPresent(ValidatorMethod.class)) { + try { + method.invoke(configurationBean); + } catch (InvocationTargetException invEx) { + if (invEx.getTargetException() instanceof ValidationException) { + problems.put(method.getName(), (ValidationException)invEx.getTargetException()); + } else { + problems.put(method.getName(), invEx); + } + } catch (Exception ex) { + problems.put(method.getName(), ex); + } + } + } + return problems; + } + private Class> findConverter(Class clazz) { for (ConverterFactory factory : converterFactories) { Class> result = factory.getConverter(clazz); @@ -292,7 +363,7 @@ public Map dump() { final Map configurationDump = new HashMap(); for (Object configurationBean : configurationBeans) { - for (Field field : getAllFields(configurationBean.getClass())) { + for (Field field : ReflectionUtils.getAllFields(configurationBean.getClass())) { final Parameter parameter = field.getAnnotation(Parameter.class); if (parameter != null) { diff --git a/src/main/java/com/github/joschi/jadconfig/LazyValidationException.java b/src/main/java/com/github/joschi/jadconfig/LazyValidationException.java new file mode 100644 index 0000000..0acb777 --- /dev/null +++ b/src/main/java/com/github/joschi/jadconfig/LazyValidationException.java @@ -0,0 +1,33 @@ +package com.github.joschi.jadconfig; + +import com.github.joschi.jadconfig.response.ProcessingOutcome; +import com.github.joschi.jadconfig.response.ProcessingResponse; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; + +public class LazyValidationException extends ValidationException { + private final ProcessingResponse processingResponse; + + public LazyValidationException(ProcessingResponse result) { + super(toMessage(result)); + this.processingResponse = result; + } + + private static String toMessage(ProcessingResponse result) { + final List stringBuilder = new LinkedList<>(); + stringBuilder.add("Following errors ocurred during configuration processing:"); + result.getOutcomes().stream() + .filter(ProcessingOutcome::hasProblems) + .flatMap(processingOutcome -> Stream.concat( + processingOutcome.getFieldProcessingProblems().values().stream().map(Throwable::getMessage), + processingOutcome.getValidationMethodsProblems().values().stream().map(Throwable::getMessage) + )).forEach(stringBuilder::add); + return String.join("\n", stringBuilder); + } + + public ProcessingResponse getProcessingResponse() { + return processingResponse; + } +} diff --git a/src/main/java/com/github/joschi/jadconfig/response/ProcessingOutcome.java b/src/main/java/com/github/joschi/jadconfig/response/ProcessingOutcome.java new file mode 100644 index 0000000..4f6e8ac --- /dev/null +++ b/src/main/java/com/github/joschi/jadconfig/response/ProcessingOutcome.java @@ -0,0 +1,35 @@ +package com.github.joschi.jadconfig.response; + +import java.util.Map; + +public class ProcessingOutcome { + + private final Object configurationBean; + private final Map fieldProcessingProblems; + private final Map validationMethodsProblems; + + public ProcessingOutcome(final Object configurationBean, + final Map fieldProcessingProblems, + final Map validationMethodsProblems) { + this.configurationBean = configurationBean; + this.fieldProcessingProblems = fieldProcessingProblems; + this.validationMethodsProblems = validationMethodsProblems; + } + + public boolean hasProblems() { + return (fieldProcessingProblems != null && !fieldProcessingProblems.isEmpty()) || + (validationMethodsProblems != null && !validationMethodsProblems.isEmpty()); + } + + public Object getConfigurationBean() { + return configurationBean; + } + + public Map getFieldProcessingProblems() { + return fieldProcessingProblems; + } + + public Map getValidationMethodsProblems() { + return validationMethodsProblems; + } +} diff --git a/src/main/java/com/github/joschi/jadconfig/response/ProcessingResponse.java b/src/main/java/com/github/joschi/jadconfig/response/ProcessingResponse.java new file mode 100644 index 0000000..e6cbe2e --- /dev/null +++ b/src/main/java/com/github/joschi/jadconfig/response/ProcessingResponse.java @@ -0,0 +1,21 @@ +package com.github.joschi.jadconfig.response; + +import java.util.List; + +public class ProcessingResponse { + + private final List outcomes; + + public ProcessingResponse(List outcomes) { + this.outcomes = outcomes; + } + + public List getOutcomes() { + return outcomes; + } + + public boolean isSuccess() { + return outcomes.stream().noneMatch(ProcessingOutcome::hasProblems); + } +} + diff --git a/src/test/java/com/github/joschi/jadconfig/JadConfigLazyProcessingTest.java b/src/test/java/com/github/joschi/jadconfig/JadConfigLazyProcessingTest.java new file mode 100644 index 0000000..086301b --- /dev/null +++ b/src/test/java/com/github/joschi/jadconfig/JadConfigLazyProcessingTest.java @@ -0,0 +1,50 @@ +package com.github.joschi.jadconfig; + +import com.github.joschi.jadconfig.repositories.InMemoryRepository; +import com.github.joschi.jadconfig.response.ProcessingOutcome; +import com.github.joschi.jadconfig.response.ProcessingResponse; +import com.github.joschi.jadconfig.testbeans.ValidatedConfigurationBean; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class JadConfigLazyProcessingTest { + + @Test + public void testProcess() throws RepositoryException { + HashMap properties = new HashMap<>(); + properties.put("test.byte", "1"); + properties.put("test.short", "2"); + properties.put("test.integer", "-3");//negative, smaller than test.short + properties.put("test.integer.port", "70000"); //bigger than allowed port + properties.put("test.long", "4"); + properties.put("test.string", "Test"); + Repository repository = new InMemoryRepository(properties); + ValidatedConfigurationBean configurationBean = new ValidatedConfigurationBean(); + JadConfig jadConfig = new JadConfig(repository, configurationBean); + try { + jadConfig.processFailingLazily(); + Assert.fail("Should throw an exception!"); + } catch (LazyValidationException e) { + final ProcessingResponse response = e.getProcessingResponse(); + assertFalse(response.isSuccess()); + assertEquals(1, response.getOutcomes().size()); + ProcessingOutcome processingOutcome = response.getOutcomes().get(0); + assertEquals(configurationBean, processingOutcome.getConfigurationBean()); + Map fieldProcessingProblems = processingOutcome.getFieldProcessingProblems(); + assertEquals(2, fieldProcessingProblems.size()); + assertTrue(fieldProcessingProblems.containsKey("test.integer")); + assertTrue(fieldProcessingProblems.containsKey("test.integer.port")); + + Map validationMethodsProblems = processingOutcome.getValidationMethodsProblems(); + assertEquals(1, validationMethodsProblems.size()); + assertTrue(validationMethodsProblems.containsKey("myCustomValidatorMethod")); + } + + + } +} diff --git a/src/test/java/com/github/joschi/jadconfig/testbeans/ValidatedConfigurationBean.java b/src/test/java/com/github/joschi/jadconfig/testbeans/ValidatedConfigurationBean.java index dda6046..b13ad10 100644 --- a/src/test/java/com/github/joschi/jadconfig/testbeans/ValidatedConfigurationBean.java +++ b/src/test/java/com/github/joschi/jadconfig/testbeans/ValidatedConfigurationBean.java @@ -7,7 +7,6 @@ import com.github.joschi.jadconfig.validators.NoValidator; import com.github.joschi.jadconfig.validators.PositiveIntegerValidator; import com.github.joschi.jadconfig.validators.PositiveLongValidator; -import com.github.joschi.jadconfig.validators.PositiveSizeValidator; public class ValidatedConfigurationBean { @@ -54,7 +53,7 @@ public long getMyLong() { } @ValidatorMethod - public void validate() throws ValidationException { + public void myCustomValidatorMethod() throws ValidationException { if (!"Test".equals(myString)) { throw new ValidationException("BOOM");