// Copyright (C) 2012 The Android Open Source Project
//
// 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.googlesource.gerrit.plugins.gitblit.app;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.utils.StringUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.annotations.PluginData;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
import com.google.gerrit.server.ssh.SshListenAddresses;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.gitblit.GitBlitUrlsConfig;
import com.googlesource.gerrit.plugins.gitblit.auth.GerritGitBlitUserManager;
@Singleton
public class GitBlitSettings extends IStoredSettings {
private static final Logger log = LoggerFactory.getLogger(GitBlitSettings.class);
private static final String OVERRIDABLE_DEFAULT_PROPERTIES = "/gitblit-plugin-default.properties";
private static final String GERRIT_GITBLIT_PROPERTIES = "/gitblit.properties";
private static final String GERRIT_GITBLIT_PROPERTY_SOURCE_KEY = "gerrit_gitblit.property_source";
private static final String INCLUDE_KEY = "include"; // Keys.include doesn't exist
private static final String GERRIT_LOGIN_URL = "gerrit.loginUrl";
private static final String GERRIT_CANONICAL_WEB_URL = "gerrit.canonicalWebUrl";
private final File homeDir;
private final Properties properties = new Properties();
@Inject
public GitBlitSettings(@GerritServerConfig Config config,
@SshListenAddresses List<SocketAddress> sshListenAddresses, @SshAdvertisedAddresses List<String> sshAdvertizedAddresses,
SitePaths sitePaths, @PluginData File homeDir) throws IOException {
super(GitBlitSettings.class);
// Give GitBlit its own baseDir, otherwise it'll create subfolders in the git repo directory.
// Note that if you enable Lucene indexing for GitBlit, it will anyway create a subdirectory
// in that directory called "lucene". See com.gitblit.service.LuceneService. But at least we
// can keep GitBlit's plugins and tickets directories out of the way.
this.homeDir = homeDir;
load(properties, sitePaths.etc_dir.toFile(), new GitBlitUrlsConfig(config, sshListenAddresses, sshAdvertizedAddresses),
sitePaths.resolve(config.getString("gerrit", null, "basePath")).toFile());
}
@Override
protected Properties read() {
return properties;
}
@Override
public boolean saveSettings(Map<String, String> updatedSettings) {
properties.putAll(updatedSettings);
return true;
}
/**
* Loads the properties from the given {@link InputStream} using the UTF-8 character encoding. The stream is closed in all cases. A no-op if the
* stream is {@code null}.
*
* @param properties
* to load; must not be {@code null}
* @param stream
* to load from; may be {@code null}
* @throws IOException
* if the stream cannot be read.
*/
protected void loadFromStream(final Properties properties, final InputStream stream) throws IOException {
if (stream != null) {
try (Reader reader = new InputStreamReader(stream, Charsets.UTF_8)) {
properties.load(reader);
}
}
}
/**
* Tests whether the {@code candidate} {@link File} is in the set {@code alreadySeen} and logs a warning if so.
*
* @param candidate
* File to test
* @param alreadySeen
* set of files to tests against
* @return {@code true} if the file is in the set, {@code false} if not.
*/
private boolean alreadyVisited(File candidate, Set<File> alreadySeen) {
if (alreadySeen.contains(candidate)) {
log.warn("Cyclic include of settings file {}: {}", candidate.getPath(), alreadySeen.toString());
return true;
}
return false;
}
/**
* Merge the settings from {@code source} into {@code into}, preserving values already present in {@code into}.
*
* @param source
* {@link Properties} to read entries to merge from
* @param into
* {@link Properties} to merge the settings from {@code source} into
*/
protected void merge(Properties source, Properties into) {
for (Map.Entry<Object, Object> entry : source.entrySet()) {
if (!into.containsKey(entry.getKey())) {
into.put(entry.getKey(), entry.getValue());
}
}
}
/**
* GitBlit 1.7.0 has introduced the notion of including properties files. We have to re-build this functionality here.
*
* @param properties
* to merge the included properties into
* @param parent
* parent file the properties were loaded from
* @param alreadySeen
* set of files already visited in the current include chain; used to detect cycles and avoid endless recursion and stack overflow.
* Must contain {@code parent}.
*/
protected void loadIncludedSettings(final Properties properties, final File parent, final Set<File> alreadySeen) {
Object includes = properties.remove(INCLUDE_KEY);
if (!(includes instanceof String)) {
return;
}
// It's a comma-separated list of possibly double-quoted strings
for (String f : StringUtils.getStringsFromValue((String) includes, ",")) {
String fileName = f.trim();
if (fileName.isEmpty()) {
continue;
}
try {
File file = new File(fileName);
if (file.isAbsolute()) {
file = file.getCanonicalFile();
} else {
// Try different possibilities: relative to parent, if not there, try relative to base dir.
file = new File(parent.getParentFile(), fileName).getCanonicalFile();
if (!file.exists()) {
file = new File(getBasePath(), fileName).getCanonicalFile();
}
}
if (alreadyVisited(file, alreadySeen)) {
continue;
}
alreadySeen.add(file);
try (InputStream stream = new FileInputStream(file)) {
Properties nested = new Properties();
loadFromStream(nested, stream);
loadIncludedSettings(nested, file, alreadySeen);
merge(nested, properties);
} finally {
alreadySeen.remove(file);
}
} catch (IOException ex) {
log.warn("Cannot load included settings '{}' from {}: {}",
new Object[] { fileName, alreadySeen.toString(), ex.getLocalizedMessage() });
}
}
}
/**
* Loads the properties from the user-supplied gitblit.properties in $GERRIT_SITE/etc, if it exists, and then overwrites with the built-in
* properties to ensure that GitBlit is set up as a viewer only and uses Gerrit's git repositories.
*
* @param properties
* to load; must not be {@code null}
* @param directory
* to look in for the user-supplied file; must not be {@code null}
* @param config
* to read Gerrit's URL config from; must not be {@code null}
* @param gerritGitDirectory
* Gerrit's git repository directory; must not be {@code null}
* @throws IOException
* if the properties cannot be loaded.
*/
protected void load(final Properties properties, final File directory, GitBlitUrlsConfig config, File gerritGitDirectory) throws IOException {
// First, try to load from the user-supplied file, if any.
File userFile = new File(directory, GERRIT_GITBLIT_PROPERTIES);
try (InputStream stream = new FileInputStream(userFile)) {
loadFromStream(properties, stream);
loadIncludedSettings(properties, userFile, Sets.newLinkedHashSet(Collections.singleton(userFile)));
// Record the fact that we loaded the user settings in the properties themselves so that toString() can show it. Useful for debugging.
properties.put(GERRIT_GITBLIT_PROPERTY_SOURCE_KEY, userFile.getAbsolutePath());
} catch (IOException ex) {
// Silently ignore if the file doesn't exist
if (!(ex instanceof FileNotFoundException)) {
// It was something else
log.warn("Cannot load settings file {}: {}", userFile.getPath(), ex.getLocalizedMessage());
}
}
// Read our default properties and merge them in, if they're not set yet. Note: although we
// should always find them, we keep mum if we don't.
Properties defaults = new Properties();
loadFromStream(defaults, getClass().getResourceAsStream(OVERRIDABLE_DEFAULT_PROPERTIES));
merge(defaults, properties);
// Remember this key, since we allow overriding the built-in configuration for this one.
// Left in our otherwise non-overridable properties because I want users who access the
// built-in configuration to see this one.
final Object authenticationRequired = properties.get(Keys.web.authenticateViewPages);
// Override with the built-in viewer-only configuration
InputStream stream = getClass().getResourceAsStream(GERRIT_GITBLIT_PROPERTIES);
if (stream == null) {
throw new IllegalStateException("Built-in configuration " + GERRIT_GITBLIT_PROPERTIES + " cannot be found");
}
loadFromStream(properties, stream);
if (authenticationRequired != null) {
properties.put(Keys.web.authenticateViewPages, authenticationRequired);
}
// Finally some built-in defaults that depend on the Gerrit configuration
properties.put(Keys.git.repositoriesFolder, gerritGitDirectory.getAbsolutePath());
properties.put(Keys.realm.userService, GerritGitBlitUserManager.class.getName());
String gerritDefaultUrls = (config.getGitHttpUrl() + ' ' + config.getGitSshUrl()).trim();
if (properties.get(Keys.web.otherUrls) != null) {
properties.put(Keys.web.otherUrls, gerritDefaultUrls + ' ' + properties.get(Keys.web.otherUrls));
} else {
properties.put(Keys.web.otherUrls, gerritDefaultUrls);
}
String loginUrl = config.getLoginUrl();
if (!Strings.isNullOrEmpty(loginUrl)) {
properties.put(GERRIT_LOGIN_URL, loginUrl);
} else {
properties.remove(GERRIT_LOGIN_URL);
}
String canonicalWebUrl = config.getCanonicalWebUrl();
if (!Strings.isNullOrEmpty(canonicalWebUrl)) {
properties.put(GERRIT_CANONICAL_WEB_URL, canonicalWebUrl);
}
}
public File getBasePath() {
return homeDir;
}
@Override
public String toString() {
return properties.toString();
}
@Override
public boolean saveSettings() {
// We might even consider updating the user-supplied file, if any...
return false;
}
}