001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.q2;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029
030import org.jdom2.Element;
031import org.jpos.core.Configuration;
032import org.jpos.core.ConfigurationException;
033import org.jpos.core.Environment;
034import org.jpos.core.SimpleConfiguration;
035import org.jpos.iso.ISOUtil;
036import org.yaml.snakeyaml.Yaml;
037
038/**
039 * Default {@link ConfigurationFactory} that builds a {@link SimpleConfiguration}
040 * from {@code <property>} children, with optional inclusion of YAML or
041 * {@code .properties} files (and per-environment overlays).
042 */
043public class SimpleConfigurationFactory implements ConfigurationFactory {
044    /** Default constructor; no instance state to initialise. */
045    public SimpleConfigurationFactory() {}
046    @Override
047    public Configuration getConfiguration(Element e) throws ConfigurationException {
048        Properties props = new Properties();
049        for (Element property : e.getChildren("property")) {
050            String name = property.getAttributeValue("name");
051            String value = property.getAttributeValue("value");
052            String baseFile = property.getAttributeValue("file");
053            if (baseFile != null) {
054                boolean isEnv = Boolean.parseBoolean(property.getAttributeValue("env", "false"));
055                processFile(props, baseFile, isEnv);
056            } else if (name != null && value != null) {
057                processProperty(props, name, value);
058            }
059        }
060        return new SimpleConfiguration(props);
061    }
062
063    /**
064     * Adds {@code value} to {@code props} under {@code name}, promoting the entry to a
065     * {@code String[]} when more than one value has been registered for the same key.
066     *
067     * @param props target properties
068     * @param name property name
069     * @param value property value to add
070     */
071    protected void processProperty(Properties props, String name, String value) {
072        Object obj = props.get(name);
073        if (obj instanceof String[]) {
074            String[] mobj = (String[]) obj;
075            String[] m = new String[mobj.length + 1];
076            System.arraycopy(mobj, 0, m, 0, mobj.length);
077            m[mobj.length] = value;
078            props.put(name, m);
079        } else if (obj instanceof String) {
080            String[] m = new String[2];
081            m[0] = (String) obj;
082            m[1] = value;
083            props.put(name, m);
084        } else
085            props.put(name, value);
086    }
087
088    /**
089     * Loads {@code baseFile} (and optional per-environment overlays) into {@code props}.
090     *
091     * @param props target properties
092     * @param baseFile path to the base file (resolved against the {@link Environment})
093     * @param isEnv when {@code true}, also loads {@code baseFile-<env>.yml/.properties} overlays
094     * @throws ConfigurationException if no matching file can be loaded
095     */
096    protected void processFile(Properties props, String baseFile, boolean isEnv) throws ConfigurationException {
097        baseFile = Environment.get(baseFile);
098        boolean foundFile = false;
099        for (String file : getFiles(baseFile, isEnv?Environment.getEnvironment().getName():"")) {
100            foundFile |= readYamlFile(props, file);
101        }
102        if (!foundFile) {
103            throw new ConfigurationException("Could not find any matches for file: " + baseFile);
104        }
105    }
106
107    /**
108     * Builds the list of candidate file names: the base file plus per-environment overlays.
109     *
110     * @param baseFile base file name
111     * @param environmnents comma-separated list of environment names to overlay
112     * @return ordered list of candidate file names to attempt loading
113     */
114    protected List<String> getFiles(String baseFile, String environmnents) {
115        List<String> files = new ArrayList<>();
116        files.add(baseFile);
117        if (baseFile.endsWith(".yml") || baseFile.endsWith(".properties")) {
118            baseFile = baseFile.substring(0, baseFile.lastIndexOf("."));
119        }
120        for (String env : ISOUtil.commaDecode(environmnents)) {
121            if (!ISOUtil.isBlank(env)) {
122                files.add(baseFile + "-" + env + ".yml");
123                files.add(baseFile + "-" + env + ".properties");
124            }
125        }
126        return files;
127    }
128
129    /**
130     * Loads the contents of {@code fileName} into {@code props}, dispatching by extension.
131     *
132     * @param props target properties
133     * @param fileName path to a {@code .yml} or {@code .properties} file
134     * @return {@code true} if the file existed and was read, {@code false} otherwise
135     * @throws ConfigurationException if the file is present but cannot be parsed
136     */
137    protected boolean readYamlFile(Properties props, String fileName) throws ConfigurationException {
138        try {
139            if (fileName.endsWith(".yml")) {
140                return readYAML(props, fileName);
141            } else {
142                return readPropertyFile(props, fileName);
143            }
144        } catch (Exception ex) {
145            throw new ConfigurationException(fileName, ex);
146        }
147    }
148
149    /**
150     * Loads a Java {@code .properties} file into {@code props}.
151     *
152     * @param props target properties
153     * @param fileName path to the properties file
154     * @return {@code true} if the file existed and was read, {@code false} otherwise
155     * @throws IOException if the file cannot be read
156     */
157    protected boolean readPropertyFile(Properties props, String fileName) throws IOException {
158        File f = new File(fileName);
159        if (f.exists() && f.canRead()) {
160            try (FileInputStream in = new FileInputStream(f)) {
161                props.load(in);
162                return true;
163            }
164        }
165        return false;
166    }
167
168    /**
169     * Loads a YAML file into {@code props}, flattening nested maps into dotted keys.
170     *
171     * @param props target properties
172     * @param fileName path to the YAML file
173     * @return {@code true} if the file existed and was read, {@code false} otherwise
174     * @throws IOException if the file cannot be read
175     */
176    protected boolean readYAML(Properties props, String fileName) throws IOException {
177        File f = new File(fileName);
178        if (f.exists() && f.canRead()) {
179            try (InputStream fis = new FileInputStream(f)) {
180                Yaml yaml = new Yaml();
181                Iterable<Object> document = yaml.loadAll(fis);
182                document.forEach(d -> {
183                    Environment.flat(props, null, (Map<String, Object>) d, true);
184                });
185            }
186            return true;
187        }
188        return false;
189    }
190}