package edu.harvard.iq.dataverse.util; import com.ocpsoft.pretty.PrettyContext; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.UnknownHostException; import java.time.Year; import java.util.Arrays; import java.util.MissingResourceException; import java.util.Properties; import java.util.ResourceBundle; import java.util.logging.Logger; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.inject.Named; /** * System-wide configuration */ @Stateless @Named public class SystemConfig { private static final Logger logger = Logger.getLogger(SystemConfig.class.getCanonicalName()); @EJB SettingsServiceBean settingsService; /** * A JVM option for the advertised fully qualified domain name (hostname) of * the Dataverse installation, such as "dataverse.example.com", which may * differ from the hostname that the server knows itself as. * * The equivalent in DVN 3.x was "dvn.inetAddress". */ public static final String FQDN = "dataverse.fqdn"; /** * A JVM option for specifying the "official" URL of the site. * Unlike the FQDN option above, this would be a complete URL, * with the protocol, port number etc. */ public static final String SITE_URL = "dataverse.siteUrl"; /** * A JVM option for where files are stored on the file system. */ public static final String FILES_DIRECTORY = "dataverse.files.directory"; /** * A JVM option to override the number of minutes for which a password reset * token is valid ({@link #minutesUntilPasswordResetTokenExpires}). */ private static final String PASSWORD_RESET_TIMEOUT_IN_MINUTES = "dataverse.auth.password-reset-timeout-in-minutes"; /** * A common place to find the String for a sane Solr hostname:port * combination. */ private String saneDefaultForSolrHostColonPort = "localhost:8983"; /** * The default number of datafiles that we allow to be created through * zip file upload. */ private static final int defaultZipUploadFilesLimit = 1000; private static final int defaultMultipleUploadFilesLimit = 1000; private static String appVersionString = null; private static String buildNumberString = null; private static final String JVM_TIMER_SERVER_OPTION = "dataverse.timerServer"; public String getVersion() { return getVersion(false); } public String getVersion(boolean withBuildNumber) { if (appVersionString == null) { // The Version Number is no longer supplied in a .properties file - so // we can't just do // return BundleUtil.getStringFromBundle("version.number", null, ResourceBundle.getBundle("VersionNumber", Locale.US)); // // Instead, we'll rely on Maven placing the version number into the // Manifest, and getting it from there: // (this is considered a better practice, and will also allow us // to maintain this number in only one place - the pom.xml file) // -- L.A. 4.0.2 // One would assume, that once the version is in the MANIFEST.MF, // as Implementation-Version:, it would be possible to obtain // said version simply as // appVersionString = getClass().getPackage().getImplementationVersion(); // alas - that's not working, for whatever reason. (perhaps that's // only how it works with jar-ed packages; not with .war files). // People on the interwebs suggest that one should instead // open the Manifest as a resource, then extract its attributes. // There were some complications with that too. Plus, relying solely // on the MANIFEST.MF would NOT work for those of the developers who // are using "in place deployment" (i.e., where // Netbeans runs their builds directly from the local target // directory, bypassing the war file deployment; and the Manifest // is only available in the .war file). For that reason, I am // going to rely on the pom.properties file, and use java.util.Properties // to read it. We have to look for this file in 2 different places // depending on whether this is a .war file deployment, or a // developers build. (the app-level META-INF is only populated when // a .war file is built; the "maven-archiver" directory, on the other // hand, is only available when it's a local build deployment). // So, long story short, I'm resorting to the convoluted steps below. // It may look hacky, but it should actually be pretty solid and // reliable. // First, find the absolute path url of the application persistence file // always supplied with the Dataverse app: java.net.URL fileUrl = Thread.currentThread().getContextClassLoader().getResource("META-INF/persistence.xml"); String filePath = null; if (fileUrl != null) { filePath = fileUrl.getFile(); if (filePath != null) { InputStream mavenPropertiesInputStream = null; String mavenPropertiesFilePath; Properties mavenProperties = new Properties(); filePath = filePath.replaceFirst("/[^/]*$", "/"); // Using a relative path, find the location of the maven pom.properties file. // First, try to look for it in the app-level META-INF. This will only be // available if it's a war file deployment: mavenPropertiesFilePath = filePath.concat("../../../META-INF/maven/edu.harvard.iq/dataverse/pom.properties"); try { mavenPropertiesInputStream = new FileInputStream(mavenPropertiesFilePath); } catch (IOException ioex) { // OK, let's hope this is a local dev. build. // In that case the properties file should be available in // the maven-archiver directory: mavenPropertiesFilePath = filePath.concat("../../../../maven-archiver/pom.properties"); // try again: try { mavenPropertiesInputStream = new FileInputStream(mavenPropertiesFilePath); } catch (IOException ioex2) { logger.warning("Failed to find and/or open for reading the pom.properties file."); mavenPropertiesInputStream = null; } } if (mavenPropertiesInputStream != null) { try { mavenProperties.load(mavenPropertiesInputStream); appVersionString = mavenProperties.getProperty("version"); } catch (IOException ioex) { logger.warning("caught IOException trying to read and parse the pom properties file."); } } } else { logger.warning("Null file path representation of the location of persistence.xml in the webapp root directory!"); } } else { logger.warning("Could not find the location of persistence.xml in the webapp root directory!"); } if (appVersionString == null) { // still null? - defaulting to 4.0: appVersionString = "4.0"; } } if (withBuildNumber) { if (buildNumberString == null) { // (build number is still in a .properties file in the source tree; it only // contains a real build number if this war file was built by // Jenkins) try { buildNumberString = ResourceBundle.getBundle("BuildNumber").getString("build.number"); } catch (MissingResourceException ex) { buildNumberString = null; } } if (buildNumberString != null && !buildNumberString.equals("")) { return appVersionString + " build " + buildNumberString; } } return appVersionString; } public String getSolrHostColonPort() { String solrHostColonPort = settingsService.getValueForKey(SettingsServiceBean.Key.SolrHostColonPort, saneDefaultForSolrHostColonPort); return solrHostColonPort; } public int getMinutesUntilConfirmEmailTokenExpires() { final int minutesInOneDay = 1440; final int reasonableDefault = minutesInOneDay; SettingsServiceBean.Key key = SettingsServiceBean.Key.MinutesUntilConfirmEmailTokenExpires; String valueFromDatabase = settingsService.getValueForKey(key); if (valueFromDatabase != null) { try { int intFromDatabase = Integer.parseInt(valueFromDatabase); if (intFromDatabase > 0) { return intFromDatabase; } else { logger.info("Returning " + reasonableDefault + " for " + key + " because value must be greater than zero, not \"" + intFromDatabase + "\"."); } } catch (NumberFormatException ex) { logger.info("Returning " + reasonableDefault + " for " + key + " because value must be an integer greater than zero, not \"" + valueFromDatabase + "\"."); } } logger.fine("Returning " + reasonableDefault + " for " + key); return reasonableDefault; } /** * The number of minutes for which a password reset token is valid. Can be * overridden by {@link #PASSWORD_RESET_TIMEOUT_IN_MINUTES}. */ public static int getMinutesUntilPasswordResetTokenExpires() { final int reasonableDefault = 60; String configuredValueAsString = System.getProperty(PASSWORD_RESET_TIMEOUT_IN_MINUTES); if (configuredValueAsString != null) { int configuredValueAsInteger = 0; try { configuredValueAsInteger = Integer.parseInt(configuredValueAsString); if (configuredValueAsInteger > 0) { return configuredValueAsInteger; } else { logger.info(PASSWORD_RESET_TIMEOUT_IN_MINUTES + " is configured as a negative number \"" + configuredValueAsInteger + "\". Using default value instead: " + reasonableDefault); return reasonableDefault; } } catch (NumberFormatException ex) { logger.info("Unable to convert " + PASSWORD_RESET_TIMEOUT_IN_MINUTES + " from \"" + configuredValueAsString + "\" into an integer value: " + ex + ". Using default value " + reasonableDefault); } } return reasonableDefault; } /** * The "official", designated URL of the site; * can be defined as a complete URL; or derived from the * "official" hostname. If none of these options is set, * defaults to the InetAddress.getLocalHOst() and https; * These are legacy JVM options. Will be eventualy replaced * by the Settings Service configuration. */ public String getDataverseSiteUrl() { String hostUrl = System.getProperty(SITE_URL); if (hostUrl != null && !"".equals(hostUrl)) { return hostUrl; } String hostName = System.getProperty(FQDN); if (hostName == null) { try { hostName = InetAddress.getLocalHost().getCanonicalHostName(); } catch (UnknownHostException e) { return null; } } hostUrl = "https://" + hostName; return hostUrl; } /** * URL Tracking: */ public String getPageURLWithQueryString() { return PrettyContext.getCurrentInstance().getRequestURL().toURL() + PrettyContext.getCurrentInstance().getRequestQueryString().toQueryString(); } /** * The "official" server's fully-qualified domain name: */ public String getDataverseServer() { // still reliese on a JVM option: String fqdn = System.getProperty(FQDN); if (fqdn == null) { try { fqdn = InetAddress.getLocalHost().getCanonicalHostName(); } catch (UnknownHostException e) { return null; } } return fqdn; } public String getGuidesBaseUrl() { String saneDefault = "http://guides.dataverse.org"; String guidesBaseUrl = settingsService.getValueForKey(SettingsServiceBean.Key.GuidesBaseUrl, saneDefault); return guidesBaseUrl + "/" + getGuidesLanguage(); } private String getGuidesLanguage() { String saneDefault = "en"; return saneDefault; } /** * Download-as-zip size limit. * returns 0 if not specified; * (the file zipper will then use the default value) * set to -1 to disable zip downloads. */ public long getZipDownloadLimit() { String zipLimitOption = settingsService.getValueForKey(SettingsServiceBean.Key.ZipDownloadLimit); Long zipLimit = null; if (zipLimitOption != null && !zipLimitOption.equals("")) { try { zipLimit = new Long(zipLimitOption); } catch (NumberFormatException nfe) { zipLimit = null; } } if (zipLimit != null) { return zipLimit.longValue(); } return 0L; } public int getZipUploadFilesLimit() { String limitOption = settingsService.getValueForKey(SettingsServiceBean.Key.ZipUploadFilesLimit); Integer limit = null; if (limitOption != null && !limitOption.equals("")) { try { limit = new Integer(limitOption); } catch (NumberFormatException nfe) { limit = null; } } if (limit != null) { return limit; } return defaultZipUploadFilesLimit; } /* ` the number of files the GUI user is allowed to upload in one batch, via drag-and-drop, or through the file select dialog */ public int getMultipleUploadFilesLimit() { String limitOption = settingsService.getValueForKey(SettingsServiceBean.Key.MultipleUploadFilesLimit); Integer limit = null; if (limitOption != null && !limitOption.equals("")) { try { limit = new Integer(limitOption); } catch (NumberFormatException nfe) { limit = null; } } if (limit != null) { return limit; } return defaultMultipleUploadFilesLimit; } // TODO: (?) // create sensible defaults for these things? -- 4.2.2 public long getThumbnailSizeLimitImage() { long limit = getThumbnailSizeLimit("Image"); return limit == 0 ? 5000000 : limit; } public long getThumbnailSizeLimitPDF() { long limit = getThumbnailSizeLimit("PDF"); return limit == 0 ? 500000 : limit; } public long getThumbnailSizeLimit(String type) { String option = null; if ("Image".equals(type)) { option = settingsService.getValueForKey(SettingsServiceBean.Key.ThumbnailSizeLimitImage); option = System.getProperty("dataverse.dataAccess.thumbnail.image.limit"); } else if ("PDF".equals(type)) { option = settingsService.getValueForKey(SettingsServiceBean.Key.ThumbnailSizeLimitPDF); option = System.getProperty("dataverse.dataAccess.thumbnail.pdf.limit"); } Long limit = null; if (option != null && !option.equals("")) { try { limit = new Long(option); } catch (NumberFormatException nfe) { limit = null; } } if (limit != null) { return limit.longValue(); } return 0l; } public boolean isThumbnailGenerationDisabledForType(String type) { return getThumbnailSizeLimit(type) == -1l; } public boolean isThumbnailGenerationDisabledForImages() { return isThumbnailGenerationDisabledForType("Image"); } public boolean isThumbnailGenerationDisabledForPDF() { return isThumbnailGenerationDisabledForType("PDF"); } public String getApplicationTermsOfUse() { String saneDefaultForAppTermsOfUse = "There are no Terms of Use for this Dataverse installation."; String appTermsOfUse = settingsService.getValueForKey(SettingsServiceBean.Key.ApplicationTermsOfUse, saneDefaultForAppTermsOfUse); return appTermsOfUse; } public String getApiTermsOfUse() { String saneDefaultForApiTermsOfUse = "There are no API Terms of Use for this Dataverse installation."; String apiTermsOfUse = settingsService.getValueForKey(SettingsServiceBean.Key.ApiTermsOfUse, saneDefaultForApiTermsOfUse); return apiTermsOfUse; } // TODO: // remove this method! // pages should be using settingsWrapper.get(":ApplicationPrivacyPolicyUrl") instead. -- 4.2.1 public String getApplicationPrivacyPolicyUrl() { String saneDefaultForPrivacyPolicyUrl = null; String appPrivacyPolicyUrl = settingsService.getValueForKey(SettingsServiceBean.Key.ApplicationPrivacyPolicyUrl, saneDefaultForPrivacyPolicyUrl); return appPrivacyPolicyUrl; } public boolean isDdiExportEnabled() { boolean safeDefaultIfKeyNotFound = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.DdiExportEnabled, safeDefaultIfKeyNotFound); } public boolean isShibEnabled() { boolean safeDefaultIfKeyNotFound = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.ShibEnabled, safeDefaultIfKeyNotFound); } public boolean myDataDoesNotUsePermissionDocs() { boolean safeDefaultIfKeyNotFound = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.MyDataDoesNotUseSolrPermissionDocs, safeDefaultIfKeyNotFound); } public boolean isFilesOnDatasetPageFromSolr() { boolean safeDefaultIfKeyNotFound = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.FilesOnDatasetPageFromSolr, safeDefaultIfKeyNotFound); } public boolean isFileLandingPageAvailable() { boolean safeDefaultIfKeyNotFound = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.ShowFileLandingPage, safeDefaultIfKeyNotFound); } public Long getMaxFileUploadSize(){ return settingsService.getValueForKeyAsLong(SettingsServiceBean.Key.MaxFileUploadSizeInBytes); } public Integer getSearchHighlightFragmentSize() { String fragSize = settingsService.getValueForKey(SettingsServiceBean.Key.SearchHighlightFragmentSize); if (fragSize != null) { try { return new Integer(fragSize); } catch (NumberFormatException nfe) { logger.info("Could not convert " + SettingsServiceBean.Key.SearchHighlightFragmentSize + " to int: " + nfe); } } return null; } public long getTabularIngestSizeLimit() { // This method will return the blanket ingestable size limit, if // set on the system. I.e., the universal limit that applies to all // tabular ingests, regardless of fromat: String limitEntry = settingsService.getValueForKey(SettingsServiceBean.Key.TabularIngestSizeLimit); if (limitEntry != null) { try { Long sizeOption = new Long(limitEntry); return sizeOption; } catch (NumberFormatException nfe) { logger.warning("Invalid value for TabularIngestSizeLimit option? - " + limitEntry); } } // -1 means no limit is set; // 0 on the other hand would mean that ingest is fully disabled for // tabular data. return -1; } public long getTabularIngestSizeLimit(String formatName) { // This method returns the size limit set specifically for this format name, // if available, otherwise - the blanket limit that applies to all tabular // ingests regardless of a format. if (formatName == null || formatName.equals("")) { return getTabularIngestSizeLimit(); } String limitEntry = settingsService.get(SettingsServiceBean.Key.TabularIngestSizeLimit.toString() + ":" + formatName); if (limitEntry != null) { try { Long sizeOption = new Long(limitEntry); return sizeOption; } catch (NumberFormatException nfe) { logger.warning("Invalid value for TabularIngestSizeLimit:" + formatName + "? - " + limitEntry ); } } return getTabularIngestSizeLimit(); } public boolean isOAIServerEnabled() { boolean defaultResponse = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.OAIServerEnabled, defaultResponse); } public void enableOAIServer() { settingsService.setValueForKey(SettingsServiceBean.Key.OAIServerEnabled, "true"); } public void disableOAIServer() { settingsService.deleteValueForKey(SettingsServiceBean.Key.OAIServerEnabled); } public boolean isTimerServer() { String optionValue = System.getProperty(JVM_TIMER_SERVER_OPTION); if ("true".equalsIgnoreCase(optionValue)) { return true; } return false; } public String getFooterCopyrightAndYear() { return BundleUtil.getStringFromBundle("footer.copyright", Arrays.asList(Year.now().getValue() + "")); } public DataFile.ChecksumType getFileFixityChecksumAlgorithm() { DataFile.ChecksumType saneDefault = DataFile.ChecksumType.MD5; String checksumStringFromDatabase = settingsService.getValueForKey(SettingsServiceBean.Key.FileFixityChecksumAlgorithm, saneDefault.toString()); try { DataFile.ChecksumType checksumTypeFromDatabase = DataFile.ChecksumType.fromString(checksumStringFromDatabase); return checksumTypeFromDatabase; } catch (IllegalArgumentException ex) { logger.info("The setting " + SettingsServiceBean.Key.FileFixityChecksumAlgorithm + " is misconfigured. " + ex.getMessage() + " Returning sane default: " + saneDefault + "."); return saneDefault; } } }