All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.yadaframework.core.YadaConfiguration Maven / Gradle / Ivy

There is a newer version: 0.7.7.R4
Show newest version
package net.yadaframework.core;

import java.io.File;
import java.nio.file.Path;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.configuration2.ConfigurationUtils;
import org.apache.commons.configuration2.ImmutableHierarchicalConfiguration;
import org.apache.commons.configuration2.builder.combined.CombinedConfigurationBuilder;
import org.apache.commons.configuration2.builder.combined.ReloadingCombinedConfigurationBuilder;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.format.Formatter;

import jakarta.servlet.http.HttpServletRequest;
import net.yadaframework.exceptions.YadaConfigurationException;
import net.yadaframework.exceptions.YadaInternalException;
import net.yadaframework.exceptions.YadaInvalidValueException;
import net.yadaframework.persistence.entity.YadaClause;
import net.yadaframework.raw.YadaIntDimension;
import net.yadaframework.web.YadaViews;

/**
 * Classe che estende CombinedConfiguration aggiungendo metodi di gestione della configurazione specifici.
 */
public abstract class YadaConfiguration {
	private static Logger log = LoggerFactory.getLogger(YadaConfiguration.class);

	protected ImmutableHierarchicalConfiguration configuration;
	protected CombinedConfigurationBuilder builder;

	// Cached values
	// Questi valori li memorizzo perchè probabilmente verranno controllati
	// ad ogni pageview e comunque non mi aspetto che cambino a runtime
	private String uploadsDir = null;
	private String contentUrl = null;
	private String contentName = null;
	private String environment = null;
	private String version = null;
	private String yadaVersion = null;
	private String releaseDate = null;
	private String build = null;
	private String logoImage = null;
	private Boolean production = null;
	private Boolean development = null;
	private Boolean beta = null;
	private Boolean alpha = null;
	private Map roleIdToKeyMap = null;
	private Map roleKeyToIdMap = null;
	private Object roleMapMonitor = new Object();
	private String googleClientId = null;
	private String googleSecret = null;
	private String facebookAppId = null;
	private String facebookPageId = null;
	private String facebookSecret = null;
	private String serverAddress = null;
	private String webappAddress = null;
	private int facebookType = -1;
	private int googleType = -1;
	private String tagReservedPrefix = null;
	private int tagMaxNum = -1;
	private int tagMaxSuggested = -1;
	private int tagFilterMax = -1;
	private int maxPwdLen = -1;
	private int minPwdLen = -1;
	private String errorPageForward = null;
	private List locales = null;
	private Set localeSet = null;
	private List localeObjects = null;
	private Map languageToCountry = null;
	private Boolean localeAddCountry = null;
	private Boolean localePathVariableEnabled = null;
	private Locale defaultLocale = null;
	private boolean defaultLocaleChecked = false;
	private Map>> localSetCache = new HashMap<>(); // Deprecated
	private String targetImageExtension=null;
	private String preserveImageExtensions=null;
	private String defaultNotifyModalView = null;
	private File uploadsFolder = null;
	private File tempFolder = null;
	private String googleApiKey = null;
	private Integer bootstrapVersion = null;
	
	/**
	 * Copy the already initialised configuration to a different instance
	 * @param yadaConfiguration a subclass of YadaConfiguration
	 */
	public void copyTo(YadaConfiguration yadaConfiguration) {
		yadaConfiguration.builder = this.builder;
		yadaConfiguration.configuration = this.configuration;
	}

	/**
	 * Returns the configured timeout for asynchronous requests in seconds.
	 * Equivalent to the Tomcat parameter asyncTimeout, but more effective (the application-defined parameter takes precedence).
	 * @return the configured timeout in minutes or 0 for the default
	 */
	public int getAsyncTimeoutMinutes() {
		return configuration.getInt("config/asyncTimeoutMinutes", 0);
	}

	/**
	 * @return true if the embedded db should be used instead of the external MySQL
	 */
	public boolean isDatabaseEnabled() {
		return configuration.getBoolean("config/database/@enabled", true);
	}
	
	/**
	 * @return true if the embedded db should be used instead of the external MySQL
	 */
	public boolean isUseEmbeddedDatabase() {
		return configuration.getBoolean("config/database/embedded/@enabled", false);
	}
	
	/**
	 * @return the location of the data folder for the embedded database
	 */
	public String getEmbeddedDatabaseDataDir() {
		return configuration.getString("config/database/embedded/datadir", "dbembedded");
	}
	
	/**
	 * Returns a pointer to the sql file configured for loading the embedded database at startup
	 * @return the File to use for reading or null if the file is not configured or not readable
	 */
	public File getEmbeddedDatabaseSourceSql() {
		String sourceSqlPath = configuration.getString("config/database/embedded/sourceSql", null);
		if (sourceSqlPath!=null) {
			File result = new File(sourceSqlPath);
			if (result.canRead()) {
				return result;
			}
		}
		log.debug("No source sql to load at startup");
		return null;
	}
	
	/**
	 * Given three strings (e.g. classes) returns the one that corresponds to the
	 * configured Bootstrap version, from 3 to 5
	 * @param classForB3
	 * @param classForB4
	 * @param classForB5
	 * @return the argument that corresponds to the configured Bootstrap version, or the last one.
	 */
	public String getForB3B4B5(String classForB3, String classForB4, String classForB5) {
		switch (getBootstrapVersion()) {
			case 3: {
				return classForB3;
			}
			case 4: {
				return classForB4;
			}
			case 5: {
				return classForB5;
			}
		}
		return classForB5;
	}
	
	public boolean isB5() {
		return getBootstrapVersion()==5;
	}
	
	public boolean isB4() {
		return getBootstrapVersion()==4;
	}
	
	public boolean isB3() {
		return getBootstrapVersion()==3;
	}
	
	/**
	 * The configured bootstrap version may be used to return the correct html for modals etc.
	 * @return the configured bootstrap version, defaults to 5
	 */
	public int getBootstrapVersion() {
		if (bootstrapVersion==null) {
			bootstrapVersion = configuration.getInt("config/bootstrapVersion", 5);
		}
		return bootstrapVersion;
	}
	
	/**
	 * Returns the configured Date Formatter. Use like <dateFormatter>net.yadaframework.components.YadaDateFormatter</dateFormatter> in config.
	 * Defaults to DefaultFormattingConversionService when not configured.
	 * @return
	 */
	public Formatter getDateFormatter() {
		String dateFormatterClass = configuration.getString("config/dateFormatter", null);
		try {
			if (dateFormatterClass!=null) {
				return (Formatter) Class.forName(dateFormatterClass).newInstance();
			}
		} catch (Exception e) {
			log.error("Can't make instance of {} (ignored)", dateFormatterClass, e);
		}
		return null;
	}
	
	/**
	 * Quick way to specify a new value for all embedded tomcat ports:
	 * the offset will be added to the default value.
	 * Some values may generate errors for overlapping ports e.g. an offset of 71 would set the AJP port 
	 * to 8080 that may be already in use by the HTTP port of another instance with the offset at 0.
	 */
	public int getTomcatPortOffset() {
		return configuration.getInt("config/tomcat/ports/offset", 0);
	}

	public int getTomcatHttpPort() {
		return configuration.getInt("config/tomcat/ports/http", 8080) + getTomcatPortOffset();
	}
	
	public int getTomcatHttpsPort() {
		return configuration.getInt("config/tomcat/ports/https", 8443) + getTomcatPortOffset();
	}
	
	public int getTomcatAjpPort() {
		return configuration.getInt("config/tomcat/ports/ajp", 8009) + getTomcatPortOffset();
	}
	
	public int getTomcatAjpRedirectPort() {
		return configuration.getInt("config/tomcat/ports/ajpRedirect", 8443) + getTomcatPortOffset();
	}
	
	public int getTomcatShutdownPort() {
		return configuration.getInt("config/tomcat/ports/shutdown", 8005) + getTomcatPortOffset();
	}
	
	public File getTomcatKeystoreFile() {
		return new File(configuration.getString("config/tomcat/keystore/file", "/srv/devtomcatkeystore"));
	}
	
	public String getTomcatKeystorePassword() {
		return configuration.getString("config/tomcat/keystore/password", "changeit");
	}

	/**
	 * The maximum size in bytes of the POST which will be handled by 
	 * the container FORM URL parameter parsing. The limit can be disabled 
	 * by setting this attribute to a value less than zero. If not specified, 
	 * this attribute is set to 2097152 (2 MiB).
	 */
	public int getTomcatMaxPostSize() {
		return configuration.getInt("config/tomcat/maxPostSize", 2097152);
	}
	
	/**
	 * Google api key (for Maps etc) read from "security.properties"
	 * @return
	 */
	public String getGoogleApiKey() {
		if (googleApiKey==null) {
			// Does not start with "config/" because it is in security.properties
			googleApiKey = configuration.getString("google/api/key", "");
		}
		return googleApiKey;
	}

	/**
	 * Gets the value of any boolean config key defined in the /config/local configuration 
	 * file (it should reside on the developers computer in a personal folder, not shared).
	 * When not defined or not a boolean, the value is false.
	 * Only available in dev env.
	 * @param name
	 * @return
	 */
	public boolean isLocalFlag(String name) {
		if (!isDevelopmentEnvironment()) {
			return false;
		}
		return configuration.getBoolean("config/local/"+name, false);
	}
	
	/**
	 * Gets the value of any config key defined in the /config/local configuration 
	 * file (it should reside on the developers computer in a personal folder, not shared).
	 * When not defined, the value is an empty string.
	 * Only available in dev env.
	 * @param name
	 * @return
	 */
	public Object getLocalConfig(String name) {
		if (!isDevelopmentEnvironment()) {
			return "";
		}
		return configuration.getString("config/local/"+name, "");
	}

	/**
	 * Returns the configured path for the notification modal.
	 * The configuration path is config/paths/notificationModalView
	 * @return
	 */
	public String getNotifyModalView() {
		if (defaultNotifyModalView==null) {
			defaultNotifyModalView = configuration.getString("config/paths/notificationModalView", getForB3B4B5(YadaViews.AJAX_NOTIFY_B3, YadaViews.AJAX_NOTIFY_B4, YadaViews.AJAX_NOTIFY_B5));
		}
		return defaultNotifyModalView;
	}

	/**
	 * Get the width/height of an image, for desktop, mobile and pdf
	 * @param relativeKey the key like "/product/gallery", relative to "config/dimension"
	 * @return { desktopDimension, mobileDimension, pdfDimension }, the cell is null when not configured
	 */
	protected YadaIntDimension[] getImageDimensions(String relativeKey) {
		YadaIntDimension[] result = new YadaIntDimension[3];
		result[0] = splitDimension(relativeKey, "/desktop");
		result[1] = splitDimension(relativeKey, "/mobile");
		result[2] = splitDimension(relativeKey, "/pdf");
		return result;
	}

	/**
	 * Converts a value like <desktop>1920,973</desktop> to a YadaIntDimension
	 * @param relativeKey relativeKey the key like "/product/gallery", relative to "config/dimension"
	 * @param type "/desktop" or "/mobile" etc as found in the configuration xml
	 * @return YadaIntDimension or null
	 */
	private YadaIntDimension splitDimension(String relativeKey, String type) {
		String widthHeight = configuration.getString("config/dimension" + relativeKey + type, null);
		if (widthHeight!=null) {
			String[] parts = widthHeight.split(",");
			int width = Integer.parseInt(parts[0]);
			int height = Integer.parseInt(parts[1]);
			return new YadaIntDimension(width, height);
		}
		return null;
	}

	/**
	 * Returns the image extension (without dot) to use when uploading user images. Defaults to "jpg".
	 * @return
	 */
	public String getTargetImageExtension() {
		if (targetImageExtension==null) {
			targetImageExtension = configuration.getString("config/dimension/@targetImageExtension", "jpg");
			targetImageExtension = StringUtils.removeStart(targetImageExtension, "."); // Remove dot if any
		}
		return targetImageExtension;
	}

	/**
	 * Check if the image extension has to be preserved when converting.
	 * The value is taken from <dimension targetImageExtension="jpg" preserveImageExtension="gif,webp">
	 * @param extensionNoDot
	 * @return
	 */
	public boolean isPreserveImageExtension(String extensionNoDot) {
		if (preserveImageExtensions==null) {
			preserveImageExtensions = configuration.getString("config/dimension/@preserveImageExtensions", "");
			preserveImageExtensions = StringUtils.remove(preserveImageExtensions, '.'); // Remove dot if any
			// Add commas for easy search
			preserveImageExtensions = ','+preserveImageExtensions.toLowerCase()+',';
		}
		return preserveImageExtensions.contains(','+extensionNoDot.toLowerCase()+',');

	}

	/**
	 * Tells if the YadaFileManager has to delete uploaded files when attaching them, or to keep them in the uploads folder
	 * for later use. true by default.
	 * @return
	 */
	public boolean isFileManagerDeletingUploads() {
		return configuration.getBoolean("config/yadaFileManager/deleteUploads", true);
	}

	/**
	 * Looks in the configuration for a list of ids, then fetches from message.properties the localized text corresponding to the ids.
	 * The id is appended to the messageBaseKey parameter after a dot.
	 * The result is a sorted set of id-text, sorted on the text.
	 * 
* Example:
* In the config we have *
	 * <countries>
	 * 	<countryId>1</countryId>
	 * 	<countryId>2</countryId>
	 * 	<countryId>3</countryId>
	 * 	<countryId>4</countryId>
	 * </countries>
	 * 
* In message.properties we have *
	 * customer.country.1 = England
	 * customer.country.2 = France
	 * customer.country.3 = USA
	 * customer.country.4 = Albania
	 * 
* The call to this method will be: *
	 * config.getLocalSet("config/countries/countryId", "customer.country", locale, messageSource);
	 * 
* The resulting set will have the countries in the following order: Albania, England, France, USA. * Results are cached forever. * * @param configPath item in the configuration file that holds the id. There should be more than one such item in the configuration. * Example: config/countries/countryId * @param messageBaseKey the prefix of the message.properties key to which the id should be appended in order to retrieve the localized text. * Example: customer.country * @param locale * @param messageSource * @return * @see YadaLocalEnum */ @Deprecated // Never tested and never used. You are probably better off with a YadaLocalEnum public SortedSet> getLocalSet(String configPath, String messageBaseKey, Locale locale, MessageSource messageSource) { String cacheKey = configPath + messageBaseKey + locale.toString(); SortedSet> result = localSetCache.get(cacheKey); if (result!=null) { return result; } result = new TreeSet<>(new Comparator>() { @Override public int compare(Entry element1, Entry element2) { return element1.getValue().compareTo(element2.getValue()); } }); List ids = configuration.getList(Integer.class, configPath); for (Integer id : ids) { String key = messageBaseKey + "." + id; String localizedText = messageSource.getMessage(key, null, key, locale); Entry entry = new AbstractMap.SimpleImmutableEntry<>(id, localizedText); result.add(entry); } localSetCache.put(cacheKey, result); return result; } public String getUploadsDirname() { if (uploadsDir==null) { uploadsDir = configuration.getString("config/paths/uploadsDir", "uploads"); } return uploadsDir; } // This has been removed because uploaded files should not be public. // They should be moved to a public folder in order to show them via apache. // /** // * Returns the url for the uploads folder // * @return // */ // public String getUploadsUrl() { // return this.getContentUrl() + "/" + getUploadsDirname(); // } /** * Folder where files are uploaded before processing. Should not be a public folder. * @return */ public File getUploadsFolder() { if (uploadsFolder==null) { uploadsFolder = new File(getBasePathString(), getUploadsDirname()); if (!uploadsFolder.exists()) { uploadsFolder.mkdirs(); } } return uploadsFolder; } /** * Returns true if YadaEmaiService should throw an exception instead of returning false when it receives an exception on send * @return */ public boolean isEmailThrowExceptions() { return configuration.getBoolean("config/email/@throwExceptions", false); } /** * @return the url to redirect to after sending the password reset email */ public String getPasswordResetSent(Locale locale) { String link = configuration.getString("config/security/passwordReset/passwordResetSent", "/"); return fixLink(link, locale); } /** * @return the link to use for the registration confirmation, e.g. "/my/registrationConfirmation" or "/en/my/registrationConfirmation" */ public String getRegistrationConfirmationLink(Locale locale) { String link = configuration.getString("config/security/registration/confirmationLink", "/registrationConfirmation"); return fixLink(link, locale); } private String fixLink(String link, Locale locale) { if (!link.endsWith("/")) { link = link + "/"; } if (!link.startsWith("/")) { link = "/" + link; } if (isLocalePathVariableEnabled()) { // Add the locale link = "/" + locale.getLanguage() + link; } return link; } /** * Checks if an email address has been blacklisted in the configuration * @param email * @return true for a blacklisted email address */ public boolean emailBlacklisted(String email) { if (email==null) { log.warn("Blacklisting null email address"); return true; } String[] patternStrings = configuration.getStringArray("config/email/blacklistPattern"); for (String patternString : patternStrings) { // (?i) is for case-insensitive match if (email.matches("(?i)"+patternString)) { log.warn("Email '{}' blacklisted by '{}'", email, patternString); return true; } } return false; } /** * True if during startup YadaAppConfig should run the FlyWay migrate operation * @return */ public boolean useDatabaseMigrationAtStartup() { return configuration.getBoolean("config/database/databaseMigrationAtStartup", false); } /** * Name of the flyway schema history table. Uses the default name of "flyway_schema_history" when not configured. * @return the table name, never null */ public String flywayTableName() { return configuration.getString("config/database/flywayTableName", "flyway_schema_history"); } /** * "Out of order" flag in FlyWay * https://flywaydb.org/documentation/configuration/parameters/outOfOrder * @return */ public boolean useDatabaseMigrationOutOfOrder() { return configuration.getBoolean("config/database/databaseMigrationAtStartup/@outOfOrder", false); } /** * Returns the configured default locale. * @return the default locale, or null if no default is set */ public Locale getDefaultLocale() { if (!defaultLocaleChecked) { defaultLocaleChecked = true; String localeString = configuration.getString("config/i18n/locale[@default='true']", null); if (localeString!=null) { try { String country = getCountryForLanguage(localeString); if (country!=null) { localeString += "_" + country; } defaultLocale = LocaleUtils.toLocale(localeString); } catch (IllegalArgumentException e) { throw new YadaConfigurationException("Locale {} is invalid", localeString); } } else { log.warn("No default locale has been set with : set a default locale if you don't want empty strings returned for missing localized values in the database"); } } return defaultLocale; } /** * True if the filter enables the use of locales in the url, like /en/mypage * @return */ public boolean isLocalePathVariableEnabled() { if (localePathVariableEnabled==null) { localePathVariableEnabled = configuration.getBoolean("config/i18n/@localePathVariable", false); } return localePathVariableEnabled.booleanValue(); } // /** // * Returns the locale to be injected in the request. // * Used when the locale string only has the language and you want to store the full language_COUNTRY locale in your request. // * The configuration must be like <locale>es<request>es_ES</request></locale> // * @param locale // * @return either the input parameter unaltered or the "request" locale if configured // */ // public String getLocaleForRequest(String locale) { // return configuration.getString("config/i18n/locale[text()='" + locale + "']/request", locale); // } public String getCountryForLanguage(String language) { if (languageToCountry==null) { languageToCountry = new HashMap<>(); List locales = configuration.immutableConfigurationsAt("config/i18n/locale"); for (ImmutableHierarchicalConfiguration localeConfig : locales) { String languageKey = localeConfig.getString("."); String countryValue = localeConfig.getString("./@country"); languageToCountry.put(languageKey, countryValue); } } return languageToCountry.get(language); } /** * True if locale paths only have the language component ("en") but you also need the country component ("US") in the request Locale */ public boolean isLocaleAddCountry() { if (localeAddCountry==null) { localeAddCountry = configuration.containsKey("config/i18n/locale/@country"); } return localeAddCountry.booleanValue(); } /** * Get the set of configured locales in no particular order. * @deprecated because doesn't consider countries when configured */ @Deprecated public Set getLocaleSet() { if (localeSet==null) { getLocaleStrings(); // Init the set } return localeSet; } /** * Get a list of iso2 locales that the webapp can handle * @deprecated because doesn't consider countries when configured */ @Deprecated public List getLocaleStrings() { if (locales==null) { locales = Arrays.asList(configuration.getStringArray("config/i18n/locale")); localeSet = new HashSet(); for (String locale : locales) { try { Locale localeObject = LocaleUtils.toLocale(locale); localeSet.add(localeObject); } catch (IllegalArgumentException e) { throw new YadaConfigurationException("Locale {} is invalid", locale); } } } return locales; } /** * Returns the configured locales as objects, using countries if configured */ public List getLocales() { if (localeObjects==null) { localeObjects = new ArrayList(); List locales = configuration.immutableConfigurationsAt("config/i18n/locale"); for (ImmutableHierarchicalConfiguration localeConfig : locales) { String languageKey = localeConfig.getString("."); String countryValue = localeConfig.getString("./@country", null); Locale locale = countryValue==null?new Locale(languageKey):new Locale(languageKey, countryValue); localeObjects.add(locale); } localeObjects = Collections.unmodifiableList(localeObjects); } return localeObjects; } /** * Returns the page to forward to after an unhandled exception or HTTP error. * The value is the @RequestMapping value, without language in the path * @return */ public String getErrorPageForward() { if (errorPageForward==null) { errorPageForward = configuration.getString("/config/paths/errorPageForward", "/"); } return errorPageForward; } public boolean isBeta() { if (beta==null) { beta=configuration.getBoolean("/config/info/beta", false); } return beta; } public boolean isAlpha() { if (alpha==null) { alpha=configuration.getBoolean("/config/info/alpha", false); } return alpha; } public int getMaxPasswordLength() { if (maxPwdLen<0) { maxPwdLen = configuration.getInt("config/security/passwordLength/@max", 16); } return maxPwdLen; } public int getMinPasswordLength() { if (minPwdLen<0) { minPwdLen = configuration.getInt("config/security/passwordLength/@min", 0); } return minPwdLen; } /** * Ritorna il path del folder in cui sono memorizzate le immagini temporanee accessibili via web, ad esempio per il preview della newsletter * @return */ public File getTempImageDir() { if (tempFolder==null) { tempFolder = new File(getContentPath(), getTempImageRelativePath()); if (!tempFolder.exists()) { tempFolder.mkdirs(); } } return tempFolder; } /** * Ritorna il path del folder in cui sono memorizzate le immagini temporanee accessibili via web, relativamente al folder "contents" * @return */ public String getTempImageRelativePath() { return "/tmp"; } /** * Return the webapp address without a trailing slash. E.g. http://www.mysite.com/app or http://www.mysite.com * The address is computed from the request when not null, else it is read from the configuration. * @return */ public String getWebappAddress(HttpServletRequest request) { if (webappAddress==null) { if (request!=null) { StringBuilder address = new StringBuilder(getServerAddress(request)); // http://www.example.com address.append(request.getContextPath()); // http://www.example.com/appname // Adding the language is a bug because the value is cached // if (isLocalePathVariableEnabled() && locale!=null) { // address.append("/").append(locale.getLanguage()); // http://www.example.com/en // } webappAddress = address.toString(); } else { webappAddress = getWebappAddress(); } } return webappAddress; } /** * Return the webapp address without a trailing slash. E.g. http://www.mysite.com/app or http://www.mysite.com * @return */ public String getWebappAddress() { if (webappAddress==null) { String contextPath = StringUtils.removeEnd(configuration.getString("config/paths/contextPath", ""), "/"); webappAddress = getServerAddress() + "/" + contextPath; webappAddress = StringUtils.removeEnd(webappAddress, "/"); } return webappAddress; } /** * Return the server address without a trailing slash. E.g. http://col.letturedametropolitana.it * Warning: this version does not work properly behind an ajp connector * @return * @deprecated use {@link #getServerAddress()} instead */ @Deprecated public String getServerAddress(HttpServletRequest request) { if (serverAddress==null) { StringBuilder address = new StringBuilder(); address.append(request.getScheme()).append("://").append(request.getServerName()); // http://www.example.com if (request.getServerPort()!=80 && request.getServerPort()!=443) { address.append(":").append(request.getServerPort()); // http://www.example.com:8080 } serverAddress = address.toString(); } return serverAddress; } /** * Return the server address without a trailing slash. E.g. http://col.letturedametropolitana.it * @return */ public String getServerAddress() { if (serverAddress==null) { serverAddress = StringUtils.removeEnd(configuration.getString("config/paths/serverAddress", "serverAddressUnset"), "/"); } return serverAddress; } public String getEmailLogoImage() { if (logoImage==null) { logoImage = configuration.getString("config/email/logoImage", ""); // e.g. /res/img/logo-small.jpg } return logoImage; } /** * Returns ".min" if this is not a development environment. Use like