Skip to content

Commit

Permalink
Config processing with lazy error handling (#127)
Browse files Browse the repository at this point in the history
Co-authored-by: Tomas Dvorak <[email protected]>
  • Loading branch information
luk-kaminski and todvora authored Jun 10, 2024
1 parent 18c9f03 commit 757eb6d
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 40 deletions.
147 changes: 109 additions & 38 deletions src/main/java/com/github/joschi/jadconfig/JadConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -85,6 +85,8 @@ public JadConfig(Collection<Repository> 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
*/
Expand All @@ -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.
* <p>
* 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<String, Exception> fieldProcessingProblems = processClassFieldsFailingLazily(bean, ReflectionUtils.getAllFields(bean.getClass()));
final Map<String, Exception> 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<String, Exception> processClassFieldsFailingLazily(Object configurationBean, Field[] fields) {
final Map<String, Exception> 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<Class<? extends Validator<?>>> validators =
new ArrayList<>(Collections.<Class<? extends Validator<?>>>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<Class<? extends Validator<?>>> validators =
new ArrayList<>(Collections.<Class<? extends Validator<?>>>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);
}
}
}
Expand Down Expand Up @@ -230,7 +280,7 @@ private void validateParameter(Collection<Class<? extends Validator<?>>> 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();
Expand All @@ -242,6 +292,27 @@ private void invokeValidatorMethods(Object configurationBean, Method[] methods)
}
}

private Map<String, Exception> invokeValidatorMethodsFailingLazily(Object configurationBean, Method[] methods) {
final Map<String, Exception> 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 <T> Class<? extends Converter<T>> findConverter(Class<T> clazz) {
for (ConverterFactory factory : converterFactories) {
Class<? extends Converter<T>> result = factory.getConverter(clazz);
Expand Down Expand Up @@ -292,7 +363,7 @@ public Map<String, String> dump() {
final Map<String, String> configurationDump = new HashMap<String, String>();

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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.github.joschi.jadconfig.response;

import java.util.Map;

public class ProcessingOutcome {

private final Object configurationBean;
private final Map<String, Exception> fieldProcessingProblems;
private final Map<String, Exception> validationMethodsProblems;

public ProcessingOutcome(final Object configurationBean,
final Map<String, Exception> fieldProcessingProblems,
final Map<String, Exception> 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<String, Exception> getFieldProcessingProblems() {
return fieldProcessingProblems;
}

public Map<String, Exception> getValidationMethodsProblems() {
return validationMethodsProblems;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.github.joschi.jadconfig.response;

import java.util.List;

public class ProcessingResponse {

private final List<ProcessingOutcome> outcomes;

public ProcessingResponse(List<ProcessingOutcome> outcomes) {
this.outcomes = outcomes;
}

public List<ProcessingOutcome> getOutcomes() {
return outcomes;
}

public boolean isSuccess() {
return outcomes.stream().noneMatch(ProcessingOutcome::hasProblems);
}
}

Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, Exception> fieldProcessingProblems = processingOutcome.getFieldProcessingProblems();
assertEquals(2, fieldProcessingProblems.size());
assertTrue(fieldProcessingProblems.containsKey("test.integer"));
assertTrue(fieldProcessingProblems.containsKey("test.integer.port"));

Map<String, Exception> validationMethodsProblems = processingOutcome.getValidationMethodsProblems();
assertEquals(1, validationMethodsProblems.size());
assertTrue(validationMethodsProblems.containsKey("myCustomValidatorMethod"));
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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");
Expand Down

0 comments on commit 757eb6d

Please sign in to comment.