diff --git a/README.md b/README.md
index da8dd3c0ae9..88dc3252276 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,26 @@ Make sure to set the `$JAVA_HOME` environment variable.
* If on Windows, a Powershell version >= 7.1.3 installed that you can download from the below link.
[Powershell](https://github.com/PowerShell/PowerShell)
+## Configuration system
+A new configuration system has been implemented.
+If you want to set up another database than H2 (Highly recommended for anything else than sandbox usage),
+you can do it easily by creating a .conf file.
+You can find examples in `framework/entity/config/samples`.
+Define a delegator and a datasource, and you're all set.
+
+Please note the following rules:
+- The config files follow the [Hocon Syntax](https://learnxinyminutes.com/hocon/), a Json superset, and uses TypeSafe
+ implementation by default. Hocon is able to use environment variables.
+- You can disable this behavior by setting 'ofbiz.config.overload.enable' in `start.properties` to false.
+ - This will allow you to use the vanilla Xml-only config system.
+- **All files** with .conf extension and located in the `config` folder of a plugin or framework component will be read.
+- You can pass files names as launching parameters with `-c` argument.
+ - Example for local developement : `./gradlew ofbiz --args="-c file=/tmp/myfile.conf"`
+ - Example for binary args (prod) : `ofbiz -c file=myfile.conf`'
+ - Warning : Files loaded in this way will be prioritized over files that are not specified.
+ - This means that if `myfile.conf` (passed as startup parameter) and `myOtherFile.conf` (in a plugin) both define a `foo` parameters,
+ it is the value in `myfile.conf` that will be used.
+- For now, you can use this system to override entitygine.xml configs, and serviceengine.xml configs.
## Quick start
diff --git a/build.gradle b/build.gradle
index 10b13996637..1276dd0fd9c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -257,6 +257,8 @@ dependencies {
compileOnly project(path: subProject.path, configuration: 'pluginLibsCompileOnly')
}
+ implementation 'com.typesafe:config:1.4.3'
+
junitReport 'junit:junit:4.13.2'
junitReport 'org.apache.ant:ant-junit:1.10.15'
diff --git a/dependencies.gradle b/dependencies.gradle
index b890c4dac12..3e458ae90ee 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -30,6 +30,7 @@ dependencies {
implementation 'com.sun.mail:javax.mail:1.6.2'
implementation 'com.rometools:rome:2.1.0'
implementation 'com.thoughtworks.xstream:xstream:1.4.21'
+ implementation 'com.typesafe:config:1.4.3'
implementation 'commons-cli:commons-cli:1.11.0'
implementation 'commons-net:commons-net:3.13.0'
implementation 'commons-validator:commons-validator:1.10.1'
@@ -68,7 +69,8 @@ dependencies {
implementation 'org.apache.xmlgraphics:batik-bridge:1.19'
implementation 'org.apache.xmlgraphics:fop:2.11'
implementation 'org.clojure:clojure:1.12.4'
- implementation 'org.apache.groovy:groovy-all:5.0.5'
+// implementation 'org.apache.groovy:groovy-all:5.0.5'
+ implementation 'org.apache.groovy:groovy-all:6.0.0-alpha-1'
implementation 'org.freemarker:freemarker:2.3.34' // Remember to change the version number in FreeMarkerWorker class when upgrading. See OFBIZ-10019 if >= 2.4
implementation 'org.owasp.esapi:esapi:2.7.0.0'
implementation 'org.springframework:spring-test:6.2.18'
diff --git a/framework/base/ofbiz-component.xml b/framework/base/ofbiz-component.xml
index 5fceb1cad08..5592732b61c 100644
--- a/framework/base/ofbiz-component.xml
+++ b/framework/base/ofbiz-component.xml
@@ -26,6 +26,8 @@ under the License.
+
+
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/AbstractConfigElement.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/AbstractConfigElement.java
new file mode 100644
index 00000000000..0138f2d2cb5
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/AbstractConfigElement.java
@@ -0,0 +1,106 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import org.apache.ofbiz.entity.GenericEntityConfException;
+import org.w3c.dom.Element;
+
+import java.util.Map;
+
+
+/**
+ * Abstract class that must be implemented by every class wishing to use the centralized configuration reader tool.
+ * Each class implementing this interface must :
+ *
+ *
Possess a {@code public static final String ELEMENT_NAME} containing the name of the XML element to which
+ * the class refers to. Often the tag name. For example for the ConnectionFactory class, it will be "connection-factory"
+ *
If needed a {@code public static final String ELEMENT_FIELD_ID_NAME} containing the attribut name of the XML element
+ * that identify as unique the element. Else the {@code name} string will be used
+ *
Have a first constructor with an {@code Element} and a {@code String} as parameters. It will be used to
+ * construct the object from composite source.
+ *
Have a second constructor with a {@code Map} and a {@code String} as parameters. It will be used to
+ * construct the object from overloaded configs.
+ *
Implement {@link AbstractConfigElement#loadFromXml(Element, String)} by calling the constructor with {@link Element}
+ *
Implement {@link AbstractConfigElement#loadFromConfig(Map, String)} by calling the constructor with {@link Map}
+ *
+ */
+public abstract class AbstractConfigElement {
+
+ /**
+ * Must invoke the constructor reading from an XML file
+ *
+ * @param element the {@link Element} represented by the class
+ * @param xPathParent the {@link javax.xml.xpath.XPath} of the parent element class invoking this method
+ * @return an instance of the class from which it was called from
+ * @throws GenericEntityConfException
+ */
+ static AbstractConfigElement loadFromXml(Element element, String xPathParent) throws GenericEntityConfException {
+ return null;
+ }
+
+ /**
+ * Invoke the constructor reading from a config overload file
+ *
+ * @param configMap a Map representing the element to create, likely coming from the Config object.
+ * @param xPath the {@link javax.xml.xpath.XPath} to this element in the configuration file
+ * @return an instance of the class from which it was called from
+ * @throws GenericEntityConfException
+ */
+ static AbstractConfigElement loadFromConfig(Map configMap, String xPath) throws GenericEntityConfException {
+ return null;
+ }
+
+ /**
+ * Return the name of the element, if the class doesn't possess a name, a unique attribute not shared among other element will be returned
+ *
+ * @return a unique String to identify the element
+ */
+ public abstract String getName();
+
+ /**
+ * Retrieve the name of the last element in the xPath, this name can be :
+ *
+ *
Either the last chain of characters at the end of the xPath (for example "foo/bar" will find bar)
+ *
Or the unique identifier searched in the xPath (for example "foo/bar[@name='baz']" will return baz
+ *
+ *
+ * @param xPath the path from which to find the name of the last element
+ * @return the name of the last element in the xPath
+ */
+ public static String getNameFromXPath(String xPath) {
+ String lastMember = xPath.substring(xPath.lastIndexOf("/") + 1);
+ int firstSimpleQuote = lastMember.indexOf('\'');
+ int lastSimpleQuote = lastMember.lastIndexOf('\'');
+ if (firstSimpleQuote != -1 && lastSimpleQuote != -1) {
+ lastMember = lastMember.substring(firstSimpleQuote + 1, lastSimpleQuote);
+ }
+ return lastMember.intern();
+ }
+
+ /**
+ * Override if the element is a list of elements like run-from-pool list.
+ * This will make the config system ignore Xml implementations if config overloads are presents.
+ *
+ * @return if the config system allows multiple sources and should merge those sources
+ */
+ public boolean allowMultipleSources() {
+ return true;
+ }
+
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/AbstractXmlConfigGetter.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/AbstractXmlConfigGetter.java
new file mode 100644
index 00000000000..84561f63790
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/AbstractXmlConfigGetter.java
@@ -0,0 +1,142 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.w3c.dom.Element;
+
+/**
+ * Abstract class that mustn't be instantiated.
+ * Instead, it must be extended for each resource file, such as entityengine or serviceengine.
+ * The child ConfigGetters define the resource name, and the root xPath
+ */
+public abstract class AbstractXmlConfigGetter {
+ private final String ressource;
+ private final String rootXPath;
+ private final ConfigurationInterface config;
+
+ protected AbstractXmlConfigGetter(String ressource, String rootXPath) {
+ this.ressource = ressource;
+ this.rootXPath = rootXPath;
+ config = ConfigurationFactory.getInstance();
+ }
+
+ /**
+ * @param key the key of the value that is looked up for.
+ * @return the value that is found
+ */
+ public String getValue(String key) {
+ return config.getValue(ressource, key);
+ }
+
+ /**
+ * See {@link AbstractXmlConfigGetter#getValue(Map, String, Object, Class)}
+ */
+ public String getValue(Map configObject, String key) {
+ return getValue(configObject, key, "", String.class);
+ }
+
+ /**
+ * Get a config value in the {@code configObject} if it has the key, else search the config system.
+ *
+ * @param configObject a Map coming from the config system.
+ * @param key the key of the value that is looked up for.
+ * @param defaultValue the value return if not found
+ * @param targetClass the output class of the looked up element.
+ * @return the single value found for {@code key}
+ */
+ public T getValue(Map configObject, String key, T defaultValue, Class targetClass) {
+ if (configObject == null) {
+ return getValue(key, defaultValue, targetClass);
+ }
+ return config.getValue(configObject, key, targetClass, defaultValue);
+ }
+
+ /**
+ * Get a config value in the config system.
+ *
+ * @param key the key of the value that is looked up for.
+ * @param defaultValue the value return if not found
+ * @param targetClass the output class of the looked up element.
+ * @return the single value found for {@code key}
+ */
+ public T getValue(String key, T defaultValue, Class targetClass) {
+ return config.getValue(ressource, key, targetClass, defaultValue);
+ }
+
+ /**
+ * See {@link AbstractXmlConfigGetter#getObjectSubElement(String, Element, Class)}
+ */
+ public T getObjectSubElement(Element parent, Class targetClass) {
+ return getObjectSubElement(rootXPath, parent, targetClass);
+ }
+
+ /**
+ * Get a config value in the config system.
+ *
+ * @param parentXPath the xPath that serves as root for the wanted config element
+ * @param parent the parent element to look into
+ * @param targetClass the output class of the looked up element.
+ * @return the single value found for {@code xPath} in {@code parent}
+ */
+ public T getObjectSubElement(String parentXPath, Element parent, Class targetClass) {
+ return config.getConfigElementAsObjectOfClass(ressource, parentXPath, parent, targetClass);
+ }
+
+ /**
+ * See {@link AbstractXmlConfigGetter#getSubElementsAsListEntries(String, Element, Class)}
+ */
+ public List getSubElementsAsListEntries(Element parent, Class targetClass) {
+ return getSubElementsAsListEntries(rootXPath, parent, targetClass);
+ }
+
+ /**
+ * Get a config List in the config system.
+ *
+ * @param parentXPath the xPath that serves as root for the wanted config element
+ * @param parent the parent element to look into
+ * @param targetClass the output class of the looked up element.
+ * @return a list of {@code targetClass} found in {@code parent}
+ */
+ public List getSubElementsAsListEntries(String parentXPath, Element parent, Class targetClass) {
+ return config.getConfigElementsAsListEntriesOfClass(ressource, parentXPath, parent, targetClass);
+ }
+
+ /**
+ * See {@link AbstractXmlConfigGetter#getSubElementsAsMapValues(String, Element, Class)}
+ */
+ public Map getSubElementsAsMapValues(Element parent, Class targetClass) {
+ return getSubElementsAsMapValues(rootXPath, parent, targetClass);
+ }
+
+ /**
+ * Get a config Map from the config system.
+ *
+ * @param parentXPath the xPath that serves as root for the wanted config element
+ * @param parent the parent element to look into
+ * @param targetClass the output class of the looked up element.
+ * @return a Map of {@code targetClass} found in {@code parent}. The element names serves as key.
+ */
+ public Map getSubElementsAsMapValues(String parentXPath, Element parent, Class targetClass) {
+ return config.getConfigElementsAsMapValuesOfClass(ressource, parentXPath, parent, targetClass);
+ }
+
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigHelper.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigHelper.java
new file mode 100644
index 00000000000..845f7b321fa
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigHelper.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import static org.apache.ofbiz.base.util.UtilProperties.getPropertyAsBoolean;
+
+/**
+ * Utility class, maily for test and Mockito purposes
+ */
+public final class ConfigHelper {
+
+ /**
+ * Small utility method, mainly for testing purposes
+ */
+ public static boolean checkStrictXmlStructure() {
+ return getPropertyAsBoolean("configuration.properties", "ofbiz.config.xml.check.strict", true);
+ }
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationFactory.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationFactory.java
new file mode 100644
index 00000000000..625595846a5
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationFactory.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * A {@link ConfigurationInterface} factory.
+ */
+public class ConfigurationFactory {
+
+ private static ConfigurationInterface configInstance = null;
+
+ /**
+ * Gets {@code configInstance}. Create an empty one if needed.
+ *
+ * @return the instance of {@code configInstance}
+ */
+ public static ConfigurationInterface getInstance() {
+ return getInstance(List.of());
+ }
+
+ /**
+ * Gets {@code configInstance}. Create an new one if needed by loading file at {@code configPath}
+ *
+ * @param configPath the override configuration file
+ * @return the instance of {@code configInstance}
+ */
+ public static ConfigurationInterface getInstance(File configPath) {
+ if (configInstance == null) {
+ synchronized (ConfigurationFactory.class) {
+ configInstance = configPath == null
+ ? new DefaultConfiguration()
+ : new DefaultConfiguration(configPath);
+ }
+ }
+ return configInstance;
+ }
+
+ /**
+ * Gets {@code configInstance}. Create a new one if needed by loading all files in {@code configPaths}
+ *
+ * @param configPaths list of override configuration files
+ * @return the instance of {@code configInstance}
+ */
+ public static ConfigurationInterface getInstance(List configPaths) {
+ if (configInstance == null) {
+ synchronized (ConfigurationFactory.class) {
+ configInstance = new DefaultConfiguration(configPaths);
+ }
+ }
+ return configInstance;
+ }
+
+ /**
+ * @return resets and returns a new empty {@code configInstance}
+ */
+ public static ConfigurationInterface resetAndGet() {
+ return resetAndGet(List.of());
+ }
+
+ /**
+ * @return resets and returns a new empty {@code configInstance}
+ */
+ public static ConfigurationInterface resetAndGet(List configPaths) {
+ configInstance = null;
+ return getInstance(configPaths);
+ }
+
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationInterface.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationInterface.java
new file mode 100644
index 00000000000..e1c2cf6c7dd
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationInterface.java
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Main interface for configuration overload system.
+ */
+public interface ConfigurationInterface {
+
+ /**
+ * See {@link ConfigurationInterface#getValue(String, String, Class, Object)}
+ */
+ String getValue(String resourceName, String key);
+
+ /**
+ * See {@link ConfigurationInterface#getValue(String, String, Class, Object)}
+ */
+ String getValue(String resourceName, String key, String defaultValue);
+
+ /**
+ * See {@link ConfigurationInterface#getValue(String, String, Class, Object)}
+ */
+ T getValue(String resourceName, String key, Class targetClass);
+
+ /**
+ * Retrieve the value contained in {@code resourceName} using {@code key}
+ *
+ * @param resourceName the name of the resource to look in for the element (eg: 'entityengine')
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @param targetClass the output class of the looked up element.
+ * @param defaultValue the value to return if no value was found for the given key.
+ * @return the value contained in the override file if any is found.
+ * Else, returns the value at {@code key} in {@code resourceName}
+ * Else return {@code defaultValue}
+ */
+ T getValue(String resourceName, String key, Class targetClass, T defaultValue);
+
+ /**
+ * Retrieve the value contained in resourceName using key
+ *
+ * @param configObject an object representation of an XML element. Likely comes from an overload config file.
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @param targetClass the output class of the looked up element.
+ * @param defaultValue the value to return if no value was found for the given key.
+ * @return the value contained in the override file if any is found.
+ * Else, returns the value at {@code key} in {@code resourceName}
+ * Else return {@code defaultValue}
+ */
+ T getValue(Map configObject, String key, Class targetClass, T defaultValue);
+
+ /**
+ * Get the attribute {@code useOverride}
+ *
+ * @return the attribute indicating if the overload config system should be enabled
+ */
+ boolean getUseOverrideValue();
+
+ /**
+ * Set the attribute {@code useOverride}
+ *
+ * @param useOverride set the parameter indicating if the overload config system should be enabled
+ */
+ void setUseOverrideValue(boolean useOverride);
+
+ /**
+ * Clears the configuration cache
+ */
+ void clearCache();
+
+ /**
+ * Constructs and / or gets an inheritor of {@link AbstractConfigElement}. All the configurations will be read and used.
+ *
+ * @param resourceName the name of the resource to look in for the element (eg: 'entityengine')
+ * @param xPath the xPath of the wanted value
+ * @param parent the {@code Element} from which to retrieve the child, must be {@code null} when creating a new element using a conf file
+ * @param targetClass the class of the child object that will be created from {@code parent}
+ * @return an instance of {@code targetClass} or {@code null} if no element was found
+ */
+ T getConfigElementAsObjectOfClass(String resourceName, String xPath, Object parent, Class targetClass);
+
+ /**
+ * Retrieve one or more XML elements and create a list of objects out of them using a class specified in parameter
+ *
+ * @param resourceName the name of the resource to look in for the element (eg: 'entityengine')
+ * @param xPath the xPath of the wanted value
+ * @param parent the {@code Element} from which to retrieve the child, must be {@code null} when creating a new element using a conf file
+ * @param targetClass the class of the child object that will be created from {@code parent}
+ * @return a list of objects of targetClass or an empty list no elements were found
+ * @return a List of {@code targetClass} or an empty List if no element was found
+ */
+ List getConfigElementsAsListEntriesOfClass(String resourceName, String xPath, Object parent, Class targetClass);
+
+ /**
+ * Retrieve one or more XML elements and create a map of objects paired with their name (or unique attribute) out of
+ * them using a class specified in parameter
+ *
+ * @param resourceName the name of the resource to look in for the element (eg: 'entityengine')
+ * @param xPath the xPath of the wanted value
+ * @param parent the {@code Element} from which to retrieve the child, must be {@code null} when creating a new element using a conf file
+ * @param targetClass the class of the child object that will be created from {@code parent}
+ * @return a Map of objects of {@code targetClass}, with their names as keys, or an empty Map if no element was found
+ */
+ Map getConfigElementsAsMapValuesOfClass(String resourceName, String xPath, Object parent, Class targetClass);
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationReaderInterface.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationReaderInterface.java
new file mode 100644
index 00000000000..ec55b020565
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/ConfigurationReaderInterface.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+
+import java.util.List;
+
+/**
+ * Interface for classes that read configuration.
+ * Each implementation handles a specific configuration type.
+ */
+public interface ConfigurationReaderInterface {
+
+ /**
+ * Inits a reader from a resource file.
+ *
+ * @param resourceName the XML, Java properties, JSON (or HOCON) file to load
+ */
+ void init(String resourceName);
+
+ /**
+ * Retrieve the value from reader at {@code key}
+ *
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @return a {@code String} representation of the value contained in the key or {@code null} if it was not found
+ */
+ String getValue(String key);
+
+ /**
+ * Retrieves an inheritor of {@link AbstractConfigElement} representing a configuration element of the wanted class.
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @param parent the parent that will be searched for the wanted elements. Can be an {@link org.w3c.dom.Element} in case of xml representation
+ * or a {@link java.util.Map} in case of configuration overload.
+ * @param targetClass the class of the element that is lookup up.
+ * @return an instance of {@code targetClass} or {@code null} if the element wasn't found
+ */
+ T findSingleElementWithClassInParent(String key, Object parent, Class targetClass);
+
+ /**
+ * Retrieves an inheritor List of {@link AbstractConfigElement} representing a list of configuration elements of the wanted class.
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @param parent the parent that will be searched for the wanted elements. Can be an {@link org.w3c.dom.Element} in case of xml representation
+ * or a {@link java.util.Map} in case of configuration overload.
+ * @param targetClass the class of the element that is lookup up.
+ * @return a List of {@code targetClass} or an empty List if the element wasn't found
+ */
+ List findElementsWithTypeInParent(String key, Object parent, Class targetClass);
+
+ /**
+ * Clears the cache of the reader.
+ */
+ void clearCache();
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/DefaultConfiguration.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/DefaultConfiguration.java
new file mode 100644
index 00000000000..dd11d102dbc
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/DefaultConfiguration.java
@@ -0,0 +1,198 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.ofbiz.base.conversion.ConversionException;
+import org.apache.ofbiz.base.conversion.Converters;
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.base.util.cache.UtilCache;
+
+import javax.xml.xpath.XPathExpressionException;
+
+public class DefaultConfiguration implements ConfigurationInterface {
+ public static final String MODULE = DefaultConfiguration.class.getName();
+ private static final UtilCache CONFIGURATION_TYPE = UtilCache.createUtilCache("base.config.type", 0, 0);
+ private final TypesafeConfigImplReader typesafeConfReader;
+ private boolean useOverrideValue = true;
+
+ public DefaultConfiguration() {
+ typesafeConfReader = new TypesafeConfigImplReader();
+ }
+
+ public DefaultConfiguration(File configPath) {
+ typesafeConfReader = new TypesafeConfigImplReader(configPath);
+ }
+
+ public DefaultConfiguration(List configPaths) {
+ typesafeConfReader = new TypesafeConfigImplReader(configPaths);
+ }
+
+ @Override
+ public String getValue(String resourceName, String key) {
+ return getValue(resourceName, key, String.class, "");
+ }
+
+ @Override
+ public String getValue(String resourceName, String key, String defaultValue) {
+ return getValue(resourceName, key, String.class, defaultValue);
+ }
+
+ @Override
+ public T getValue(String resourceName, String key, Class targetClass) {
+ return getValue(resourceName, key, targetClass, null);
+ }
+
+ @Override
+ public T getValue(String resourceName, String key, Class targetClass, T defaultValue) {
+ String value = null;
+ if (getUseOverrideValue()) {
+ value = getValueFromOverride(resourceName, key);
+ }
+ if (value == null) {
+ value = getRelevantReader(resourceName).getValue(key);
+ }
+ if (targetClass == String.class) {
+ return value == null
+ ? defaultValue
+ : UtilGenerics.cast(value);
+ }
+ try {
+ return value == null
+ ? defaultValue
+ : Converters.getConverter(String.class, targetClass).convert(value);
+ } catch (ClassNotFoundException | ConversionException e) {
+ Debug.logError(String.format("Try to convert a String %s to %s with error %s", value, targetClass, e), MODULE);
+ }
+ return null;
+ }
+
+ @Override
+ public T getValue(Map configObject, String key, Class targetClass, T defaultValue) {
+ Object value = null;
+ try {
+ value = typesafeConfReader.getValue(configObject, TypesafeConfigImplReader.convertToConfigPath(key));
+ } catch (XPathExpressionException e) {
+ Debug.logError("Error getting value from typesafe configs", MODULE);
+ }
+ if (targetClass == String.class) {
+ return value == null ? defaultValue : UtilGenerics.cast(value);
+ }
+ try {
+ return value == null
+ ? defaultValue
+ : Converters.getConverter(String.class, targetClass).convert(value.toString());
+ } catch (ClassNotFoundException | ConversionException e) {
+ Debug.logError(String.format("Try to convert a String %s to %s with error %s", value, targetClass, e), MODULE);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the value contained in {@code resourceName} at {@code key} in the override reader.
+ *
+ * @param resourceName the name of the resource to look in for the element (eg: 'entityengine')
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @return the value contained by the key or {@code null} if it was not found
+ */
+ private String getValueFromOverride(String resourceName, String key) {
+ return typesafeConfReader.getValue(resourceName, key);
+ }
+
+ /**
+ * Find the correct type of config reader implementation to use given a resource file
+ *
+ * @param resourceName the XML or Java properties file to read
+ * @return the appropriate implementation of {@code ConfigurationReaderInterface} depending on the extension of {@code resourceName}
+ */
+ private ConfigurationReaderInterface getRelevantReader(String resourceName) {
+ ConfigurationReaderInterface readerToUse = CONFIGURATION_TYPE.get(resourceName);
+ if (readerToUse != null) {
+ return readerToUse;
+ }
+ String ext = FilenameUtils.getExtension(resourceName);
+ readerToUse = switch (ext) {
+ case "xml" -> new XmlConfigurationReader(resourceName);
+ default -> new PropertiesConfigurationReader(resourceName);
+ };
+ CONFIGURATION_TYPE.put(resourceName, readerToUse);
+ return readerToUse;
+ }
+
+ /**
+ * return true if we need to return the override value or origin value
+ *
+ * @return true is we override values
+ */
+ public boolean getUseOverrideValue() {
+ return useOverrideValue; //use for test
+ }
+
+ /**
+ * Set true if we want override values
+ *
+ * @param useOverrideValue parameter indicating if the class should override its configuration (true by default)
+ */
+ public void setUseOverrideValue(boolean useOverrideValue) {
+ this.useOverrideValue = useOverrideValue; //use for test
+ }
+
+ /**
+ * Empty all config cache
+ */
+ public void clearCache() {
+ CONFIGURATION_TYPE.clear();
+ typesafeConfReader.clearCache();
+ }
+
+ @Override
+ public T getConfigElementAsObjectOfClass(String resourceName, String xPath, Object parent, Class targetClass) {
+ return getRelevantReader(resourceName).findSingleElementWithClassInParent(xPath, parent, targetClass);
+ }
+
+ @Override
+ public List getConfigElementsAsListEntriesOfClass(String resourceName, String xPath, Object parent, Class targetClass) {
+ return typesafeConfReader.collectElementsAsListEntriesWithConfigsApplied(resourceName, xPath, targetClass,
+ getRelevantReader(resourceName).findElementsWithTypeInParent(xPath, parent, targetClass));
+ }
+
+ @Override
+ public Map getConfigElementsAsMapValuesOfClass(String resourceName, String xPath, Object parent, Class targetClass) {
+ List childList = getConfigElementsAsListEntriesOfClass(resourceName, xPath, parent, targetClass);
+ Map uniqueMap = new LinkedHashMap<>();
+ try {
+ Method method = targetClass.getMethod("getName");
+ for (T value : childList) {
+ uniqueMap.put((String) method.invoke(value), value);
+ }
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ uniqueMap.putAll(typesafeConfReader.collectElementsAsMapValuesWithConfigsApplied(resourceName, xPath, targetClass, uniqueMap));
+ return uniqueMap;
+ }
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/PropertiesConfigurationReader.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/PropertiesConfigurationReader.java
new file mode 100644
index 00000000000..c39693c5120
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/PropertiesConfigurationReader.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import java.util.List;
+import java.util.Properties;
+import org.apache.ofbiz.base.util.UtilProperties;
+
+public class PropertiesConfigurationReader implements ConfigurationReaderInterface {
+ private Properties props;
+
+ PropertiesConfigurationReader(String resourceName) {
+ init(resourceName);
+ }
+
+ @Override
+ public void init(String resourceName) {
+ Properties vanilla = UtilProperties.getProperties(resourceName);
+ props = vanilla != null ? vanilla : new Properties();
+ }
+
+ @Override
+ public String getValue(String key) {
+ String value = props.getProperty(key);
+ return value == null ? "" : value.intern().trim();
+ }
+
+ @Override
+ public T findSingleElementWithClassInParent(String key, Object parent, Class targetClass) {
+ return null;
+ }
+
+ @Override
+ public List findElementsWithTypeInParent(String key, Object parent, Class targetClass) {
+ return List.of();
+ }
+
+ @Override
+ public void clearCache() {
+ }
+}
diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/config/TypesafeConfigImplReader.java b/framework/base/src/main/java/org/apache/ofbiz/base/config/TypesafeConfigImplReader.java
new file mode 100644
index 00000000000..97c8a311121
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/config/TypesafeConfigImplReader.java
@@ -0,0 +1,387 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.base.config;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.MatchResult;
+import java.util.regex.Pattern;
+
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.base.util.UtilMisc;
+import org.apache.ofbiz.base.util.UtilValidate;
+import org.apache.ofbiz.base.util.cache.UtilCache;
+
+import javax.xml.xpath.XPathExpressionException;
+
+public class TypesafeConfigImplReader implements ConfigurationReaderInterface {
+ private static final String MODULE = TypesafeConfigImplReader.class.getName();
+ private Config globalConfig = ConfigFactory.empty();
+ private static final UtilCache CONFIGURATION_CACHE = UtilCache.createUtilCache("base.config", 0, 0);
+ private static final int MAX_PATH_LENGTH = 500;
+
+ public TypesafeConfigImplReader() {
+ this((String) null);
+ }
+
+ public TypesafeConfigImplReader(String confOverload) {
+ init(confOverload);
+ }
+
+ public TypesafeConfigImplReader(File confOverload) {
+ init(List.of(confOverload));
+ }
+
+ public TypesafeConfigImplReader(List confOverloads) {
+ init(confOverloads);
+ }
+
+ @Override
+ public void init(String resourceName) {
+ globalConfig = UtilValidate.isNotEmpty(resourceName)
+ ? ConfigFactory.load(resourceName)
+ : ConfigFactory.load();
+ }
+
+ /**
+ * Specific initializer for Typesafe reader implementation, since its must be able to load multiple files.
+ *
+ * @param confFiles the list of files that will be loaded.
+ */
+ public void init(List confFiles) {
+ Debug.logInfo("Collect available configuration", MODULE);
+ List configsToLoad = UtilMisc.toList(
+ ConfigFactory.systemEnvironment(),
+ ConfigFactory.systemProperties());
+ if (UtilValidate.isNotEmpty(confFiles)) {
+ confFiles.sort(Comparator.comparing(File::getName));
+ for (File confFile : confFiles) {
+ configsToLoad.add(ConfigFactory.parseFile(confFile));
+ }
+ }
+ configsToLoad.stream()
+ .filter(Objects::nonNull)
+ .toList()
+ .forEach(config -> {
+ globalConfig = globalConfig.withFallback(config);
+ Debug.logInfo("Loaded config element with origin: " + config.origin(), MODULE);
+ });
+ globalConfig = globalConfig.resolve();
+ Debug.logInfo("Configurations loaded and merged", MODULE);
+ }
+
+ @Override
+ public String getValue(String key) {
+ return getValue("", key);
+ }
+
+ @Override
+ public List findElementsWithTypeInParent(String key, Object parent, Class targetClass) {
+ return List.of();
+ }
+
+ @Override
+ public T findSingleElementWithClassInParent(String xPath, Object parent, Class targetClass) {
+ return null;
+ }
+
+ /**
+ * Retrieve a value from the loaded typesafe configs.
+ *
+ * @param resourceName the name of the resource to look in for the element (eg: 'entityengine').
+ * @param key the key associated with the value that is looked for. Can be in standard java property format or xPath.
+ * @return a {@code String} representation of the value at key or {@code null} if it was not found
+ */
+ public String getValue(String resourceName, String key) {
+ String configMapKey = String.format("%s#%s", resourceName, key);
+ if (CONFIGURATION_CACHE.containsKey(configMapKey)) {
+ return CONFIGURATION_CACHE.get(configMapKey);
+ }
+
+ String configPath = convertToConfigPath(resourceName, key);
+ String value = null;
+ if (configPath == null) {
+ return "";
+ } else {
+ if (!globalConfig.hasPath(configPath) && !key.startsWith("/")) { // property case, escaping xml xpaths
+ configPath = convertToConfigPath(resourceName, '"' + key + '"');
+ }
+ if (globalConfig.hasPath(configPath)) {
+ value = globalConfig.getString(configPath).intern();
+ }
+ }
+ CONFIGURATION_CACHE.put(configMapKey, value);
+ return value;
+ }
+
+ /**
+ * Return a value in {@code configObject} using {@code key}. Here, {@code configObject} is a Map, likely comming from
+ * a typesafe config object.
+ *
+ * @param configObject the representation of a config loaded in the system.
+ * @param key the key that is wanted in the wanted map
+ * @return a value that can be either {@code String}, {@code Number}, {@code Boolean}, {@code Map},
+ * {@code List