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 2 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
140 changes: 106 additions & 34 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,7 @@

import com.github.joschi.jadconfig.converters.NoConverter;
import com.github.joschi.jadconfig.converters.StringConverter;
import com.github.joschi.jadconfig.response.ProcessingResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -19,9 +20,7 @@
import java.util.Objects;
import java.util.Optional;

import static com.github.joschi.jadconfig.ReflectionUtils.getAllFields;
import static com.github.joschi.jadconfig.ReflectionUtils.getAllMethods;
import static com.github.joschi.jadconfig.ReflectionUtils.invokeMethodsWithAnnotation;
import static com.github.joschi.jadconfig.ReflectionUtils.*;
bernd marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 +84,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 @@ -103,53 +104,103 @@ public void process() throws RepositoryException, ValidationException {
}
}

/**
* 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 {
ProcessingResponse response = new ProcessingResponse();
bernd marked this conversation as resolved.
Show resolved Hide resolved

for (Repository repository : repositories) {
LOG.debug("Opening repository {}", repository);
repository.open();
}

for (Object configurationBean : configurationBeans) {
LOG.debug("Processing configuration bean {}", configurationBean);

response.addOutcome(
configurationBean,
processClassFieldsFailingLazily(configurationBean, getAllFields(configurationBean.getClass())),
invokeValidatorMethodsFailingLazily(configurationBean, getAllMethods(configurationBean.getClass()))
);
}
return response;
}


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) {
Map<String, Exception> fieldProcessingProblems = new HashMap<>();
bernd marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -242,6 +293,27 @@ private void invokeValidatorMethods(Object configurationBean, Method[] methods)
}
}

private Map<String, Exception> invokeValidatorMethodsFailingLazily(Object configurationBean, Method[] methods) {
Map<String, Exception> problems = new HashMap<>();
bernd marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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(e -> toMessage(processingOutcome, e)),
processingOutcome.getValidationMethodsProblems().values().stream().map(e -> toMessage(processingOutcome, e))
)).forEach(stringBuilder::add);
return String.join("\n", stringBuilder);
}

private static String toMessage(ProcessingOutcome processingOutcome, Exception exception) {
// TODO: should we distinct between field processing problem and validation method?
// TODO: should we include class name of the bean or not?
bernd marked this conversation as resolved.
Show resolved Hide resolved
return exception.getMessage();
}

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,29 @@
package com.github.joschi.jadconfig.response;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class ProcessingResponse {

private List<ProcessingOutcome> outcomes;

public ProcessingResponse() {
this.outcomes = new LinkedList<>();
}

public void addOutcome(final Object configurationBean,
final Map<String, Exception> fieldProcessingProblems,
final Map<String, Exception> validationMethodsProblems) {
outcomes.add(new ProcessingOutcome(configurationBean, fieldProcessingProblems, validationMethodsProblems));
}

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