Skip to content

Commit 1a13cfc

Browse files
committed
feat: importers configuration by annotation with placeholder
1 parent 69b56b0 commit 1a13cfc

20 files changed

Lines changed: 714 additions & 105 deletions

File tree

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public final class Constants {
2424
public static final String DEFAULT_MONGOCK_ORIGIN = "mongockChangeLog";
2525

2626
public static final String MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY = "internal.mongock.import.origin";
27-
public static final String MONGOCK_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY = "internal.mongock.import.emptyOriginAllowed";
27+
public static final String MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY = "internal.mongock.import.emptyOriginAllowed";
2828

2929
private Constants() {}
3030

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.processor;
17+
18+
import io.flamingock.internal.common.core.util.LoggerPreProcessor;
19+
20+
import javax.annotation.processing.RoundEnvironment;
21+
22+
public interface AnnotationProcessorPlugin {
23+
24+
void initialize(RoundEnvironment roundEnv, LoggerPreProcessor logger);
25+
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/discover/ChangeDiscoverer.java renamed to core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/processor/ChangeDiscoverer.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.flamingock.internal.common.core.discover;
16+
package io.flamingock.internal.common.core.processor;
1717

1818
import io.flamingock.internal.common.core.preview.CodePreviewChange;
1919
import io.flamingock.internal.common.core.util.LoggerPreProcessor;
2020

2121
import javax.annotation.processing.RoundEnvironment;
2222
import java.util.Collection;
23-
import java.util.Map;
2423

2524
public interface ChangeDiscoverer {
2625

27-
//TODO: move configuration properties to another interface
28-
Collection<CodePreviewChange> findAnnotatedChanges(RoundEnvironment roundEnv, LoggerPreProcessor logger, Map<String, String> properties);
26+
Collection<CodePreviewChange> findAnnotatedChanges(RoundEnvironment roundEnv, LoggerPreProcessor logger);
2927
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.processor;
17+
18+
import io.flamingock.internal.common.core.util.LoggerPreProcessor;
19+
20+
import javax.annotation.processing.RoundEnvironment;
21+
import java.util.Map;
22+
23+
public interface ConfigurationPropertiesProvider {
24+
25+
Map<String, String> getConfigurationProperties(RoundEnvironment roundEnv, LoggerPreProcessor logger);
26+
}
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.util;
17+
18+
import java.util.Objects;
19+
import java.util.Optional;
20+
import java.util.function.Predicate;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
/**
25+
* Generic configuration value parser for Flamingock configuration values.
26+
*
27+
* <p>Supported placeholder syntax:</p>
28+
* <ul>
29+
* <li>{@code ${name}}</li>
30+
* <li>{@code ${name:defaultValue}}</li>
31+
* </ul>
32+
*
33+
* <p>Notes:</p>
34+
* <ul>
35+
* <li>The placeholder {@code name} may include separators such as {@code .}, {@code -} and {@code _}.</li>
36+
* <li>The first {@code :} (if present) separates the name from the default value.</li>
37+
* <li>Whitespace around the name is allowed and ignored.</li>
38+
* <li>Nested placeholders (e.g. {@code ${a:${b}}}) are intentionally not supported.</li>
39+
* <li>If a value starts with ${ but is not a valid placeholder, this class throws an exception.
40+
* It must not be treated as a literal.</li>
41+
* </ul>
42+
*/
43+
public final class ConfigValueParser {
44+
45+
private ConfigValueParser() {
46+
// Utility class
47+
}
48+
49+
private static final Pattern PLACEHOLDER_PATTERN =
50+
Pattern.compile("^\\$\\{\\s*([^}:\\s]+)\\s*(?::([^}]*))?}$");
51+
52+
/**
53+
* Validator suitable for boolean-like values:
54+
* allowed values: "", "true", "false" (case-insensitive).
55+
*/
56+
public static final Predicate<String> BOOLEAN_VALUE_VALIDATOR =
57+
value -> value == null
58+
|| value.trim().isEmpty()
59+
|| "true".equalsIgnoreCase(value.trim())
60+
|| "false".equalsIgnoreCase(value.trim());
61+
62+
/**
63+
* Parses the provided value and classifies it as EMPTY, PLACEHOLDER, or LITERAL.
64+
*
65+
* <p>This overload performs only placeholder syntax validation. Literals and placeholder defaults
66+
* are accepted as-is.</p>
67+
*
68+
* @param valueName logical name used in exception messages (e.g. "origin", "emptyOriginAllowed")
69+
* @param raw raw input value
70+
* @return parsed configuration value (never invalid; invalid inputs throw)
71+
*/
72+
public static ConfigValue parse(String valueName, String raw) {
73+
return parse(valueName, raw, null);
74+
}
75+
76+
/**
77+
* Parses the provided value and classifies it as EMPTY, PLACEHOLDER, or LITERAL, while also validating:
78+
* <ul>
79+
* <li>Placeholder syntax (always, when value starts with ${)</li>
80+
* <li>Placeholder default value (only when present, using {@code valueValidator} if provided)</li>
81+
* <li>Literal value (using {@code valueValidator} if provided)</li>
82+
* </ul>
83+
*
84+
* <p>Fail-fast rule: if the value starts with ${} but is not a valid placeholder,
85+
* this method throws {@link IllegalArgumentException}.</p>
86+
*
87+
* @param valueName logical name used in exception messages (e.g. "origin", "emptyOriginAllowed")
88+
* @param raw raw input value
89+
* @param valueValidator validator applied to literal values and placeholder default values (may be null)
90+
* @return parsed configuration value (never invalid; invalid inputs throw)
91+
*/
92+
public static ConfigValue parse(String valueName, String raw, Predicate<String> valueValidator) {
93+
String value = normalise(raw);
94+
95+
if (value.isEmpty()) {
96+
return ConfigValue.empty(raw);
97+
}
98+
99+
if (value.startsWith("${")) {
100+
Placeholder placeholder = parsePlaceholderOrThrow(valueName, raw, value);
101+
102+
if (placeholder.hasDefault()) {
103+
validateValue(valueName, raw, placeholder.getDefaultValue(), valueValidator);
104+
}
105+
106+
return ConfigValue.placeholder(raw, placeholder);
107+
}
108+
109+
// Literal
110+
validateValue(valueName, raw, value, valueValidator);
111+
return ConfigValue.literal(raw, value);
112+
}
113+
114+
/**
115+
* Parses a placeholder if (and only if) the entire value is a placeholder.
116+
*
117+
* <p>Returns {@link Optional#empty()} if the input is {@code null}, blank, or not a placeholder.</p>
118+
*
119+
* <p>Important: if the value starts with ${} but is not a valid placeholder,
120+
* this method throws {@link IllegalArgumentException}.</p>
121+
*/
122+
public static Optional<Placeholder> parsePlaceholder(String valueName, String raw) {
123+
ConfigValue parsed = parse(valueName, raw, null);
124+
return parsed.getPlaceholder();
125+
}
126+
127+
/**
128+
* Returns {@code true} if the supplied value is syntactically a valid placeholder.
129+
*
130+
* <p>This method never throws. If you need fail-fast behaviour for values starting with ${,
131+
* use {@link #parse(String, String)}.</p>
132+
*/
133+
public static boolean isValidPlaceholder(String raw) {
134+
String value = normalise(raw);
135+
if (value.isEmpty()) {
136+
return false;
137+
}
138+
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
139+
if (!matcher.matches()) {
140+
return false;
141+
}
142+
String name = matcher.group(1);
143+
return name != null && !name.trim().isEmpty();
144+
}
145+
146+
private static Placeholder parsePlaceholderOrThrow(String valueName, String raw, String normalised) {
147+
Matcher matcher = PLACEHOLDER_PATTERN.matcher(normalised);
148+
if (!matcher.matches()) {
149+
throw new IllegalArgumentException(buildPlaceholderSyntaxError(valueName, raw));
150+
}
151+
152+
String name = matcher.group(1);
153+
String defaultValue = matcher.group(2); // null if ':' not present; may be "" if ${name:}
154+
155+
if (name == null || name.trim().isEmpty()) {
156+
throw new IllegalArgumentException(buildPlaceholderSyntaxError(valueName, raw));
157+
}
158+
159+
return new Placeholder(name.trim(), defaultValue);
160+
}
161+
162+
private static void validateValue(String valueName,
163+
String raw,
164+
String value,
165+
Predicate<String> validator) {
166+
Predicate<String> effectiveValidator = validator != null ? validator : v -> true;
167+
if (!effectiveValidator.test(value)) {
168+
throw new IllegalArgumentException(buildLiteralOrDefaultError(valueName, raw));
169+
}
170+
}
171+
172+
private static String buildPlaceholderSyntaxError(String valueName, String raw) {
173+
return "Invalid placeholder syntax for " + valueName + ": '" + raw + "'. "
174+
+ "Expected ${name} or ${name:defaultValue}.";
175+
}
176+
177+
private static String buildLiteralOrDefaultError(String valueName, String raw) {
178+
return "Invalid value for " + valueName + ": '" + raw + "'.";
179+
}
180+
181+
private static String normalise(String s) {
182+
return s == null ? "" : s.trim();
183+
}
184+
185+
// --------------------------------------------------------
186+
// Value Objects
187+
// --------------------------------------------------------
188+
189+
public enum ValueType {
190+
EMPTY,
191+
LITERAL,
192+
PLACEHOLDER
193+
}
194+
195+
/**
196+
* Result of parsing an input configuration value.
197+
*
198+
* <p>Exactly one of {@code literal} or {@code placeholder} will be present when type is LITERAL / PLACEHOLDER.
199+
* When type is EMPTY, neither is present.</p>
200+
*/
201+
public static final class ConfigValue {
202+
203+
private final ValueType type;
204+
private final String raw;
205+
private final String literal;
206+
private final Placeholder placeholder;
207+
208+
private ConfigValue(ValueType type, String raw, String literal, Placeholder placeholder) {
209+
this.type = Objects.requireNonNull(type, "type");
210+
this.raw = raw;
211+
this.literal = literal;
212+
this.placeholder = placeholder;
213+
}
214+
215+
public static ConfigValue empty(String raw) {
216+
return new ConfigValue(ValueType.EMPTY, raw, null, null);
217+
}
218+
219+
public static ConfigValue literal(String raw, String literal) {
220+
return new ConfigValue(ValueType.LITERAL, raw, Objects.requireNonNull(literal, "literal"), null);
221+
}
222+
223+
public static ConfigValue placeholder(String raw, Placeholder placeholder) {
224+
return new ConfigValue(ValueType.PLACEHOLDER, raw, null, Objects.requireNonNull(placeholder, "placeholder"));
225+
}
226+
227+
public ValueType getType() {
228+
return type;
229+
}
230+
231+
public String getRaw() {
232+
return raw;
233+
}
234+
235+
public boolean isEmpty() {
236+
return type == ValueType.EMPTY;
237+
}
238+
239+
public boolean isLiteral() {
240+
return type == ValueType.LITERAL;
241+
}
242+
243+
public boolean isPlaceholder() {
244+
return type == ValueType.PLACEHOLDER;
245+
}
246+
247+
public Optional<String> getLiteral() {
248+
return Optional.ofNullable(literal);
249+
}
250+
251+
public Optional<Placeholder> getPlaceholder() {
252+
return Optional.ofNullable(placeholder);
253+
}
254+
}
255+
256+
/**
257+
* Immutable representation of a parsed placeholder.
258+
*
259+
* <p>The {@code defaultValue} is:</p>
260+
* <ul>
261+
* <li>{@code null} when no {@code :} is present (e.g. {@code ${name}})</li>
262+
* <li>possibly empty when {@code :} is present (e.g. {@code ${name:}})</li>
263+
* </ul>
264+
*/
265+
public static final class Placeholder {
266+
267+
private final String name;
268+
private final String defaultValue;
269+
270+
private Placeholder(String name, String defaultValue) {
271+
this.name = Objects.requireNonNull(name, "name");
272+
this.defaultValue = defaultValue;
273+
}
274+
275+
public String getName() {
276+
return name;
277+
}
278+
279+
public String getDefaultValue() {
280+
return defaultValue;
281+
}
282+
283+
public boolean hasDefault() {
284+
return defaultValue != null;
285+
}
286+
}
287+
}

0 commit comments

Comments
 (0)