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}