// Copyright 2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.enterprise.connector.common; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.enterprise.connector.instantiator.EncryptedPropertyPlaceholderConfigurer; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Logger; public class PropertiesUtils { private static final Logger LOGGER = Logger.getLogger(PropertiesUtils.class.getName()); // Non-persistable special google properties. public static final String GOOGLE_CONNECTOR_WORK_DIR = "googleConnectorWorkDir"; public static final String GOOGLE_WORK_DIR = "googleWorkDir"; public static final String GOOGLE_FEED_HOST = "googleFeedHost"; public static final String GOOGLE_CONNECTOR_NAME = "googleConnectorName"; public static final Set<String> GOOGLE_NONPERSISTABLE_PROPERTIES = ImmutableSet.of(GOOGLE_CONNECTOR_WORK_DIR, GOOGLE_WORK_DIR, GOOGLE_FEED_HOST, GOOGLE_CONNECTOR_NAME); // Persistable special google properties. public static final String GOOGLE_GLOBAL_NAMESPACE = "googleGlobalNamespace"; public static final String GOOGLE_LOCAL_NAMESPACE = "googleLocalNamespace"; public static final String GOOGLE_PROPERTIES_VERSION = "googlePropertiesVersion"; public static final int GOOGLE_PROPERTIES_VERSION_NUMBER = 3; // Non-XML format Properties files are by definition 8859-1 encoding. public static final String PROPERTIES_ENCODING = "ISO-8859-1"; private PropertiesUtils() { // prevents instantiation } /** * Read Properties from a file. Decrypt passwords. * * @param propertiesFile Properties File to read * @return Properties as read from file * @throws PropertiesException if error reading file */ public static Properties loadFromFile(File propertiesFile) throws PropertiesException { try { InputStream is = new BufferedInputStream(new FileInputStream(propertiesFile)); try { return loadProperties(is); } finally { is.close(); } } catch (Exception e) { throw new PropertiesException("Unable to load Properties from file " + propertiesFile.getPath(), e); } } /** * Write the properties to a file. Encrypt passwords, * version the properties. * * @param properties Properties to write * @param propertiesFile File to write properties to * @param comment optional comment String to pass to Properties.store() * @throws PropertiesException if error writing to file */ public static void storeToFile(Properties properties, File propertiesFile, String comment) throws PropertiesException { try { FileOutputStream fos = new FileOutputStream(propertiesFile); try { storeProperties(properties, fos, comment); } finally { fos.close(); } } catch (Exception e) { throw new PropertiesException("Unable to store Properties to file " + propertiesFile.getPath(), e); } } /** * Store a set of Properties to a String. This is effectively * java.util.Properties.store(StringOutputStream), if there were * such a thing as StringOutputStream. The returned string is * suitable for loading back into as set of Properties using * fromString(String). * * @param properties to encode into a String * @param comment optional comment string to pass to Properties.store() * @return a String object with containing the properties. * @throws PropertiesException */ public static String storeToString(Properties properties, String comment) throws PropertiesException { try { ByteArrayOutputStream os = null; try { os = new ByteArrayOutputStream(); storeProperties(properties, os, comment); return os.toString(PROPERTIES_ENCODING); } finally { os.close(); } } catch (IOException e) { throw new PropertiesException("Unable to encode Properties to String", e); } } /** * Load a set of Properties from a String. This is effectively * java.util.Properties.load(StringInputStream), if there were * such a thing as StringInputStream. This should be able to * load properties from strings created by toString(); * * @param propertiesString * @return a Properties object, or null if null string * @throws PropertiesException */ public static Properties loadFromString(String propertiesString) throws PropertiesException { if (propertiesString != null) { try { ByteArrayInputStream is = null; try { is = new ByteArrayInputStream( propertiesString.getBytes(PROPERTIES_ENCODING)); return loadProperties(is); } finally { is.close(); } } catch (IOException e) { throw new PropertiesException("Unable to decode Properties from String", e); } } return null; } /** * Read Properties from an InputStream. Decrypt passwords. * * @param inputStream InputStream to read Properties from * @return Properties as read from inputStream * @throws PropertiesException */ @VisibleForTesting static Properties loadProperties(InputStream inputStream) throws PropertiesException { if (inputStream == null) { return null; } Properties properties = new Properties(); try { properties.load(inputStream); } catch (Exception e) { throw new PropertiesException("Error loading properties from stream", e); } // Decrypt stored passwords. decryptSensitiveProperties(properties); return properties; } /** * Write the properties to an OutputStream. Encrypt passwords, * version the properties. * * @param properties Properties to write * @param outputStream OutputStream to write properties to * @param comment optional comment String * @throws PropertiesException if error writing to stream */ @VisibleForTesting static void storeProperties(Properties properties, OutputStream outputStream, String comment) throws PropertiesException { if (properties == null) { return; } try { // Make a copy of the Properties before munging them. Properties props = copy(properties); stampPropertiesVersion(props); encryptSensitiveProperties(props); // If the comment contains embedded newlines, we must comment out each // subsequent line after the first, as Java Properties won't do it for us. if (comment != null && comment.indexOf('\n') > 0) { comment = comment.replaceAll("\n", "\n#"); } props.store(outputStream, comment); } catch (Exception e) { throw new PropertiesException("Error storing properties to stream", e); } } /** * Make a Properties object from a Map, copying all the keys and values. * * @param sourceMap a Map representing properties key-value map * @return new Properties object that may be modified without altering * the source properties. */ public static Properties fromMap(Map<String, String> sourceMap) { if (sourceMap == null) { return null; } Properties properties = new Properties(); properties.putAll(sourceMap); return properties; } /** * Make a Map<String, String> from the supplied Properties, * copying all the keys and values. * * @param sourceProperties Properties representing properties key-value map. * @return a Map<String, String> representation of the source * Properties. */ public static Map<String, String> toMap(Properties sourceProperties) { if (sourceProperties == null) { return null; } Map<String, String> configMap = new HashMap<String, String>(); Iterator<?> iter = sourceProperties.keySet().iterator(); while (iter.hasNext()) { String key = (String) iter.next(); configMap.put(key, sourceProperties.getProperty(key)); } return configMap; } /** * Make a deep copy of a Properties object - copying all * the keys and values. This is in contrast to java.util.Propeties.copy(), * which makes a shallow copy. * * @param sourceProperties a source set of Properties. * @return new Properties object that may be modified without altering * the source properties. */ public static Properties copy(Properties sourceProperties) { Properties props = new Properties(); if (sourceProperties != null) { props.putAll(sourceProperties); } return props; } /** * Encrypt Properties values that may be sensitive. At this point, * any property that has the case-insensitive substring 'password' * in the key is considered sensitive. Encrypting sensitive properties * is advisable when storing or transmitting properties in plain text. * * @param properties a set of Properties. */ public static void encryptSensitiveProperties(Properties properties) { EncryptedPropertyPlaceholderConfigurer.encryptSensitiveProperties(properties); } /** * Decrypt Properties values that may be sensitive. At this point, * any property that has the case-insensitive substring 'password' * in the key is considered sensitive. This decrypts a set of * properties that was encrypted via encryptSensitiveProperties(); * * @param properties a set of Properties. */ public static void decryptSensitiveProperties(Properties properties) { EncryptedPropertyPlaceholderConfigurer.decryptSensitiveProperties(properties); } /** * Stamp the Properties set with the current Properties Version. * * @param properties a set of Properties. */ public static void stampPropertiesVersion(Properties properties) { properties.put(GOOGLE_PROPERTIES_VERSION, Integer.toString(GOOGLE_PROPERTIES_VERSION_NUMBER)); } /** * Retrieve the Properties Version stamp from this Properties set. * * @param properties a set of Properties. */ public static int getPropertiesVersion(Properties properties) { String versionStr = properties.getProperty( GOOGLE_PROPERTIES_VERSION, "0"); int version = 0; try { version = Integer.parseInt(versionStr); if (version > GOOGLE_PROPERTIES_VERSION_NUMBER) { LOGGER.warning("Properties appear to have been written by a newer " + "version of Connector Manager (" + version + ")"); } } catch (NumberFormatException e) { LOGGER.warning("Invalid Properties Version: " + versionStr); } return version; } }