View Javadoc
1   /*
2    *    Copyright 2006-2026 the original author or authors.
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    *       https://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 org.mybatis.generator.config.xml;
17  
18  import static org.mybatis.generator.internal.util.StringUtility.isTrue;
19  import static org.mybatis.generator.internal.util.StringUtility.parseNullableBoolean;
20  import static org.mybatis.generator.internal.util.StringUtility.trimToNull;
21  import static org.mybatis.generator.internal.util.messages.Messages.getString;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.net.URI;
26  import java.net.URL;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.Objects;
30  import java.util.Optional;
31  import java.util.Properties;
32  
33  import org.jspecify.annotations.Nullable;
34  import org.mybatis.generator.config.ClientGeneratorConfiguration;
35  import org.mybatis.generator.config.ColumnOverride;
36  import org.mybatis.generator.config.ColumnRenamingRule;
37  import org.mybatis.generator.config.CommentGeneratorConfiguration;
38  import org.mybatis.generator.config.Configuration;
39  import org.mybatis.generator.config.ConnectionFactoryConfiguration;
40  import org.mybatis.generator.config.Context;
41  import org.mybatis.generator.config.DomainObjectRenamingRule;
42  import org.mybatis.generator.config.GeneratedKey;
43  import org.mybatis.generator.config.IgnoredColumn;
44  import org.mybatis.generator.config.IgnoredColumnException;
45  import org.mybatis.generator.config.IgnoredColumnPattern;
46  import org.mybatis.generator.config.JDBCConnectionConfiguration;
47  import org.mybatis.generator.config.JavaTypeResolverConfiguration;
48  import org.mybatis.generator.config.ModelGeneratorConfiguration;
49  import org.mybatis.generator.config.ModelType;
50  import org.mybatis.generator.config.NullableProperties;
51  import org.mybatis.generator.config.PluginConfiguration;
52  import org.mybatis.generator.config.Property;
53  import org.mybatis.generator.config.SqlMapGeneratorConfiguration;
54  import org.mybatis.generator.config.TableConfiguration;
55  import org.mybatis.generator.exception.XMLParserException;
56  import org.mybatis.generator.internal.ObjectFactory;
57  import org.w3c.dom.Element;
58  import org.w3c.dom.NamedNodeMap;
59  import org.w3c.dom.Node;
60  import org.w3c.dom.NodeList;
61  
62  /**
63   * This class parses configuration files into the new Configuration API.
64   *
65   * @author Jeff Butler
66   */
67  public class MyBatisGeneratorConfigurationParser {
68      private final Properties extraProperties;
69      private final Properties configurationProperties;
70      private final List<String> warnings;
71  
72      public MyBatisGeneratorConfigurationParser(@Nullable Properties extraProperties, List<String> warnings) {
73          this.extraProperties = Objects.requireNonNullElseGet(extraProperties, Properties::new);
74          configurationProperties = new Properties();
75          this.warnings = warnings;
76      }
77  
78      public Configuration parseConfiguration(Element rootNode) throws XMLParserException {
79          Configuration configuration = new Configuration();
80  
81          NodeList nodeList = rootNode.getChildNodes();
82          for (int i = 0; i < nodeList.getLength(); i++) {
83              Node childNode = nodeList.item(i);
84  
85              if (childNode.getNodeType() != Node.ELEMENT_NODE) {
86                  continue;
87              }
88  
89              switch (childNode.getNodeName()) {
90              case "properties" ->  //$NON-NLS-1$
91                      parsePropertiesElement(childNode);
92              case "classPathEntry" ->  //$NON-NLS-1$
93                      configuration.addClasspathEntry(parseClassPathEntry(childNode));
94              case "context" ->  //$NON-NLS-1$
95                      configuration.addContext(parseContext(childNode));
96              default -> {
97                  // Ignore unrecognized elements
98              }
99              }
100         }
101 
102         return configuration;
103     }
104 
105     protected void parsePropertiesElement(Node node) throws XMLParserException {
106         NullableProperties attributes = parseAttributes(node);
107         String resource = attributes.getProperty("resource"); //$NON-NLS-1$
108         String url = attributes.getProperty("url"); //$NON-NLS-1$
109 
110         if (resource == null && url == null) {
111             throw new XMLParserException(getString("RuntimeError.14")); //$NON-NLS-1$
112         }
113 
114         if (resource != null && url != null) {
115             throw new XMLParserException(getString("RuntimeError.14")); //$NON-NLS-1$
116         }
117 
118         if (resource != null) {
119             loadPropertiesFromResource(resource);
120         } else {
121             loadPropertiesFromURL(url);
122         }
123     }
124 
125     private void loadPropertiesFromResource(String resource) throws XMLParserException {
126         try {
127             URL resourceUrl = ObjectFactory.getResource(resource)
128                     .orElseThrow(() -> new XMLParserException(getString("RuntimeError.15", resource)));
129             InputStream inputStream = resourceUrl.openConnection().getInputStream();
130             configurationProperties.load(inputStream);
131             inputStream.close();
132         } catch (IOException e) {
133             throw new XMLParserException(getString("RuntimeError.16", resource)); //$NON-NLS-1$
134         }
135     }
136 
137     private void loadPropertiesFromURL(String url) throws XMLParserException {
138         try {
139             URL resourceUrl = URI.create(url).toURL();
140             InputStream inputStream = resourceUrl.openConnection().getInputStream();
141             configurationProperties.load(inputStream);
142             inputStream.close();
143         } catch (IOException e) {
144             throw new XMLParserException(getString("RuntimeError.17", url)); //$NON-NLS-1$
145         }
146     }
147 
148     private Context parseContext(Node node) {
149         NullableProperties attributes = parseAttributes(node);
150         String defaultModelType = attributes.getProperty("defaultModelType"); //$NON-NLS-1$
151         String targetRuntime = attributes.getProperty("targetRuntime"); //$NON-NLS-1$
152         String introspectedColumnImpl = attributes.getProperty("introspectedColumnImpl"); //$NON-NLS-1$
153         String id = attributes.getProperty("id"); //$NON-NLS-1$
154         assert id != null;
155         ModelType dmt =
156                 defaultModelType == null ? null : ModelType.getModelType(defaultModelType);
157 
158         Context.Builder builder = new Context.Builder()
159                 .withId(id)
160                 .withDefaultModelType(dmt)
161                 .withIntrospectedColumnImpl(introspectedColumnImpl)
162                 .withTargetRuntime(targetRuntime);
163 
164         NodeList nodeList = node.getChildNodes();
165         for (int i = 0; i < nodeList.getLength(); i++) {
166             Node childNode = nodeList.item(i);
167 
168             if (childNode.getNodeType() != Node.ELEMENT_NODE) {
169                 continue;
170             }
171 
172             switch (childNode.getNodeName()) {
173             case "property" ->  //$NON-NLS-1$
174                     parseProperty(childNode).ifPresent(builder::withProperty);
175             case "plugin" ->  //$NON-NLS-1$
176                     builder.withPluginConfiguration(parsePlugin(childNode));
177             case "commentGenerator" ->  //$NON-NLS-1$
178                     builder.withCommentGeneratorConfiguration(parseCommentGenerator(childNode));
179             case "jdbcConnection" ->  //$NON-NLS-1$
180                     builder.withJdbcConnectionConfiguration(parseJdbcConnection(childNode));
181             case "connectionFactory" ->  //$NON-NLS-1$
182                     builder.withConnectionFactoryConfiguration(parseConnectionFactory(childNode));
183             case "modelGenerator" ->  //$NON-NLS-1$
184                     builder.withModelGeneratorConfiguration(parseModelGenerator(childNode));
185             case "javaModelGenerator" -> { //$NON-NLS-1$
186                 warnings.add(getString("Warning.33")); //$NON-NLS-1$
187                 builder.withModelGeneratorConfiguration(parseModelGenerator(childNode));
188             }
189             case "javaTypeResolver" ->  //$NON-NLS-1$
190                     builder.withJavaTypeResolverConfiguration(parseJavaTypeResolver(childNode));
191             case "sqlMapGenerator" ->  //$NON-NLS-1$
192                     builder.withSqlMapGeneratorConfiguration(parseSqlMapGenerator(childNode));
193             case "clientGenerator" ->  //$NON-NLS-1$
194                     builder.withClientGeneratorConfiguration(parseClientGenerator(childNode, id));
195             case "javaClientGenerator" -> { //$NON-NLS-1$
196                 warnings.add(getString("Warning.34")); //$NON-NLS-1$
197                 builder.withClientGeneratorConfiguration(parseClientGenerator(childNode, id));
198             }
199             case "table" ->  //$NON-NLS-1$
200                     builder.withTableConfiguration(parseTable(childNode));
201             default -> {
202                 // Ignore unrecognized elements
203             }
204             }
205         }
206 
207         return builder.build();
208     }
209 
210     protected SqlMapGeneratorConfiguration parseSqlMapGenerator(Node node) {
211         NullableProperties attributes = parseAttributes(node);
212         String targetPackage = attributes.getProperty("targetPackage"); //$NON-NLS-1$
213         String targetProject = attributes.getProperty("targetProject"); //$NON-NLS-1$
214         Properties properties = parseProperties(node.getChildNodes());
215         return new SqlMapGeneratorConfiguration.Builder()
216                 .withTargetPackage(targetPackage)
217                 .withTargetProject(targetProject)
218                 .withProperties(properties)
219                 .build();
220     }
221 
222     protected TableConfiguration parseTable(Node node) {
223         NullableProperties attributes = parseAttributes(node);
224         String catalog = attributes.getProperty("catalog"); //$NON-NLS-1$
225         String schema = attributes.getProperty("schema"); //$NON-NLS-1$
226         String tableName = attributes.getProperty("tableName"); //$NON-NLS-1$
227         String domainObjectName = attributes.getProperty("domainObjectName"); //$NON-NLS-1$
228         String alias = attributes.getProperty("alias"); //$NON-NLS-1$
229         String modelType = attributes.getProperty("modelType"); //$NON-NLS-1$
230         String mapperName = attributes.getProperty("mapperName"); //$NON-NLS-1$
231         String sqlProviderName = attributes.getProperty("sqlProviderName"); //$NON-NLS-1$
232 
233         TableConfiguration.Builder builder = new TableConfiguration.Builder()
234                 .withModelType(modelType)
235                 .withCatalog(catalog)
236                 .withSchema(schema)
237                 .withTableName(tableName)
238                 .withDomainObjectName(domainObjectName)
239                 .withAlias(alias)
240                 .withMapperName(mapperName)
241                 .withSqlProviderName(sqlProviderName);
242 
243         String enableInsert = attributes.getProperty("enableInsert"); //$NON-NLS-1$
244         if (enableInsert != null) {
245             builder.withInsertStatementEnabled(isTrue(enableInsert));
246         }
247 
248         String enableSelectByPrimaryKey = attributes.getProperty("enableSelectByPrimaryKey"); //$NON-NLS-1$
249         if (enableSelectByPrimaryKey != null) {
250             builder.withSelectByPrimaryKeyStatementEnabled(isTrue(enableSelectByPrimaryKey));
251         }
252 
253         String enableSelectByExample = attributes.getProperty("enableSelectByExample"); //$NON-NLS-1$
254         if (enableSelectByExample != null) {
255             builder.withSelectByExampleStatementEnabled(isTrue(enableSelectByExample));
256         }
257 
258         String enableUpdateByPrimaryKey = attributes.getProperty("enableUpdateByPrimaryKey"); //$NON-NLS-1$
259         if (enableUpdateByPrimaryKey != null) {
260             builder.withUpdateByPrimaryKeyStatementEnabled(isTrue(enableUpdateByPrimaryKey));
261         }
262 
263         String enableDeleteByPrimaryKey = attributes.getProperty("enableDeleteByPrimaryKey"); //$NON-NLS-1$
264         if (enableDeleteByPrimaryKey != null) {
265             builder.withDeleteByPrimaryKeyStatementEnabled(isTrue(enableDeleteByPrimaryKey));
266         }
267 
268         String enableDeleteByExample = attributes.getProperty("enableDeleteByExample"); //$NON-NLS-1$
269         if (enableDeleteByExample != null) {
270             builder.withDeleteByExampleStatementEnabled(isTrue(enableDeleteByExample));
271         }
272 
273         String enableCountByExample = attributes.getProperty("enableCountByExample"); //$NON-NLS-1$
274         if (enableCountByExample != null) {
275             builder.withCountByExampleStatementEnabled(isTrue(enableCountByExample));
276         }
277 
278         String enableUpdateByExample = attributes.getProperty("enableUpdateByExample"); //$NON-NLS-1$
279         if (enableUpdateByExample != null) {
280             builder.withUpdateByExampleStatementEnabled(isTrue(enableUpdateByExample));
281         }
282 
283         String escapeWildcards = attributes.getProperty("escapeWildcards"); //$NON-NLS-1$
284         if (escapeWildcards != null) {
285             builder.withWildcardEscapingEnabled(isTrue(escapeWildcards));
286         }
287 
288         String delimitIdentifiers = attributes.getProperty("delimitIdentifiers"); //$NON-NLS-1$
289         if (delimitIdentifiers != null) {
290             builder.withDelimitIdentifiers(isTrue(delimitIdentifiers));
291         }
292 
293         String delimitAllColumns = attributes.getProperty("delimitAllColumns"); //$NON-NLS-1$
294         if (delimitAllColumns != null) {
295             builder.withAllColumnDelimitingEnabled(isTrue(delimitAllColumns));
296         }
297 
298         NodeList nodeList = node.getChildNodes();
299         for (int i = 0; i < nodeList.getLength(); i++) {
300             Node childNode = nodeList.item(i);
301 
302             if (childNode.getNodeType() != Node.ELEMENT_NODE) {
303                 continue;
304             }
305 
306             switch (childNode.getNodeName()) {
307             case "property" ->  //$NON-NLS-1$
308                     parseProperty(childNode).ifPresent(builder::withProperty);
309             case "columnOverride" ->  //$NON-NLS-1$
310                     builder.withColumnOverride(parseColumnOverride(childNode));
311             case "ignoreColumn" ->  //$NON-NLS-1$
312                     builder.withIgnoredColumn(parseIgnoreColumn(childNode));
313             case "ignoreColumnsByRegex" ->  //$NON-NLS-1$
314                     builder.withIgnoredColumnPattern(parseIgnoreColumnByRegex(childNode));
315             case "generatedKey" ->  //$NON-NLS-1$
316                     builder.withGeneratedKey(parseGeneratedKey(childNode));
317             case "domainObjectRenamingRule" ->  //$NON-NLS-1$
318                     builder.withDomainObjectRenamingRule(parseDomainObjectRenamingRule(childNode));
319             case "columnRenamingRule" ->  //$NON-NLS-1$
320                     builder.withColumnRenamingRule(parseColumnRenamingRule(childNode));
321             default -> {
322                 // Ignore unrecognized elements
323             }
324             }
325         }
326 
327         return builder.build();
328     }
329 
330     private ColumnOverride parseColumnOverride(Node node) {
331         NullableProperties attributes = parseAttributes(node);
332         String column = attributes.getProperty("column"); //$NON-NLS-1$
333         String javaProperty = attributes.getProperty("property"); //$NON-NLS-1$
334         String javaType = attributes.getProperty("javaType"); //$NON-NLS-1$
335         String jdbcType = attributes.getProperty("jdbcType"); //$NON-NLS-1$
336         String typeHandler = attributes.getProperty("typeHandler"); //$NON-NLS-1$
337         String delimitedColumnName = attributes.getProperty("delimitedColumnName"); //$NON-NLS-1$
338         String isGeneratedAlways = attributes.getProperty("isGeneratedAlways"); //$NON-NLS-1$
339         Properties properties = parseProperties(node.getChildNodes());
340 
341         return new ColumnOverride.Builder()
342                 .withColumnName(column)
343                 .withJavaProperty(javaProperty)
344                 .withJavaType(javaType)
345                 .withJdbcType(jdbcType)
346                 .withTypeHandler(typeHandler)
347                 .withColumnNameDelimited(parseNullableBoolean(delimitedColumnName))
348                 .withGeneratedAlways(isTrue(isGeneratedAlways))
349                 .withProperties(properties)
350                 .build();
351     }
352 
353     private GeneratedKey parseGeneratedKey(Node node) {
354         NullableProperties attributes = parseAttributes(node);
355         String column = attributes.getProperty("column"); //$NON-NLS-1$
356         boolean identity = isTrue(attributes.getProperty("identity")); //$NON-NLS-1$
357         String sqlStatement = attributes.getProperty("sqlStatement"); //$NON-NLS-1$
358         return new GeneratedKey(column, sqlStatement, identity);
359     }
360 
361     private IgnoredColumn parseIgnoreColumn(Node node) {
362         NullableProperties attributes = parseAttributes(node);
363         String column = attributes.getProperty("column"); //$NON-NLS-1$
364         String delimitedColumnName = attributes.getProperty("delimitedColumnName"); //$NON-NLS-1$
365         return new IgnoredColumn(column, isTrue(delimitedColumnName));
366     }
367 
368     private IgnoredColumnPattern parseIgnoreColumnByRegex(Node node) {
369         NullableProperties attributes = parseAttributes(node);
370         String pattern = attributes.getProperty("pattern"); //$NON-NLS-1$
371 
372         IgnoredColumnPattern.Builder builder = new IgnoredColumnPattern.Builder().withPattern(pattern);
373 
374         NodeList nodeList = node.getChildNodes();
375         for (int i = 0; i < nodeList.getLength(); i++) {
376             Node childNode = nodeList.item(i);
377 
378             if (childNode.getNodeType() != Node.ELEMENT_NODE) {
379                 continue;
380             }
381 
382             if ("except".equals(childNode.getNodeName())) {
383                 builder.addException(parseException(childNode));
384             }
385         }
386 
387         return builder.build();
388     }
389 
390     private IgnoredColumnException parseException(Node node) {
391         NullableProperties attributes = parseAttributes(node);
392         String column = attributes.getProperty("column"); //$NON-NLS-1$
393         String delimitedColumnName = attributes.getProperty("delimitedColumnName"); //$NON-NLS-1$
394         return new IgnoredColumnException(column, isTrue(delimitedColumnName));
395     }
396 
397     private DomainObjectRenamingRule parseDomainObjectRenamingRule(Node node) {
398         NullableProperties attributes = parseAttributes(node);
399         String searchString = attributes.getProperty("searchString"); //$NON-NLS-1$
400         String replaceString = attributes.getProperty("replaceString"); //$NON-NLS-1$
401         return new DomainObjectRenamingRule(searchString, replaceString);
402     }
403 
404     private ColumnRenamingRule parseColumnRenamingRule(Node node) {
405         NullableProperties attributes = parseAttributes(node);
406         String searchString = attributes.getProperty("searchString"); //$NON-NLS-1$
407         String replaceString = attributes.getProperty("replaceString"); //$NON-NLS-1$
408         return new ColumnRenamingRule(searchString, replaceString);
409     }
410 
411     protected JavaTypeResolverConfiguration parseJavaTypeResolver(Node node) {
412         NullableProperties attributes = parseAttributes(node);
413         String type = attributes.getProperty("type"); //$NON-NLS-1$
414         Properties properties = parseProperties(node.getChildNodes());
415         return new JavaTypeResolverConfiguration.Builder()
416                 .withConfigurationType(type)
417                 .withProperties(properties)
418                 .build();
419     }
420 
421     private PluginConfiguration parsePlugin(Node node) {
422         NullableProperties attributes = parseAttributes(node);
423         String type = attributes.getProperty("type"); //$NON-NLS-1$
424         Properties properties = parseProperties(node.getChildNodes());
425         return new PluginConfiguration.Builder()
426                 .withConfigurationType(type)
427                 .withProperties(properties)
428                 .build();
429     }
430 
431     protected ModelGeneratorConfiguration parseModelGenerator(Node node) {
432         NullableProperties attributes = parseAttributes(node);
433         String targetPackage = attributes.getProperty("targetPackage"); //$NON-NLS-1$
434         String targetProject = attributes.getProperty("targetProject"); //$NON-NLS-1$
435         Properties properties = parseProperties(node.getChildNodes());
436         return new ModelGeneratorConfiguration.Builder()
437                 .withTargetPackage(targetPackage)
438                 .withTargetProject(targetProject)
439                 .withProperties(properties)
440                 .build();
441     }
442 
443     private ClientGeneratorConfiguration parseClientGenerator(Node node, String contextId) {
444         NullableProperties attributes = parseAttributes(node);
445         String type = attributes.getProperty("type"); //$NON-NLS-1$
446         String targetPackage = attributes.getProperty("targetPackage"); //$NON-NLS-1$
447         String targetProject = attributes.getProperty("targetProject"); //$NON-NLS-1$
448         Properties properties = parseProperties(node.getChildNodes());
449         ClientGeneratorConfiguration.LegacyClientType legacyClientType = null;
450         if (type != null) {
451             legacyClientType = ClientGeneratorConfiguration.LegacyClientType.getByAlias(type);
452             if (legacyClientType == null) {
453                 warnings.add(getString("ValidationError.31", type, contextId)); //$NON-NLS-1$
454             }
455         }
456 
457         return new ClientGeneratorConfiguration.Builder()
458                 .withLegacyClientType(legacyClientType)
459                 .withTargetPackage(targetPackage)
460                 .withTargetProject(targetProject)
461                 .withProperties(properties)
462                 .build();
463     }
464 
465     protected JDBCConnectionConfiguration parseJdbcConnection(Node node) {
466         NullableProperties attributes = parseAttributes(node);
467         String driverClass = attributes.getProperty("driverClass"); //$NON-NLS-1$
468         String connectionURL = attributes.getProperty("connectionURL"); //$NON-NLS-1$
469         String userId = attributes.getProperty("userId"); //$NON-NLS-1$
470         String password = attributes.getProperty("password"); //$NON-NLS-1$
471         Properties properties = parseProperties(node.getChildNodes());
472         return new JDBCConnectionConfiguration.Builder()
473                 .withDriverClass(driverClass)
474                 .withConnectionURL(connectionURL)
475                 .withUserId(userId)
476                 .withPassword(password)
477                 .withProperties(properties)
478                 .build();
479     }
480 
481     protected @Nullable String parseClassPathEntry(Node node) {
482         NullableProperties attributes = parseAttributes(node);
483         return attributes.getProperty("location"); //$NON-NLS-1$
484     }
485 
486     protected Properties parseProperties(NodeList nodeList) {
487         Properties properties = new Properties();
488         for (int i = 0; i < nodeList.getLength(); i++) {
489             Node childNode = nodeList.item(i);
490 
491             if (childNode.getNodeType() != Node.ELEMENT_NODE) {
492                 continue;
493             }
494 
495             if ("property".equals(childNode.getNodeName())) { //$NON-NLS-1$
496                 parseProperty(childNode).ifPresent(p -> properties.setProperty(p.name(), p.value()));
497             }
498         }
499         return properties;
500     }
501 
502     protected Optional<Property> parseProperty(Node node) {
503         NullableProperties attributes = parseAttributes(node);
504         String name = attributes.getProperty("name"); //$NON-NLS-1$
505         String value = attributes.getProperty("value"); //$NON-NLS-1$
506 
507         if (name == null || value == null) {
508             return Optional.empty();
509         } else {
510             return Optional.of(new Property(name, value));
511         }
512     }
513 
514     /**
515      * Parses node attributes.
516      *
517      * <p>Any attribute with an empty value (defined as missing, or blank string) will be dropped.
518      *
519      * @param node the node
520      * @return properties containing all non-empty attributes
521      */
522     protected NullableProperties parseAttributes(Node node) {
523         NullableProperties attributes = new NullableProperties();
524         NamedNodeMap nnm = node.getAttributes();
525         for (int i = 0; i < nnm.getLength(); i++) {
526             Node attribute = nnm.item(i);
527             String value = parsePropertyTokens(attribute.getNodeValue());
528             attributes.put(attribute.getNodeName(), trimToNull(value));
529         }
530 
531         return attributes;
532     }
533 
534     String parsePropertyTokens(String s) {
535         final String OPEN = "${"; //$NON-NLS-1$
536         final String CLOSE = "}"; //$NON-NLS-1$
537         int currentIndex = 0;
538 
539         List<String> answer = new ArrayList<>();
540 
541         int markerStartIndex = s.indexOf(OPEN);
542         if (markerStartIndex < 0) {
543             // no parameter markers
544             answer.add(s);
545             currentIndex = s.length();
546         }
547 
548         while (markerStartIndex > -1) {
549             if (markerStartIndex > currentIndex) {
550                 // add the characters before the next parameter marker
551                 answer.add(s.substring(currentIndex, markerStartIndex));
552                 currentIndex = markerStartIndex;
553             }
554 
555             int markerEndIndex = s.indexOf(CLOSE, currentIndex);
556             int nestedStartIndex = s.indexOf(OPEN, markerStartIndex + OPEN.length());
557             while (nestedStartIndex > -1 && markerEndIndex > -1 && nestedStartIndex < markerEndIndex) {
558                 nestedStartIndex = s.indexOf(OPEN, nestedStartIndex + OPEN.length());
559                 markerEndIndex = s.indexOf(CLOSE, markerEndIndex + CLOSE.length());
560             }
561 
562             if (markerEndIndex < 0) {
563                 // no closing delimiter, just move to the end of the string
564                 answer.add(s.substring(markerStartIndex));
565                 currentIndex = s.length();
566                 break;
567             }
568 
569             // we have a valid property marker...
570             String property = s.substring(markerStartIndex + OPEN.length(), markerEndIndex);
571             String propertyValue = resolveProperty(parsePropertyTokens(property));
572             if (propertyValue == null) {
573                 // add the property marker back into the stream
574                 answer.add(s.substring(markerStartIndex, markerEndIndex + 1));
575             } else {
576                 answer.add(propertyValue);
577             }
578 
579             currentIndex = markerEndIndex + CLOSE.length();
580             markerStartIndex = s.indexOf(OPEN, currentIndex);
581         }
582 
583         if (currentIndex < s.length()) {
584             answer.add(s.substring(currentIndex));
585         }
586 
587         return String.join("", answer);
588     }
589 
590     protected CommentGeneratorConfiguration parseCommentGenerator(Node node) {
591         NullableProperties attributes = parseAttributes(node);
592         String type = attributes.getProperty("type"); //$NON-NLS-1$
593         Properties properties = parseProperties(node.getChildNodes());
594         return new CommentGeneratorConfiguration.Builder()
595                 .withConfigurationType(type)
596                 .withProperties(properties)
597                 .build();
598     }
599 
600     protected ConnectionFactoryConfiguration parseConnectionFactory(Node node) {
601         NullableProperties attributes = parseAttributes(node);
602         String type = attributes.getProperty("type"); //$NON-NLS-1$
603         Properties properties = parseProperties(node.getChildNodes());
604         return new ConnectionFactoryConfiguration.Builder()
605                 .withConfigurationType(type)
606                 .withProperties(properties)
607                 .build();
608     }
609 
610     /**
611      * This method resolves a property from one of the three sources: system properties,
612      * properties loaded from the &lt;properties&gt; configuration element, and
613      * "extra" properties that may be supplied by the Maven or Ant environments.
614      *
615      * <p>If there is a name collision, system properties take precedence, followed by
616      * configuration properties, followed by extra properties.
617      *
618      * @param key property key
619      * @return the resolved property.  This method will return null if the property is
620      *     undefined in any of the sources.
621      */
622     private @Nullable String resolveProperty(String key) {
623         String property = System.getProperty(key);
624 
625         if (property == null) {
626             property = configurationProperties.getProperty(key);
627         }
628 
629         if (property == null) {
630             property = extraProperties.getProperty(key);
631         }
632 
633         return property;
634     }
635 }