| /* | |
| * Copyright 2011 gitblit.com. | |
| * | |
| * 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.gitblit; | |
| 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 File propertiesFile; | |
| private final Properties properties = new Properties(); | |
| private volatile long lastModified; | |
| private volatile boolean forceReload; | |
| 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; | |
| } | |
| } | |
| /** | |
| * Returns a properties object which contains the most recent contents of | |
| * the properties file. | |
| */ | |
| @Override | |
| protected synchronized Properties read() { | |
| 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(); | |
| properties.putAll(props); | |
| lastModified = propertiesFile.lastModified(); | |
| forceReload = false; | |
| } catch (FileNotFoundException f) { | |
| // IGNORE - won't happen because file.exists() check above | |
| } catch (Throwable t) { | |
| logger.error("Failed to read " + propertiesFile.getName(), t); | |
| } finally { | |
| if (is != null) { | |
| try { | |
| is.close(); | |
| } catch (Throwable t) { | |
| // IGNORE | |
| } | |
| } | |
| } | |
| } | |
| return properties; | |
| } | |
| /** | |
| * 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|.))*+$"; | |
| String oldContent = content; | |
| content = content.replaceAll(regex, setting.getKey() + " = " + setting.getValue()); | |
| if (content.equals(oldContent)) { | |
| // did not replace value because it does not exist in the file | |
| // append new setting to content (issue-85) | |
| content += "\n" + setting.getKey() + " = " + setting.getValue(); | |
| } | |
| } | |
| 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; | |
| } | |
| private String regExEscape(String input) { | |
| return input.replace(".", "\\.").replace("$", "\\$").replace("{", "\\{"); | |
| } | |
| /** | |
| * @return the last modification date of the properties file | |
| */ | |
| protected long lastModified() { | |
| return lastModified; | |
| } | |
| /** | |
| * @return the state of the force reload flag | |
| */ | |
| protected boolean forceReload() { | |
| return forceReload; | |
| } | |
| @Override | |
| public String toString() { | |
| return propertiesFile.getAbsolutePath(); | |
| } | |
| } |