Paul Martin
2016-04-03 cd7e4f9186f2ace4416780a7dd6341e01e23a45f
src/main/java/com/gitblit/FileSettings.java
@@ -18,31 +18,57 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.StringUtils;
/**
 * Dynamically loads and reloads a properties file by keeping track of the last
 * modification date.
 *
 *
 * @author James Moger
 *
 *
 */
public class FileSettings extends IStoredSettings {
   protected final File propertiesFile;
   protected File propertiesFile;
   private final Properties properties = new Properties();
   private volatile long lastModified;
   private volatile boolean forceReload;
   public FileSettings(String file) {
   public FileSettings() {
      super(FileSettings.class);
   }
   public FileSettings(String file) {
      this();
      load(file);
   }
   public void load(String file) {
      this.propertiesFile = new File(file);
   }
   /**
    * Merges the provided settings into this instance.  This will also
    * set the target file for this instance IFF it is unset AND the merge
    * source is also a FileSettings.  This is a little sneaky.
    */
   @Override
   public void merge(IStoredSettings settings) {
      super.merge(settings);
      // sneaky: set the target file from the merge source
      if (propertiesFile == null && settings instanceof FileSettings) {
         this.propertiesFile = ((FileSettings) settings).propertiesFile;
      }
   }
   /**
@@ -51,12 +77,16 @@
    */
   @Override
   protected synchronized Properties read() {
      if (propertiesFile.exists() && (forceReload || (propertiesFile.lastModified() > lastModified))) {
      if (propertiesFile != null && propertiesFile.exists() && (forceReload || (propertiesFile.lastModified() > lastModified))) {
         FileInputStream is = null;
         try {
            logger.debug("loading {}", propertiesFile);
            Properties props = new Properties();
            is = new FileInputStream(propertiesFile);
            props.load(is);
            // ticket-110
            props = readIncludes(props);
            // load properties after we have successfully read file
            properties.clear();
@@ -81,13 +111,87 @@
   }
   /**
    * Recursively read "include" properties files.
    *
    * @param properties
    * @return
    * @throws IOException
    */
   private Properties readIncludes(Properties properties) throws IOException {
      Properties baseProperties = new Properties();
      String include = (String) properties.remove("include");
      if (!StringUtils.isEmpty(include)) {
         // allow for multiples
         List<String> names = StringUtils.getStringsFromValue(include, ",");
         for (String name : names) {
            if (StringUtils.isEmpty(name)) {
               continue;
            }
            // try co-located
            File file = new File(propertiesFile.getParentFile(), name.trim());
            if (!file.exists()) {
               // try absolute path
               file = new File(name.trim());
            }
            if (!file.exists()) {
               logger.warn("failed to locate {}", file);
               continue;
            }
            // load properties
            logger.debug("loading {}", file);
            try (FileInputStream iis = new FileInputStream(file)) {
               baseProperties.load(iis);
            }
            // read nested includes
            baseProperties = readIncludes(baseProperties);
         }
      }
      // includes are "default" properties, they must be set first and the
      // props which specified the "includes" must override
      Properties merged = new Properties();
      merged.putAll(baseProperties);
      merged.putAll(properties);
      return merged;
   }
   @Override
   public boolean saveSettings() {
      String content = FileUtils.readContent(propertiesFile, "\n");
      for (String key : removals) {
         String regex = "(?m)^(" + regExEscape(key) + "\\s*+=\\s*+)"
               + "(?:[^\r\n\\\\]++|\\\\(?:\r?\n|\r|.))*+$";
         content = content.replaceAll(regex, "");
      }
      removals.clear();
      FileUtils.writeContent(propertiesFile, content);
      // manually set the forceReload flag because not all JVMs support real
      // millisecond resolution of lastModified. (issue-55)
      forceReload = true;
      return true;
   }
   /**
    * Updates the specified settings in the settings file.
    */
   @Override
   public synchronized boolean saveSettings(Map<String, String> settings) {
      String content = FileUtils.readContent(propertiesFile, "\n");
      for (Map.Entry<String, String> setting:settings.entrySet()) {
         String regex = "(?m)^(" + regExEscape(setting.getKey()) + "\\s*+=\\s*+)"
                + "(?:[^\r\n\\\\]++|\\\\(?:\r?\n|\r|.))*+$";
               + "(?:[^\r\n\\\\]++|\\\\(?:\r?\n|\r|.))*+$";
         String oldContent = content;
         content = content.replaceAll(regex, setting.getKey() + " = " + setting.getValue());
         if (content.equals(oldContent)) {
@@ -98,11 +202,11 @@
      }
      FileUtils.writeContent(propertiesFile, content);
      // manually set the forceReload flag because not all JVMs support real
      // millisecond resolution of lastModified. (issue-55)
      // millisecond resolution of lastModified. (issue-55)
      forceReload = true;
      return true;
   }
   private String regExEscape(String input) {
      return input.replace(".", "\\.").replace("$", "\\$").replace("{", "\\{");
   }