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.core; 020 021import java.util.ArrayList; 022import java.util.List; 023import java.util.function.Predicate; 024import java.util.regex.Pattern; 025 026/** 027 * Declarative configuration validation framework. 028 * 029 * <p>Provides a fluent API for defining validation rules for jPOS configuration properties. 030 * 031 * <p>Example usage: 032 * <pre> 033 * ConfigValidator validator = new ConfigValidator() 034 * .required("bootstrap-servers") 035 * .required("app-id") 036 * .range("startup-timeout-seconds", 1, 3600) 037 * .range("max-retries", 0, 10) 038 * .nonEmpty("topic") 039 * .pattern("group-id", "^[a-zA-Z0-9._-]+$") 040 * .custom("processing-guarantee", 041 * v -> v.equals("at_least_once") || v.equals("exactly_once"), 042 * "must be 'at_least_once' or 'exactly_once'"); 043 * 044 * validator.validate(config); 045 * </pre> 046 * 047 * @author apr 048 * @since 3.0.2 049 */ 050public class ConfigValidator { 051 private final List<ValidationRule> rules = new ArrayList<>(); 052 053 /** 054 * Validate that a property is present and non-empty. 055 * @param key the property key 056 * @return this validator for chaining 057 */ 058 public ConfigValidator required(String key) { 059 rules.add(new ValidationRule(key, config -> { 060 String value = config.get(key, null); 061 if (value == null || value.trim().isEmpty()) { 062 throw new ConfigurationException("Required property '" + key + "' is missing or empty"); 063 } 064 })); 065 return this; 066 } 067 068 /** 069 * Validate that a property, if present, is non-empty. 070 * @param key the property key 071 * @return this validator for chaining 072 */ 073 public ConfigValidator nonEmpty(String key) { 074 rules.add(new ValidationRule(key, config -> { 075 String value = config.get(key, null); 076 if (value != null && value.trim().isEmpty()) { 077 throw new ConfigurationException("Property '" + key + "' must not be empty"); 078 } 079 })); 080 return this; 081 } 082 083 /** 084 * Validate that an integer property is within a specified range. 085 * Only validates if the property is present. 086 * @param key the property key 087 * @param min minimum value (inclusive) 088 * @param max maximum value (inclusive) 089 * @return this validator for chaining 090 */ 091 public ConfigValidator range(String key, int min, int max) { 092 rules.add(new ValidationRule(key, config -> { 093 String strValue = config.get(key, null); 094 if (strValue != null && !strValue.trim().isEmpty()) { 095 try { 096 int value = Integer.parseInt(strValue.trim()); 097 if (value < min || value > max) { 098 throw new ConfigurationException( 099 "Property '" + key + "' must be between " + min + " and " + max + ": " + value 100 ); 101 } 102 } catch (NumberFormatException e) { 103 throw new ConfigurationException("Property '" + key + "' must be an integer: " + strValue); 104 } 105 } 106 })); 107 return this; 108 } 109 110 /** 111 * Validate that a long property is within a specified range. 112 * Only validates if the property is present. 113 * @param key the property key 114 * @param min minimum value (inclusive) 115 * @param max maximum value (inclusive) 116 * @return this validator for chaining 117 */ 118 public ConfigValidator rangeLong(String key, long min, long max) { 119 rules.add(new ValidationRule(key, config -> { 120 String strValue = config.get(key, null); 121 if (strValue != null && !strValue.trim().isEmpty()) { 122 try { 123 long value = Long.parseLong(strValue.trim()); 124 if (value < min || value > max) { 125 throw new ConfigurationException( 126 "Property '" + key + "' must be between " + min + " and " + max + ": " + value 127 ); 128 } 129 } catch (NumberFormatException e) { 130 throw new ConfigurationException("Property '" + key + "' must be a long: " + strValue); 131 } 132 } 133 })); 134 return this; 135 } 136 137 /** 138 * Validate that a double property is within a specified range. 139 * Only validates if the property is present. 140 * @param key the property key 141 * @param min minimum value (inclusive) 142 * @param max maximum value (inclusive) 143 * @return this validator for chaining 144 */ 145 public ConfigValidator rangeDouble(String key, double min, double max) { 146 rules.add(new ValidationRule(key, config -> { 147 String strValue = config.get(key, null); 148 if (strValue != null && !strValue.trim().isEmpty()) { 149 try { 150 double value = Double.parseDouble(strValue.trim()); 151 if (value < min || value > max) { 152 throw new ConfigurationException( 153 "Property '" + key + "' must be between " + min + " and " + max + ": " + value 154 ); 155 } 156 } catch (NumberFormatException e) { 157 throw new ConfigurationException("Property '" + key + "' must be a double: " + strValue); 158 } 159 } 160 })); 161 return this; 162 } 163 164 /** 165 * Validate that a property matches a regex pattern. 166 * Only validates if the property is present. 167 * @param key the property key 168 * @param regex the regular expression pattern 169 * @return this validator for chaining 170 */ 171 public ConfigValidator pattern(String key, String regex) { 172 Pattern compiled = Pattern.compile(regex); 173 rules.add(new ValidationRule(key, config -> { 174 String value = config.get(key, null); 175 if (value != null && !value.trim().isEmpty()) { 176 if (!compiled.matcher(value.trim()).matches()) { 177 throw new ConfigurationException( 178 "Property '" + key + "' does not match pattern '" + regex + "': " + value 179 ); 180 } 181 } 182 })); 183 return this; 184 } 185 186 /** 187 * Validate a property using a custom predicate. 188 * Only validates if the property is present. 189 * @param key the property key 190 * @param predicate the validation predicate (returns true if valid) 191 * @param errorMessage error message if validation fails 192 * @return this validator for chaining 193 */ 194 public ConfigValidator custom(String key, Predicate<String> predicate, String errorMessage) { 195 rules.add(new ValidationRule(key, config -> { 196 String value = config.get(key, null); 197 if (value != null && !value.trim().isEmpty()) { 198 if (!predicate.test(value.trim())) { 199 throw new ConfigurationException("Property '" + key + "' " + errorMessage + ": " + value); 200 } 201 } 202 })); 203 return this; 204 } 205 206 /** 207 * Validate that at least one of the specified properties is present. 208 * @param keys the property keys 209 * @return this validator for chaining 210 */ 211 public ConfigValidator requireAtLeastOne(String... keys) { 212 rules.add(new ValidationRule("requireAtLeastOne", config -> { 213 for (String key : keys) { 214 String value = config.get(key, null); 215 if (value != null && !value.trim().isEmpty()) { 216 return; 217 } 218 } 219 throw new ConfigurationException( 220 "At least one of these properties must be present: " + String.join(", ", keys) 221 ); 222 })); 223 return this; 224 } 225 226 /** 227 * Validate that exactly one of the specified properties is present. 228 * @param keys the property keys 229 * @return this validator for chaining 230 */ 231 public ConfigValidator requireExactlyOne(String... keys) { 232 rules.add(new ValidationRule("requireExactlyOne", config -> { 233 int count = 0; 234 for (String key : keys) { 235 String value = config.get(key, null); 236 if (value != null && !value.trim().isEmpty()) { 237 count++; 238 } 239 } 240 if (count == 0) { 241 throw new ConfigurationException( 242 "Exactly one of these properties must be present: " + String.join(", ", keys) 243 ); 244 } else if (count > 1) { 245 throw new ConfigurationException( 246 "Only one of these properties can be present: " + String.join(", ", keys) 247 ); 248 } 249 })); 250 return this; 251 } 252 253 /** 254 * Validate that a boolean property has a valid value. 255 * Accepted values: true, false, yes, no, 1, 0 (case-insensitive). 256 * Only validates if the property is present. 257 * @param key the property key 258 * @return this validator for chaining 259 */ 260 public ConfigValidator validBoolean(String key) { 261 rules.add(new ValidationRule(key, config -> { 262 String value = config.get(key, null); 263 if (value != null && !value.trim().isEmpty()) { 264 String v = value.trim().toLowerCase(); 265 if (!v.equals("true") && !v.equals("false") && 266 !v.equals("yes") && !v.equals("no") && 267 !v.equals("1") && !v.equals("0")) { 268 throw new ConfigurationException( 269 "Property '" + key + "' must be a boolean (true/false/yes/no/1/0): " + value 270 ); 271 } 272 } 273 })); 274 return this; 275 } 276 277 /** 278 * Validate the configuration. 279 * @param config the configuration to validate 280 * @throws ConfigurationException if validation fails 281 */ 282 public void validate(Configuration config) throws ConfigurationException { 283 for (ValidationRule rule : rules) { 284 rule.validate(config); 285 } 286 } 287 288 /** 289 * Internal validation rule. 290 */ 291 private static class ValidationRule { 292 private final String key; 293 private final ValidationFunction validator; 294 295 ValidationRule(String key, ValidationFunction validator) { 296 this.key = key; 297 this.validator = validator; 298 } 299 300 void validate(Configuration config) throws ConfigurationException { 301 validator.validate(config); 302 } 303 } 304 305 /** 306 * Validation function interface. 307 */ 308 @FunctionalInterface 309 private interface ValidationFunction { 310 void validate(Configuration config) throws ConfigurationException; 311 } 312}