Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config processing with lazy error handling #127

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
bernd marked this conversation as resolved.
Show resolved Hide resolved

if (!"Test".equals(myString)) {
throw new ValidationException("BOOM");
Expand Down
Loading