Skip to content

Commit

Permalink
Config processing with lazy error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
luk-kaminski committed Mar 27, 2024
1 parent 18c9f03 commit 28af121
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 34 deletions.
136 changes: 102 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.*;

/**
* 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,99 @@ public void process() throws RepositoryException, ValidationException {
}
}

/**
* 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 return them in single response,
* allowing identifying many problems at once.
*
* @return Response object, containing encountered problems if processing was not successful.
*/
public ProcessingResponse processFailingLazily() throws RepositoryException {

ProcessingResponse response = new ProcessingResponse();

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<>();
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 +289,27 @@ private void invokeValidatorMethods(Object configurationBean, Method[] methods)
}
}

private Map<String, Exception> invokeValidatorMethodsFailingLazily(Object configurationBean, Method[] methods) {
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
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,47 @@
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.Test;

import java.util.HashMap;
import java.util.Map;

import static org.junit.Assert.*;

public class JadConfigLazyProcessingTest {

private JadConfig jadConfig;
private Repository repository;


@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 = new InMemoryRepository(properties);
ValidatedConfigurationBean configurationBean = new ValidatedConfigurationBean();
jadConfig = new JadConfig(repository, configurationBean);

ProcessingResponse response = jadConfig.processFailingLazily();
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("validate"));
}
}

0 comments on commit 28af121

Please sign in to comment.