Skip to content

Commit

Permalink
Refactored annotation parser
Browse files Browse the repository at this point in the history
- added support for extracting multiple values
- extracted parser into separate class
- updated tests
  • Loading branch information
egoettelmann committed Jul 13, 2023
1 parent 44d0c36 commit ec8d833
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 214 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.egoettelmann.spring.configuration.extensions.samples.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SampleConfig2 {

/**
* Custom config value injected by @Value, with default value referencing another config.
*/
@Value("${sample.custom.conf2:${sample.custom.conf}}")
private String customConfig2;

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
"description": "Config including unicode characters.\n",
"sourceType": "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig",
"defaultValue": null
},
{
"name": "sample.custom.conf",
"type": "java.lang.String",
"description": "Custom config value injected by @Value, with default value referencing another config.\n",
"sourceType": "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig2",
"defaultValue": null
},
{
"name": "sample.custom.conf2",
"type": "java.lang.String",
"description": "Custom config value injected by @Value, with default value referencing another config.\n",
"sourceType": "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig2",
"defaultValue": "${sample.custom.conf}"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"properties" : [ {
"name" : "sample.app.title",
"type" : "java.lang.String",
"description": "Sample app title injected through @ConfigurationProperties",
"description" : "Sample app title injected through @ConfigurationProperties",
"defaultValue" : "Sample Application",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
Expand All @@ -12,17 +12,31 @@
}, {
"name" : "sample.custom.conf",
"type" : "java.lang.String",
"description": "Custom config value injected by @Value.\n",
"description" : "Custom config value injected by @Value.\n\nCustom config value injected by @Value, with default value referencing another config.",
"defaultValue" : "Custom Config Value",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"sourceType" : "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig"
}, {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"sourceType" : "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig2"
} ]
}, {
"name" : "sample.custom.conf2",
"type" : "java.lang.String",
"description" : "Custom config value injected by @Value, with default value referencing another config.\n",
"defaultValue" : "${sample.custom.conf}",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"sourceType" : "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig2"
} ]
}, {
"name" : "sample.unicode.chars",
"type" : "java.lang.String",
"description": "Config including unicode characters.\n",
"description" : "Config including unicode characters.\n",
"defaultValue" : "\u0000\u0001\u0002\u0003\u0004\u0005",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"artifacts" : [ {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"version" : "0.0.7-SNAPSHOT",
"version" : "0.1.3-SNAPSHOT",
"name" : "Spring Configuration Extensions: Samples",
"description" : "Samples for illustrating Spring Configuration Extensions and performing tests",
"properties" : [ {
"name" : "sample.app.title",
"type" : "java.lang.String",
"description": "Sample app title injected through @ConfigurationProperties",
"description" : "Sample app title injected through @ConfigurationProperties",
"defaultValue" : "Sample Application",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
Expand All @@ -18,17 +18,31 @@
}, {
"name" : "sample.custom.conf",
"type" : "java.lang.String",
"description": "Custom config value injected by @Value.\n",
"description" : "Custom config value injected by @Value.\n\nCustom config value injected by @Value, with default value referencing another config.",
"defaultValue" : "Custom Config Value",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"sourceType" : "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig"
}, {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"sourceType" : "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig2"
} ]
}, {
"name" : "sample.custom.conf2",
"type" : "java.lang.String",
"description" : "Custom config value injected by @Value, with default value referencing another config.\n",
"defaultValue" : "${sample.custom.conf}",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
"artifactId" : "spring-configuration-extensions-samples",
"sourceType" : "com.github.egoettelmann.spring.configuration.extensions.samples.config.SampleConfig2"
} ]
}, {
"name" : "sample.unicode.chars",
"type" : "java.lang.String",
"description": "Config including unicode characters.\n",
"description" : "Config including unicode characters.\n",
"defaultValue" : "\u0000\u0001\u0002\u0003\u0004\u0005",
"sourceTypes" : [ {
"groupId" : "com.github.egoettelmann",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;

@SupportedAnnotationTypes(ElementReader.VALUE_ANNOTATION_CLASS)
Expand Down Expand Up @@ -45,12 +44,11 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
for (final TypeElement annotation : annotations) {
for (final Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
try {
final Optional<ValueAnnotationMetadata> metadata = this.elementReader.read(element);
if (!metadata.isPresent()) {
continue;
final List<ValueAnnotationMetadata> results = this.elementReader.read(element);
for (final ValueAnnotationMetadata metadata : results) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Found @Value annotation with property '" + metadata.getName() + "'");
metadataList.add(metadata);
}
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Found @Value annotation with property '" + metadata.get().getName() + "'");
metadataList.add(metadata.get());
} catch (final Exception e) {
final String errorMessage = "Error while processing @Value annotations on " + element.getSimpleName();
if (this.failOnError) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.github.egoettelmann.spring.configuration.extensions.annotationprocessor.value.core;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ValueAnnotationMetadataBuilder {

private static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]*)}");

private static final String DEFAULT_VALUE_SEPARATOR = ":";

private final String rawValue;
private String type;
private String description;
Expand Down Expand Up @@ -38,43 +34,23 @@ public ValueAnnotationMetadataBuilder sourceType(final String sourceType) {
return this;
}

public Optional<ValueAnnotationMetadata> build() {
ValueAnnotationMetadata metadata = new ValueAnnotationMetadata();

// Adding metadata
metadata.setType(this.type);
metadata.setDescription(this.description);
metadata.setSourceType(this.sourceType);

// Extracting property name
final Optional<String> definition = extractPropertyDefinition(this.rawValue);
if (!definition.isPresent()) {
return Optional.empty();
}
final String definitionValue = definition.get();
final String[] values = definitionValue.split(DEFAULT_VALUE_SEPARATOR, 2);
metadata.setName(values[0].trim());

// Extracting default value of defined
if (values.length > 1) {
metadata.setDefaultValue(values[1].trim());
}
public List<ValueAnnotationMetadata> build() {
final List<ValueAnnotationMetadata> results = new ArrayList<>();

return Optional.of(metadata);
}

private static Optional<String> extractPropertyDefinition(final String rawValue) {
final Matcher matcher = PATTERN.matcher(rawValue);
if (!matcher.find()) {
return Optional.empty();
}
final ValueAnnotationParser parser = new ValueAnnotationParser();
final Map<String, String> properties = parser.parse(this.rawValue);

final String match = matcher.group(1);
if (match == null) {
return Optional.empty();
for (final Map.Entry<String, String> entry : properties.entrySet()) {
final ValueAnnotationMetadata metadata = new ValueAnnotationMetadata();
metadata.setType(this.type);
metadata.setDescription(this.description);
metadata.setSourceType(this.sourceType);
metadata.setName(entry.getKey());
metadata.setDefaultValue(entry.getValue());
results.add(metadata);
}

return Optional.of(match.trim());
return results;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.github.egoettelmann.spring.configuration.extensions.annotationprocessor.value.core;

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

/**
* Parser to resolve property placeholders within value annotations
*/
public class ValueAnnotationParser {

private final String prefix = "${";
private final String suffix = "}";
private final String separator = ":";
private final String opposingSuffix = "{";

/**
* Parses a value and returns a map of properties found, with:
* - the key being the property
* - the value being the default value (can be null)
*
* @param value the value to parse
* @return the map of parse properties and their default values
* @throws IllegalArgumentException if the provided value is invalid and cannot be parsed
*/
public Map<String, String> parse(final String value) throws IllegalArgumentException {
final Map<String, String> values = new HashMap<>();

// Searching first occurrence of PREFIX
int startIdx = value.indexOf(this.prefix);
if (startIdx == -1) {
// No occurrence found
return values;
}
startIdx += this.prefix.length();

// Searching end indexes (matching SEPARATOR and SUFFIX positions)
final String afterPrefix = value.substring(startIdx);
int[] indexes = this.findIndexes(afterPrefix);
int separatorIdx = indexes[0];
int endIdx = indexes[1];

// Retrieving property and default value
String property = afterPrefix.substring(0, endIdx).trim();
String defaultValue = null;

// Separator found: splitting to retrieve values
if (separatorIdx > -1) {
property = afterPrefix.substring(0, separatorIdx).trim();
defaultValue = afterPrefix.substring(separatorIdx + this.separator.length(), endIdx).trim();
}
values.put(property, defaultValue);

// Parsing default value
if (defaultValue != null) {
values.putAll(this.parse(defaultValue));
}

// Parsing remainder recursively
if (endIdx + this.suffix.length() < afterPrefix.length()) {
final String remainder = afterPrefix.substring(endIdx + this.suffix.length());
values.putAll(this.parse(remainder));
}

// Returning values
return values;
}

/**
* Finds the indexes of the separator and the suffix within the provided string, with:
* - at position 0, the index of the separator (can be -1)
* - at position 1, the index of the ending suffix
*
* @param string the string to search
* @return the indexes
* @throws IllegalArgumentException if no matching ending suffix has been found
*/
private int[] findIndexes(final String string) throws IllegalArgumentException {
int[] result = {-1, -1};
int depth = 0;

int index = 0;
while (index < string.length()) {
final String substring = string.substring(index);

// Prefix encountered: incrementing depth
if (substring.startsWith(this.prefix)) {
// Checking that separator has been encountered
if (result[0] < 0) {
throw new IllegalArgumentException(String.format("Prefix encountered before separator in '%s'", string));
}

// Incrementing depth
depth++;
index += this.prefix.length();
continue;
}

// Opposing suffix encountered
if (substring.startsWith(this.opposingSuffix)) {
// Checking that separator has been encountered
if (result[0] < 0) {
throw new IllegalArgumentException(String.format("Opposing suffix encountered before separator in '%s'", string));
}

// Incrementing depth
depth++;
index += this.opposingSuffix.length();
continue;
}

// Separator encountered: checking depth
if (substring.startsWith(this.separator)) {
// No separator found yet: saving index
if (result[0] == -1) {
result[0] = index;
}

// Continuing
index += this.separator.length();
continue;
}

// Suffix encountered: checking depth
if (substring.startsWith(this.suffix)) {
// Depth not 0: decrementing depth, ignoring and continuing
if (depth > 0) {
depth--;
index += this.suffix.length();
continue;
}

// End found
result[1] = index;
return result;
}

// Neither prefix nor suffix found: continuing
index++;
}

// No end found
throw new IllegalArgumentException(String.format("No ending suffix found in '%s'", string));
}

}
Loading

0 comments on commit ec8d833

Please sign in to comment.