package org.gbif.ipt.config; import org.gbif.ipt.service.InvalidConfigException; import org.gbif.ipt.service.InvalidConfigException.TYPE; import org.gbif.ipt.utils.InputStreamUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; import java.math.BigDecimal; import java.net.URI; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.Properties; import javax.validation.constraints.NotNull; import javax.ws.rs.core.UriBuilder; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.Closer; import com.google.gson.annotations.Since; import com.google.inject.Inject; import com.google.inject.Singleton; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; @Singleton public class AppConfig { public enum REGISTRY_TYPE { PRODUCTION, DEVELOPMENT } protected static final String DATADIR_PROPFILE = "ipt.properties"; private static final String CLASSPATH_PROPFILE = "application.properties"; public static final String BASEURL = "ipt.baseURL"; @Since(2.1) public static final String CORE_ROW_TYPES = "ipt.core_rowTypes"; public static final String CORE_ROW_ID_TERMS = "ipt.core_idTerms"; public static final String PROXY = "proxy"; public static final String DEBUG = "debug"; public static final String ARCHIVAL_MODE = "archivalMode"; public static final String ANALYTICS_GBIF = "analytics.gbif"; public static final String ANALYTICS_KEY = "analytics.key"; public static final String IPT_LATITUDE = "location.lat"; public static final String IPT_LONGITUDE = "location.lon"; private static final String PRODUCTION_TYPE_LOCKFILE = ".gbifreg"; private Properties properties = new Properties(); private static final Logger LOG = Logger.getLogger(AppConfig.class); private DataDir dataDir; private REGISTRY_TYPE type; // to support compatibility with historical data directories, we default to the original hard coded // types that were scattered across the code. private static final List<String> DEFAULT_CORE_ROW_TYPES = ImmutableList.of(Constants.DWC_ROWTYPE_OCCURRENCE, Constants.DWC_ROWTYPE_TAXON, Constants.DWC_ROWTYPE_EVENT); // mapping of the id to the term that is the row ID private static final Map<String, String> DEFAULT_CORE_ROW_TYPES_ID_TERMS = Maps.newHashMap((ImmutableMap .of(Constants.DWC_ROWTYPE_OCCURRENCE, Constants.DWC_OCCURRENCE_ID, Constants.DWC_ROWTYPE_TAXON, Constants.DWC_TAXON_ID, Constants.DWC_ROWTYPE_EVENT, Constants.DWC_EVENT_ID))); private static List<String> coreRowTypes = DEFAULT_CORE_ROW_TYPES; private static Map<String, String> coreRowTypeIdTerms = DEFAULT_CORE_ROW_TYPES_ID_TERMS; private AppConfig() { } @Inject public AppConfig(DataDir dataDir) throws InvalidConfigException { this.dataDir = dataDir; // also loaded via ConfigManager constructor if datadir was linked at startup already // If it wasn't, this is the only place to load at least the default classpath config settings loadConfig(); } /** * Returns the term to use as the ID for core. * * @return The expected field for the given core. */ public static String coreIdTerm(String rowType) { if (coreRowTypeIdTerms.containsKey(rowType)) { return coreRowTypeIdTerms.get(rowType); } else { throw new IllegalArgumentException("IPT is not configured correctly to support rowType[" + rowType + "]. Hint: are you missing mappings for the row type and id term in the properties?"); } } /** * Returns the core types that the application is configured to support. * Exposed with static accessor to allow model objects to access this without the need for dependency * injection. This is set during */ public static List<String> getCoreRowTypes() { return coreRowTypes; } /** * @return true if the row type is suitable for use as a core. */ public static boolean isCore(String rowType) { return coreRowTypes.contains(rowType); } public boolean debug() { return "true".equalsIgnoreCase(properties.getProperty(DEBUG)); } public boolean devMode() { return !"false".equalsIgnoreCase(properties.getProperty("dev.devmode")); } public String getAnalyticsKey() { return properties.getProperty(ANALYTICS_KEY); } public String getBaseUrl() { String base = properties.getProperty(BASEURL); while (base != null && base.endsWith("/")) { base = base.substring(0, base.length() - 1); } return base; } public DataDir getDataDir() { return dataDir; } public Double getLatitude() { try { String val = properties.getProperty(IPT_LATITUDE); if (!Strings.isNullOrEmpty(val)) { return Double.valueOf(val); } } catch (NumberFormatException e) { LOG.warn("IPT latitude was invalid: " + e.getMessage()); } return null; } public Double getLongitude() { try { String val = properties.getProperty(IPT_LONGITUDE); if (!Strings.isNullOrEmpty(val)) { return Double.valueOf(val); } } catch (NumberFormatException e) { LOG.warn("IPT longitude was invalid: " +e.getMessage()); } return null; } public int getMaxThreads() { try { return Integer.parseInt(getProperty("dev.maxthreads")); } catch (NumberFormatException e) { return 3; } } public String getProperty(String key) { return properties.getProperty(key); } public String getProxy() { return properties.getProperty(PROXY); } public REGISTRY_TYPE getRegistryType() { return type; } private File getRegistryTypeLockFile() { return dataDir.configFile(PRODUCTION_TYPE_LOCKFILE); } public String getRegistryUrl() { if (REGISTRY_TYPE.PRODUCTION == type) { return getProperty("dev.registry.url"); } return getProperty("dev.registrydev.url"); } /** * @return the GBIF Data Portal base URL, different depending on the IPT's mode */ public String getPortalUrl() { if (REGISTRY_TYPE.PRODUCTION == type) { return getProperty("dev.portal.url"); } return getProperty("dev.portaldev.url"); } /** * @return String URI to resource's last published darwin core archive (no version number) */ @NotNull public String getResourceArchiveUrl(@NotNull String shortname) { Preconditions.checkNotNull(getBaseUrl()); return UriBuilder.fromPath(getBaseUrl()).path(Constants.REQ_PATH_DWCA) .queryParam(Constants.REQ_PARAM_RESOURCE, shortname).build().toString(); } /** * @return String URI to resource's last published EML file (no version number) */ @NotNull public String getResourceEmlUrl(@NotNull String shortname) { Preconditions.checkNotNull(getBaseUrl()); return UriBuilder.fromPath(getBaseUrl()).path(Constants.REQ_PATH_EML) .queryParam(Constants.REQ_PARAM_RESOURCE, shortname).build().toString(); } /** * @return String URI to resource's logo (no version number) */ @NotNull public String getResourceLogoUrl(@NotNull String shortname) { Preconditions.checkNotNull(getBaseUrl()); return UriBuilder.fromPath(getBaseUrl()).path(Constants.REQ_PATH_LOGO) .queryParam(Constants.REQ_PARAM_RESOURCE, shortname).build().toString(); } /** * @return String URI to resource default homepage (no version number) */ @NotNull public String getResourceUrl(@NotNull String shortname) { return getResourceUri(shortname).toString(); } /** * @return URI to resource default homepage (no version number) used in DOI registration */ @NotNull public URI getResourceUri(@NotNull String shortname) { Preconditions.checkNotNull(getBaseUrl()); return UriBuilder.fromPath(getBaseUrl()).path(Constants.REQ_PATH_RESOURCE) .queryParam(Constants.REQ_PARAM_RESOURCE, shortname).build(); } /** * @return String URI used as resource EML GUID, similar to resource homepage URI but with id param versus r param */ @NotNull public String getResourceGuid(@NotNull String shortname) { Preconditions.checkNotNull(getBaseUrl()); return UriBuilder.fromPath(getBaseUrl()).path(Constants.REQ_PATH_RESOURCE) .queryParam(Constants.REQ_PARAM_ID, shortname).build().toString(); } /** * @return URI to resource homepage for a specific version of resource used in DOI registration */ @NotNull public URI getResourceVersionUri(@NotNull String shortname, @NotNull BigDecimal version) { Preconditions.checkNotNull(getBaseUrl()); return UriBuilder.fromPath(getBaseUrl()).path(Constants.REQ_PATH_RESOURCE) .queryParam(Constants.REQ_PARAM_RESOURCE, shortname) .queryParam(Constants.REQ_PARAM_VERSION, version.toPlainString()).build(); } /** * Called from citations metadata page. * * @return URI to resource homepage for a specific version of resource used in DOI registration */ @NotNull public String getResourceVersionUri(@NotNull String shortname, @NotNull String version) { Preconditions.checkNotNull(getBaseUrl()); return getResourceVersionUri(shortname, new BigDecimal(version)).toString(); } public String getVersion() { return properties.getProperty("dev.version"); } public boolean hasLocation() { if (getLongitude() != null && getLatitude() != null) { return true; } return false; } /** * Checks whether the IPT has been configured to use archival mode. * * @return whether the IPT is used in archival mode */ public boolean isArchivalMode() { return "true".equalsIgnoreCase(properties.getProperty(ARCHIVAL_MODE)); } public boolean isGbifAnalytics() { return "true".equalsIgnoreCase(properties.getProperty(ANALYTICS_GBIF)); } /** * @return true if the datadir is linked to the test registry, false otherwise * */ public boolean isTestInstallation() { return REGISTRY_TYPE.DEVELOPMENT == type; } /** * Load application configuration from application properties file (application.properties) and from * user configuration file (ipt.properties), which includes populating core configuration. */ protected void loadConfig() throws InvalidConfigException { InputStreamUtils streamUtils = new InputStreamUtils(); InputStream configStream = streamUtils.classpathStream(CLASSPATH_PROPFILE); // load default configuration from application.properties try { Properties props = new Properties(); if (configStream == null) { LOG.error("Could not load default configuration from application.properties in classpath"); } else { props.load(configStream); LOG.debug("Loaded default configuration from application.properties in classpath"); } if (dataDir.dataDir != null && dataDir.dataDir.exists()) { // load user configuration properties from data dir ipt.properties (if it exists) File userCfgFile = new File(dataDir.dataDir, "config/" + DATADIR_PROPFILE); if (userCfgFile.exists()) { FileInputStream fis = null; try { fis = new FileInputStream(userCfgFile); props.load(fis); LOG.debug("Loaded user configuration from " + userCfgFile.getAbsolutePath()); } catch (IOException e) { LOG.warn("DataDir configured, but failed to load ipt.properties from " + userCfgFile.getAbsolutePath(), e); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { LOG.debug("Failed to close input stream on ipt.properties file"); } } } } else { LOG.warn("DataDir configured, but user configuration doesnt exist: " + userCfgFile.getAbsolutePath()); } // check if this datadir is a production or test installation // we use a hidden file to indicate the production type readRegistryLock(); } // without error replace existing config with new one this.properties = props; // populates the cores supported populateCoreConfiguration(); } catch (IOException e) { LOG.error("Failed to load the default application configuration from application.properties", e); } } /** * Reads the configuration and populates the cores supported and mapping between core and the ID term to use * for the core. */ private void populateCoreConfiguration() { String cores = properties.getProperty(CORE_ROW_TYPES); String ids = properties.getProperty(CORE_ROW_ID_TERMS); if (cores != null && ids != null) { LOG.info("Using custom core mapping"); List<String> configCores = Lists.newArrayList(Splitter.on('|').trimResults().omitEmptyStrings().split(cores)); List<String> configIDs = Lists.newArrayList(Splitter.on('|').trimResults().omitEmptyStrings().split(ids)); if (configCores.size() == configIDs.size()) { coreRowTypes = Lists.newArrayList(DEFAULT_CORE_ROW_TYPES); coreRowTypes.addAll(configCores); coreRowTypeIdTerms = Maps.newHashMap(DEFAULT_CORE_ROW_TYPES_ID_TERMS); for (int i = 0; i < configCores.size(); i++) { coreRowTypeIdTerms.put(configCores.get(i), configIDs.get(i)); } LOG.info("IPT configured to support cores and id terms: " + coreRowTypeIdTerms); return; } else { LOG.error("Invalid configuration of [" + CORE_ROW_TYPES + "," + CORE_ROW_ID_TERMS + "]. Should have same number of elements - using defaults"); } } coreRowTypes = DEFAULT_CORE_ROW_TYPES; coreRowTypeIdTerms = DEFAULT_CORE_ROW_TYPES_ID_TERMS; } /** * Reads registry lock file and determines what registry the DataDir is locked to. */ private void readRegistryLock() throws InvalidConfigException { File lockFile = getRegistryTypeLockFile(); if (lockFile.exists()) { try { LOG.info("Reading registry lock file to determine if the DataDir is locked to a registry yet."); String regTypeAsString = StringUtils.trimToEmpty(FileUtils.readFileToString(lockFile, "UTF-8")); this.type = REGISTRY_TYPE.valueOf(regTypeAsString); LOG.info("DataDir is locked to registry type: " + type.toString()); } catch (IllegalArgumentException e) { LOG.error("Cannot interpret registry lock file contents!", e); throw new InvalidConfigException(TYPE.INVALID_DATA_DIR, "Cannot interpret registry lock file contents!"); } catch (IOException e) { LOG.error("Cannot read registry lock file!", e); throw new InvalidConfigException(TYPE.INVALID_DATA_DIR, "Cannot read registry lock file!"); } } else { LOG.warn("Registry lock file not found meaning the DataDir is NOT locked to a registry yet!"); } } protected void saveConfig() throws IOException { // save property config file OutputStream out = null; try { File userCfgFile = new File(dataDir.dataDir, "config/" + DATADIR_PROPFILE); // if (userCfgFile.exists()) { // } out = new FileOutputStream(userCfgFile); Properties props = (Properties) properties.clone(); Enumeration<?> e = props.propertyNames(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); if (key.startsWith("dev.")) { props.remove(key); } } props.store(out, "IPT configuration, last saved " + new Date().toString()); out.close(); } finally { if (out != null) { out.close(); } } } public void setProperty(String key, String value) { properties.setProperty(key, StringUtils.trimToEmpty(value)); } protected void setRegistryType(REGISTRY_TYPE newType) throws InvalidConfigException { Preconditions.checkNotNull(newType, "Registry type cannot be null"); if (this.type != null) { if (this.type == newType) { // already contains the same information. Dont do anything return; } else { throw new InvalidConfigException(TYPE.DATADIR_ALREADY_REGISTERED, "The datadir is already designated as " + this.type); } } try { writeRegistryLockFile(newType); this.type = newType; } catch (IOException e) { LOG.error("Cannot lock the datadir to registry type " + newType, e); throw new InvalidConfigException(TYPE.CONFIG_WRITE, "Cannot lock the datadir to registry type " + newType); } } private void writeRegistryLockFile(REGISTRY_TYPE registryType) throws IOException { Closer closer = Closer.create(); try { // set lock file if not yet existing File lockFile = getRegistryTypeLockFile(); Writer lock = closer.register(new FileWriter(lockFile, false)); lock.write(registryType.name()); lock.flush(); LOG.info("Locked DataDir to registry of type " + registryType); } finally { closer.close(); } } }