package org.simplejavamail.util; import org.simplejavamail.mailer.config.TransportStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import static java.util.Collections.unmodifiableMap; import static org.simplejavamail.internal.util.MiscUtil.checkArgumentNotEmpty; import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty; /** * Contains list of possible properties names and can produce a map of property values, if provided as file "{@value #DEFAULT_CONFIG_FILENAME}" on the * classpath or as environment property. * <p> * The following properties are allowed: * <pre><ul> * <li>simplejavamail.javaxmail.debug</li> * <li>simplejavamail.transportstrategy</li> * <li>simplejavamail.smtp.host</li> * <li>simplejavamail.smtp.port</li> * <li>simplejavamail.smtp.username</li> * <li>simplejavamail.smtp.password</li> * <li>simplejavamail.proxy.host</li> * <li>simplejavamail.proxy.port</li> * <li>simplejavamail.proxy.username</li> * <li>simplejavamail.proxy.password</li> * <li>simplejavamail.proxy.socks5bridge.port</li> * <li>simplejavamail.defaults.subject</li> * <li>simplejavamail.defaults.from.name</li> * <li>simplejavamail.defaults.from.address</li> * <li>simplejavamail.defaults.replyto.name</li> * <li>simplejavamail.defaults.replyto.address</li> * <li>simplejavamail.defaults.to.name</li> * <li>simplejavamail.defaults.to.address</li> * <li>simplejavamail.defaults.cc.name</li> * <li>simplejavamail.defaults.cc.address</li> * <li>simplejavamail.defaults.bcc.name</li> * <li>simplejavamail.defaults.bcc.address</li> * <li>simplejavamail.defaults.poolsize</li> * <li>simplejavamail.transport.mode.logging.only</li> * </ul></pre> */ public final class ConfigLoader { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigLoader.class); private static final String DEFAULT_CONFIG_FILENAME = "simplejavamail.properties"; /** * Initially try to load properties from "{@value #DEFAULT_CONFIG_FILENAME}". * * @see #loadProperties(String, boolean) * @see #loadProperties(InputStream, boolean) */ private static final Map<Property, Object> RESOLVED_PROPERTIES = new HashMap<>(); static { // static initializer block, because loadProperties needs to modify RESOLVED_PROPERTIES while loading // this is not possible when we are initializing the same field. // RESOLVED_PROPERTIES = loadProperties(DEFAULT_CONFIG_FILENAME); <-- not possible loadProperties(DEFAULT_CONFIG_FILENAME, false); } public enum Property { JAVAXMAIL_DEBUG("simplejavamail.javaxmail.debug"), TRANSPORT_STRATEGY("simplejavamail.transportstrategy"), SMTP_HOST("simplejavamail.smtp.host"), SMTP_PORT("simplejavamail.smtp.port"), SMTP_USERNAME("simplejavamail.smtp.username"), SMTP_PASSWORD("simplejavamail.smtp.password"), PROXY_HOST("simplejavamail.proxy.host"), PROXY_PORT("simplejavamail.proxy.port"), PROXY_USERNAME("simplejavamail.proxy.username"), PROXY_PASSWORD("simplejavamail.proxy.password"), PROXY_SOCKS5BRIDGE_PORT("simplejavamail.proxy.socks5bridge.port"), DEFAULT_SUBJECT("simplejavamail.defaults.subject"), DEFAULT_FROM_NAME("simplejavamail.defaults.from.name"), DEFAULT_FROM_ADDRESS("simplejavamail.defaults.from.address"), DEFAULT_REPLYTO_NAME("simplejavamail.defaults.replyto.name"), DEFAULT_REPLYTO_ADDRESS("simplejavamail.defaults.replyto.address"), DEFAULT_TO_NAME("simplejavamail.defaults.to.name"), DEFAULT_TO_ADDRESS("simplejavamail.defaults.to.address"), DEFAULT_CC_NAME("simplejavamail.defaults.cc.name"), DEFAULT_CC_ADDRESS("simplejavamail.defaults.cc.address"), DEFAULT_BCC_NAME("simplejavamail.defaults.bcc.name"), DEFAULT_BCC_ADDRESS("simplejavamail.defaults.bcc.address"), DEFAULT_POOL_SIZE("simplejavamail.defaults.poolsize"), TRANSPORT_MODE_LOGGING_ONLY("simplejavamail.transport.mode.logging.only"); private final String key; Property(final String key) { this.key = key; } public String key() { return key; } } /** * @return The value if not null or else the value from config file if provided or else <code>null</code>. */ public static <T> T valueOrProperty(final T value, final Property property) { return valueOrProperty(value, property, null); } /** * Returns the given value if not null and not empty, otherwise tries to resolve the given property and if still not found resot to the default value if * provided. * <p> * Null or blank values are never allowed, so they are always ignored. * * @return The value if not null or else the value from config file if provided or else <code>defaultValue</code>. */ public static <T> T valueOrProperty(final T value, final Property property, final T defaultValue) { if (!valueNullOrEmpty(value)) { LOGGER.trace("using provided argument value {} for property {}", value, property); return value; } else if (hasProperty(property)) { final T propertyValue = getProperty(property); LOGGER.trace("using value {} from config file for property {}", propertyValue, property); return propertyValue; } else { LOGGER.trace("no value provided as argument or in config file for property {}, using default value {}", property, defaultValue); return defaultValue; } } public static synchronized boolean hasProperty(final Property property) { return !valueNullOrEmpty(RESOLVED_PROPERTIES.get(property)); } public static synchronized <T> T getProperty(final Property property) { //noinspection unchecked return (T) RESOLVED_PROPERTIES.get(property); } /** * Loads properties from property file on the classpath, if provided. Calling this method only has effect on new Email and Mailer instances after * this. * * @param filename Any file that is on the classpath that holds a list of key=value pairs. * @param addProperties Flag to indicate if the new properties should be added or replacing the old properties. * @return The updated properties map that is used internally. */ public static Map<Property, Object> loadProperties(final String filename, final boolean addProperties) { final InputStream input = ConfigLoader.class.getClassLoader().getResourceAsStream(filename); if (input != null) { return loadProperties(input, addProperties); } LOGGER.debug("Property file not found on classpath, skipping config file"); return new HashMap<>(); } /** * Loads properties from another properties source, in case you want to provide your own list. * * @param properties Your own list of properties * @param addProperties Flag to indicate if the new properties should be added or replacing the old properties. * @return The updated properties map that is used internally. */ public static Map<Property, Object> loadProperties(final Properties properties, final boolean addProperties) { if (!addProperties) { RESOLVED_PROPERTIES.clear(); } RESOLVED_PROPERTIES.putAll(readProperties(properties)); return unmodifiableMap(RESOLVED_PROPERTIES); } /** * Loads properties from property {@link File}, if provided. Calling this method only has effect on new Email and Mailer instances after this. * * @param filename Any file reference that holds a properties list. * @param addProperties Flag to indicate if the new properties should be added or replacing the old properties. * @return The updated properties map that is used internally. */ public static Map<Property, Object> loadProperties(final File filename, final boolean addProperties) { try { return loadProperties(new FileInputStream(filename), addProperties); } catch (final FileNotFoundException e) { throw new IllegalStateException("error reading properties file from File", e); } } /** * Loads properties from {@link InputStream}. Calling this method only has effect on new Email and Mailer instances after this. * * @param inputStream Source of property key=value pairs separated by newline \n characters. * @param addProperties Flag to indicate if the new properties should be added or replacing the old properties. * @return The updated properties map that is used internally. */ public static synchronized Map<Property, Object> loadProperties(final InputStream inputStream, final boolean addProperties) { final Properties prop = new Properties(); try { prop.load(checkArgumentNotEmpty(inputStream, "InputStream was null")); } catch (final IOException e) { throw new IllegalStateException("error reading properties file from inputstream", e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (final IOException e) { LOGGER.error(e.getMessage(), e); } } } if (!addProperties) { RESOLVED_PROPERTIES.clear(); } RESOLVED_PROPERTIES.putAll(readProperties(prop)); return unmodifiableMap(RESOLVED_PROPERTIES); } /** * @return All properties in priority of System property > File properties. */ private static Map<Property, Object> readProperties(final Properties fileProperties) { final Properties filePropertiesLeft = new Properties(); filePropertiesLeft.putAll(fileProperties); final Map<Property, Object> resolvedProps = new HashMap<>(); for (final Property prop : Property.values()) { if (System.getProperty(prop.key) != null) { System.out.println(prop.key + ": " + System.getProperty(prop.key)); } final Object asSystemProperty = parsePropertyValue(System.getProperty(prop.key)); if (asSystemProperty != null) { resolvedProps.put(prop, asSystemProperty); filePropertiesLeft.remove(prop.key); } else { final Object asEnvProperty = parsePropertyValue(System.getenv().get(prop.key)); if (asEnvProperty != null) { resolvedProps.put(prop, asEnvProperty); filePropertiesLeft.remove(prop.key); } else { final Object rawValue = filePropertiesLeft.remove(prop.key); if (rawValue != null) { if (rawValue instanceof String) { resolvedProps.put(prop, parsePropertyValue((String) rawValue)); } else { resolvedProps.put(prop, rawValue); } } } } } if (!filePropertiesLeft.isEmpty()) { throw new IllegalArgumentException("unknown properties provided " + filePropertiesLeft); } return resolvedProps; } /** * @return The property value in boolean, integer or as original string value. */ static Object parsePropertyValue(final String propertyValue) { if (propertyValue == null) { return null; } // read boolean value final Map<String, Boolean> booleanConversionMap = new HashMap<>(); booleanConversionMap.put("0", false); booleanConversionMap.put("1", true); booleanConversionMap.put("false", false); booleanConversionMap.put("true", true); booleanConversionMap.put("no", false); booleanConversionMap.put("yes", true); if (booleanConversionMap.containsKey(propertyValue)) { return booleanConversionMap.get(propertyValue.toLowerCase()); } // read number value try { return Integer.valueOf(propertyValue); } catch (final NumberFormatException nfe) { // ok, so not a number } // read TransportStrategy value try { return TransportStrategy.valueOf(propertyValue); } catch (final IllegalArgumentException nfe) { // ok, so not a TransportStrategy either } // return value as is (which should be string) return propertyValue; } }