James Moger
2011-11-01 c25a1d65ed2c94b65741d81862a7612ae12bdf76
src/com/gitblit/GitBlit.java
@@ -15,16 +15,30 @@
 */
package com.gitblit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.Cookie;
@@ -44,9 +58,24 @@
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.FederationToken;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.FederationSet;
import com.gitblit.models.Metric;
import com.gitblit.models.ObjectCache;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.ServerStatus;
import com.gitblit.models.SettingModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.MetricUtils;
import com.gitblit.utils.StringUtils;
/**
@@ -71,7 +100,20 @@
   private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
   private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
   private final List<FederationModel> federationRegistrations = Collections
         .synchronizedList(new ArrayList<FederationModel>());
   private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
   private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
   private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
   private RepositoryResolver<Void> repositoryResolver;
   private ServletContext servletContext;
   private File repositoriesFolder;
@@ -80,6 +122,12 @@
   private IUserService userService;
   private IStoredSettings settings;
   private ServerSettings settingsModel;
   private ServerStatus serverStatus;
   private MailExecutor mailExecutor;
   public GitBlit() {
      if (gitblit == null) {
@@ -98,6 +146,15 @@
         new GitBlit();
      }
      return gitblit;
   }
   /**
    * Determine if this is the GO variant of Gitblit.
    *
    * @return true if this is the GO variant of Gitblit.
    */
   public static boolean isGO() {
      return self().settings instanceof FileSettings;
   }
   /**
@@ -141,7 +198,7 @@
   public static char getChar(String key, char defaultValue) {
      return self().settings.getChar(key, defaultValue);
   }
   /**
    * Returns the string value for the specified key. If the key does not exist
    * or the value for the key can not be interpreted as a string, the
@@ -190,6 +247,23 @@
   }
   /**
    * Updates the list of server settings.
    *
    * @param settings
    * @return true if the update succeeded
    */
   public boolean updateSettings(Map<String, String> updatedSettings) {
      return settings.saveSettings(updatedSettings);
   }
   public ServerStatus getStatus() {
      // update heap memory status
      serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
      serverStatus.heapFree = Runtime.getRuntime().freeMemory();
      return serverStatus;
   }
   /**
    * Returns the list of non-Gitblit clone urls. This allows Gitblit to
    * advertise alternative urls for Git client repository access.
    * 
@@ -213,6 +287,7 @@
   public void setUserService(IUserService userService) {
      logger.info("Setting up user service " + userService.toString());
      this.userService = userService;
      this.userService.setup(settings);
   }
   /**
@@ -224,6 +299,30 @@
    * @return a user object or null
    */
   public UserModel authenticate(String username, char[] password) {
      if (StringUtils.isEmpty(username)) {
         // can not authenticate empty username
         return null;
      }
      String pw = new String(password);
      if (StringUtils.isEmpty(pw)) {
         // can not authenticate empty password
         return null;
      }
      // check to see if this is the federation user
      if (canFederate()) {
         if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
            List<String> tokens = getFederationTokens();
            if (tokens.contains(pw)) {
               // the federation user is an administrator
               UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
               federationUser.canAdmin = true;
               return federationUser;
            }
         }
      }
      // delegate authentication to the user service
      if (userService == null) {
         return null;
      }
@@ -351,9 +450,26 @@
    */
   public void updateUserModel(String username, UserModel user, boolean isCreate)
         throws GitBlitException {
      if (!username.equalsIgnoreCase(user.username)) {
         if (userService.getUserModel(user.username) != null) {
            throw new GitBlitException(MessageFormat.format(
                  "Failed to rename ''{0}'' because ''{1}'' already exists.", username,
                  user.username));
         }
      }
      if (!userService.updateUserModel(username, user)) {
         throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
      }
   }
   /**
    * Clears all the cached data for the specified repository.
    *
    * @param repositoryName
    */
   public void clearRepositoryCache(String repositoryName) {
      repositorySizeCache.remove(repositoryName);
      repositoryMetricsCache.remove(repositoryName);
   }
   /**
@@ -408,6 +524,20 @@
            repositories.add(model);
         }
      }
      if (getBoolean(Keys.web.showRepositorySizes, true)) {
         int repoCount = 0;
         long startTime = System.currentTimeMillis();
         ByteFormat byteFormat = new ByteFormat();
         for (RepositoryModel model : repositories) {
            if (!model.skipSizeCalculation) {
               repoCount++;
               model.size = byteFormat.format(calculateSize(model));
            }
         }
         long duration = System.currentTimeMillis() - startTime;
         logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
               repoCount, duration));
      }
      return repositories;
   }
@@ -461,24 +591,94 @@
         model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
         model.isFrozen = getConfig(config, "isFrozen", false);
         model.showReadme = getConfig(config, "showReadme", false);
         model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
         model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
         model.federationStrategy = FederationStrategy.fromName(getConfig(config,
               "federationStrategy", null));
         model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
               "gitblit", null, "federationSets")));
         model.isFederated = getConfig(config, "isFederated", false);
         model.origin = config.getString("remote", "origin", "url");
      }
      r.close();
      return model;
   }
   /**
    * Returns the size in bytes of the repository.
    * Returns the size in bytes of the repository. Gitblit caches the
    * repository sizes to reduce the performance penalty of recursive
    * calculation. The cache is updated if the repository has been changed
    * since the last calculation.
    * 
    * @param model
    * @return size in bytes
    */
   public long calculateSize(RepositoryModel model) {
      if (repositorySizeCache.hasCurrent(model.name, model.lastChange)) {
         return repositorySizeCache.getObject(model.name);
      }
      File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
      return com.gitblit.utils.FileUtils.folderSize(gitDir);
      long size = com.gitblit.utils.FileUtils.folderSize(gitDir);
      repositorySizeCache.updateObject(model.name, model.lastChange, size);
      return size;
   }
   /**
    * Returns the gitblit string vlaue for the specified key. If key is not
    * Ensure that a cached repository is completely closed and its resources
    * are properly released.
    *
    * @param repositoryName
    */
   private void closeRepository(String repositoryName) {
      Repository repository = getRepository(repositoryName);
      // assume 2 uses in case reflection fails
      int uses = 2;
      try {
         // The FileResolver caches repositories which is very useful
         // for performance until you want to delete a repository.
         // I have to use reflection to call close() the correct
         // number of times to ensure that the object and ref databases
         // are properly closed before I can delete the repository from
         // the filesystem.
         Field useCnt = Repository.class.getDeclaredField("useCnt");
         useCnt.setAccessible(true);
         uses = ((AtomicInteger) useCnt.get(repository)).get();
      } catch (Exception e) {
         logger.warn(MessageFormat
               .format("Failed to reflectively determine use count for repository {0}",
                     repositoryName), e);
      }
      if (uses > 0) {
         logger.info(MessageFormat
               .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
                     repositoryName, uses, uses));
         for (int i = 0; i < uses; i++) {
            repository.close();
         }
      }
   }
   /**
    * Returns the metrics for the default branch of the specified repository.
    * This method builds a metrics cache. The cache is updated if the
    * repository is updated. A new copy of the metrics list is returned on each
    * call so that modifications to the list are non-destructive.
    *
    * @param model
    * @param repository
    * @return a new array list of metrics
    */
   public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
      if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
         return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
      }
      List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null);
      repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
      return new ArrayList<Metric>(metrics);
   }
   /**
    * Returns the gitblit string value for the specified key. If key is not
    * set, returns defaultValue.
    * 
    * @param config
@@ -540,6 +740,16 @@
      } else {
         // rename repository
         if (!repositoryName.equalsIgnoreCase(repository.name)) {
            if (!repository.name.toLowerCase().endsWith(
                  org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
               repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
            }
            if (new File(repositoriesFolder, repository.name).exists()) {
               throw new GitBlitException(MessageFormat.format(
                     "Failed to rename ''{0}'' because ''{1}'' already exists.",
                     repositoryName, repository.name));
            }
            closeRepository(repositoryName);
            File folder = new File(repositoriesFolder, repositoryName);
            File destFolder = new File(repositoriesFolder, repository.name);
            if (destFolder.exists()) {
@@ -559,6 +769,9 @@
                     "Failed to rename repository permissions ''{0}'' to ''{1}''.",
                     repositoryName, repository.name));
            }
            // clear the cache
            clearRepositoryCache(repositoryName);
         }
         // load repository
@@ -576,22 +789,39 @@
      // update settings
      if (r != null) {
         StoredConfig config = JGitUtils.readConfig(r);
         config.setString("gitblit", null, "description", repository.description);
         config.setString("gitblit", null, "owner", repository.owner);
         config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
         config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
         config.setString("gitblit", null, "accessRestriction",
               repository.accessRestriction.name());
         config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
         config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
         config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
         try {
            config.save();
         } catch (IOException e) {
            logger.error("Failed to save repository config!", e);
         }
         updateConfiguration(r, repository);
         r.close();
      }
   }
   /**
    * Updates the Gitblit configuration for the specified repository.
    *
    * @param r
    *            the Git repository
    * @param repository
    *            the Gitblit repository model
    */
   public void updateConfiguration(Repository r, RepositoryModel repository) {
      StoredConfig config = JGitUtils.readConfig(r);
      config.setString("gitblit", null, "description", repository.description);
      config.setString("gitblit", null, "owner", repository.owner);
      config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
      config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
      config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name());
      config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
      config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
      config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
      config.setBoolean("gitblit", null, "skipSizeCalculation", repository.skipSizeCalculation);
      config.setBoolean("gitblit", null, "skipSummaryMetrics", repository.skipSummaryMetrics);
      config.setStringList("gitblit", null, "federationSets", repository.federationSets);
      config.setString("gitblit", null, "federationStrategy",
            repository.federationStrategy.name());
      config.setBoolean("gitblit", null, "isFederated", repository.isFederated);
      try {
         config.save();
      } catch (IOException e) {
         logger.error("Failed to save repository config!", e);
      }
   }
@@ -615,6 +845,7 @@
    */
   public boolean deleteRepository(String repositoryName) {
      try {
         closeRepository(repositoryName);
         File folder = new File(repositoriesFolder, repositoryName);
         if (folder.exists() && folder.isDirectory()) {
            FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
@@ -622,6 +853,9 @@
               return true;
            }
         }
         // clear the repository cache
         clearRepositoryCache(repositoryName);
      } catch (Throwable t) {
         logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
      }
@@ -672,18 +906,495 @@
   }
   /**
    * Returns Gitblit's scheduled executor service for scheduling tasks.
    *
    * @return scheduledExecutor
    */
   public ScheduledExecutorService executor() {
      return scheduledExecutor;
   }
   public static boolean canFederate() {
      String passphrase = getString(Keys.federation.passphrase, "");
      return !StringUtils.isEmpty(passphrase);
   }
   /**
    * Configures this Gitblit instance to pull any registered federated gitblit
    * instances.
    */
   private void configureFederation() {
      boolean validPassphrase = true;
      String passphrase = settings.getString(Keys.federation.passphrase, "");
      if (StringUtils.isEmpty(passphrase)) {
         logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
         validPassphrase = false;
      }
      if (validPassphrase) {
         // standard tokens
         for (FederationToken tokenType : FederationToken.values()) {
            logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
                  getFederationToken(tokenType)));
         }
         // federation set tokens
         for (String set : settings.getStrings(Keys.federation.sets)) {
            logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
                  getFederationToken(set)));
         }
      }
      // Schedule the federation executor
      List<FederationModel> registrations = getFederationRegistrations();
      if (registrations.size() > 0) {
         FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
         scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
      }
   }
   /**
    * Returns the list of federated gitblit instances that this instance will
    * try to pull.
    *
    * @return list of registered gitblit instances
    */
   public List<FederationModel> getFederationRegistrations() {
      if (federationRegistrations.isEmpty()) {
         federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
      }
      return federationRegistrations;
   }
   /**
    * Retrieve the specified federation registration.
    *
    * @param name
    *            the name of the registration
    * @return a federation registration
    */
   public FederationModel getFederationRegistration(String url, String name) {
      // check registrations
      for (FederationModel r : getFederationRegistrations()) {
         if (r.name.equals(name) && r.url.equals(url)) {
            return r;
         }
      }
      // check the results
      for (FederationModel r : getFederationResultRegistrations()) {
         if (r.name.equals(name) && r.url.equals(url)) {
            return r;
         }
      }
      return null;
   }
   /**
    * Returns the list of federation sets.
    *
    * @return list of federation sets
    */
   public List<FederationSet> getFederationSets(String gitblitUrl) {
      List<FederationSet> list = new ArrayList<FederationSet>();
      // generate standard tokens
      for (FederationToken type : FederationToken.values()) {
         FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
         fset.repositories = getRepositories(gitblitUrl, fset.token);
         list.add(fset);
      }
      // generate tokens for federation sets
      for (String set : settings.getStrings(Keys.federation.sets)) {
         FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
               getFederationToken(set));
         fset.repositories = getRepositories(gitblitUrl, fset.token);
         list.add(fset);
      }
      return list;
   }
   /**
    * Returns the list of possible federation tokens for this Gitblit instance.
    *
    * @return list of federation tokens
    */
   public List<String> getFederationTokens() {
      List<String> tokens = new ArrayList<String>();
      // generate standard tokens
      for (FederationToken type : FederationToken.values()) {
         tokens.add(getFederationToken(type));
      }
      // generate tokens for federation sets
      for (String set : settings.getStrings(Keys.federation.sets)) {
         tokens.add(getFederationToken(set));
      }
      return tokens;
   }
   /**
    * Returns the specified federation token for this Gitblit instance.
    *
    * @param type
    * @return a federation token
    */
   public String getFederationToken(FederationToken type) {
      return getFederationToken(type.name());
   }
   /**
    * Returns the specified federation token for this Gitblit instance.
    *
    * @param value
    * @return a federation token
    */
   public String getFederationToken(String value) {
      String passphrase = settings.getString(Keys.federation.passphrase, "");
      return StringUtils.getSHA1(passphrase + "-" + value);
   }
   /**
    * Compares the provided token with this Gitblit instance's tokens and
    * determines if the requested permission may be granted to the token.
    *
    * @param req
    * @param token
    * @return true if the request can be executed
    */
   public boolean validateFederationRequest(FederationRequest req, String token) {
      String all = getFederationToken(FederationToken.ALL);
      String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
      String jur = getFederationToken(FederationToken.REPOSITORIES);
      switch (req) {
      case PULL_REPOSITORIES:
         return token.equals(all) || token.equals(unr) || token.equals(jur);
      case PULL_USERS:
         return token.equals(all) || token.equals(unr);
      case PULL_SETTINGS:
         return token.equals(all);
      }
      return false;
   }
   /**
    * Acknowledge and cache the status of a remote Gitblit instance.
    *
    * @param identification
    *            the identification of the pulling Gitblit instance
    * @param registration
    *            the registration from the pulling Gitblit instance
    * @return true if acknowledged
    */
   public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
      // reset the url to the identification of the pulling Gitblit instance
      registration.url = identification;
      String id = identification;
      if (!StringUtils.isEmpty(registration.folder)) {
         id += "-" + registration.folder;
      }
      federationPullResults.put(id, registration);
      return true;
   }
   /**
    * Returns the list of registration results.
    *
    * @return the list of registration results
    */
   public List<FederationModel> getFederationResultRegistrations() {
      return new ArrayList<FederationModel>(federationPullResults.values());
   }
   /**
    * Submit a federation proposal. The proposal is cached locally and the
    * Gitblit administrator(s) are notified via email.
    *
    * @param proposal
    *            the proposal
    * @param gitblitUrl
    *            the url of your gitblit instance to send an email to
    *            administrators
    * @return true if the proposal was submitted
    */
   public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
      // convert proposal to json
      String json = JsonUtils.toJsonString(proposal);
      try {
         // make the proposals folder
         File proposalsFolder = new File(getString(Keys.federation.proposalsFolder, "proposals")
               .trim());
         proposalsFolder.mkdirs();
         // cache json to a file
         File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
         com.gitblit.utils.FileUtils.writeContent(file, json);
      } catch (Exception e) {
         logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
      }
      // send an email, if possible
      try {
         Message message = mailExecutor.createMessageForAdministrators();
         if (message != null) {
            message.setSubject("Federation proposal from " + proposal.url);
            message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
                  + proposal.token);
            mailExecutor.queue(message);
         }
      } catch (Throwable t) {
         logger.error("Failed to notify administrators of proposal", t);
      }
      return true;
   }
   /**
    * Returns the list of pending federation proposals
    *
    * @return list of federation proposals
    */
   public List<FederationProposal> getPendingFederationProposals() {
      List<FederationProposal> list = new ArrayList<FederationProposal>();
      File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
      if (folder.exists()) {
         File[] files = folder.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
               return file.isFile()
                     && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
            }
         });
         for (File file : files) {
            String json = com.gitblit.utils.FileUtils.readContent(file, null);
            FederationProposal proposal = JsonUtils.fromJsonString(json,
                  FederationProposal.class);
            list.add(proposal);
         }
      }
      return list;
   }
   /**
    * Get repositories for the specified token.
    *
    * @param gitblitUrl
    *            the base url of this gitblit instance
    * @param token
    *            the federation token
    * @return a map of <cloneurl, RepositoryModel>
    */
   public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
      Map<String, String> federationSets = new HashMap<String, String>();
      for (String set : getStrings(Keys.federation.sets)) {
         federationSets.put(getFederationToken(set), set);
      }
      // Determine the Gitblit clone url
      StringBuilder sb = new StringBuilder();
      sb.append(gitblitUrl);
      sb.append(Constants.GIT_PATH);
      sb.append("{0}");
      String cloneUrl = sb.toString();
      // Retrieve all available repositories
      UserModel user = new UserModel(Constants.FEDERATION_USER);
      user.canAdmin = true;
      List<RepositoryModel> list = getRepositoryModels(user);
      // create the [cloneurl, repositoryModel] map
      Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
      for (RepositoryModel model : list) {
         // by default, setup the url for THIS repository
         String url = MessageFormat.format(cloneUrl, model.name);
         switch (model.federationStrategy) {
         case EXCLUDE:
            // skip this repository
            continue;
         case FEDERATE_ORIGIN:
            // federate the origin, if it is defined
            if (!StringUtils.isEmpty(model.origin)) {
               url = model.origin;
            }
            break;
         }
         if (federationSets.containsKey(token)) {
            // include repositories only for federation set
            String set = federationSets.get(token);
            if (model.federationSets.contains(set)) {
               repositories.put(url, model);
            }
         } else {
            // standard federation token for ALL
            repositories.put(url, model);
         }
      }
      return repositories;
   }
   /**
    * Creates a proposal from the token.
    *
    * @param gitblitUrl
    *            the url of this Gitblit instance
    * @param token
    * @return a potential proposal
    */
   public FederationProposal createFederationProposal(String gitblitUrl, String token) {
      FederationToken tokenType = FederationToken.REPOSITORIES;
      for (FederationToken type : FederationToken.values()) {
         if (token.equals(getFederationToken(type))) {
            tokenType = type;
            break;
         }
      }
      Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
      FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
            repositories);
      return proposal;
   }
   /**
    * Returns the proposal identified by the supplied token.
    *
    * @param token
    * @return the specified proposal or null
    */
   public FederationProposal getPendingFederationProposal(String token) {
      List<FederationProposal> list = getPendingFederationProposals();
      for (FederationProposal proposal : list) {
         if (proposal.token.equals(token)) {
            return proposal;
         }
      }
      return null;
   }
   /**
    * Deletes a pending federation proposal.
    *
    * @param a
    *            proposal
    * @return true if the proposal was deleted
    */
   public boolean deletePendingFederationProposal(FederationProposal proposal) {
      File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
      File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
      return file.delete();
   }
   /**
    * Notify the administrators by email.
    *
    * @param subject
    * @param message
    */
   public void notifyAdministrators(String subject, String message) {
      try {
         Message mail = mailExecutor.createMessageForAdministrators();
         if (mail != null) {
            mail.setSubject(subject);
            mail.setText(message);
            mailExecutor.queue(mail);
         }
      } catch (MessagingException e) {
         logger.error("Messaging error", e);
      }
   }
   /**
    * Returns the descriptions/comments of the Gitblit config settings.
    *
    * @return SettingsModel
    */
   public ServerSettings getSettingsModel() {
      // ensure that the current values are updated in the setting models
      for (String key : settings.getAllKeys(null)) {
         SettingModel setting = settingsModel.get(key);
         if (setting != null) {
            setting.currentValue = settings.getString(key, "");
         }
      }
      return settingsModel;
   }
   /**
    * Parse the properties file and aggregate all the comments by the setting
    * key. A setting model tracks the current value, the default value, the
    * description of the setting and and directives about the setting.
    *
    * @return Map<String, SettingModel>
    */
   private ServerSettings loadSettingModels() {
      ServerSettings settingsModel = new ServerSettings();
      try {
         // Read bundled Gitblit properties to extract setting descriptions.
         // This copy is pristine and only used for populating the setting
         // models map.
         InputStream is = servletContext.getResourceAsStream("/WEB-INF/reference.properties");
         BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
         StringBuilder description = new StringBuilder();
         SettingModel setting = new SettingModel();
         String line = null;
         while ((line = propertiesReader.readLine()) != null) {
            if (line.length() == 0) {
               description.setLength(0);
               setting = new SettingModel();
            } else {
               if (line.charAt(0) == '#') {
                  if (line.length() > 1) {
                     String text = line.substring(1).trim();
                     if (SettingModel.CASE_SENSITIVE.equals(text)) {
                        setting.caseSensitive = true;
                     } else if (SettingModel.RESTART_REQUIRED.equals(text)) {
                        setting.restartRequired = true;
                     } else if (SettingModel.SPACE_DELIMITED.equals(text)) {
                        setting.spaceDelimited = true;
                     } else if (text.startsWith(SettingModel.SINCE)) {
                        try {
                           setting.since = text.split(" ")[1];
                        } catch (Exception e) {
                           setting.since = text;
                        }
                     } else {
                        description.append(text);
                        description.append('\n');
                     }
                  }
               } else {
                  String[] kvp = line.split("=", 2);
                  String key = kvp[0].trim();
                  setting.name = key;
                  setting.defaultValue = kvp[1].trim();
                  setting.currentValue = setting.defaultValue;
                  setting.description = description.toString().trim();
                  settingsModel.add(setting);
                  description.setLength(0);
                  setting = new SettingModel();
               }
            }
         }
         propertiesReader.close();
      } catch (NullPointerException e) {
         logger.error("Failed to find resource copy of gitblit.properties");
      } catch (IOException e) {
         logger.error("Failed to load resource copy of gitblit.properties");
      }
      return settingsModel;
   }
   /**
    * Configure the Gitblit singleton with the specified settings source. This
    * source may be file settings (Gitblit GO) or may be web.xml settings
    * (Gitblit WAR).
    * 
    * @param settings
    */
   public void configureContext(IStoredSettings settings) {
   public void configureContext(IStoredSettings settings, boolean startFederation) {
      logger.info("Reading configuration from " + settings.toString());
      this.settings = settings;
      repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git"));
      logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
      repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
      serverStatus = new ServerStatus(isGO());
      String realm = settings.getString(Keys.realm.userService, "users.properties");
      IUserService loginService = null;
      try {
@@ -707,6 +1418,15 @@
         loginService = new FileUserService(realmFile);
      }
      setUserService(loginService);
      mailExecutor = new MailExecutor(settings);
      if (mailExecutor.isReady()) {
         scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
      } else {
         logger.warn("Mail server is not properly configured.  Mail services disabled.");
      }
      if (startFederation) {
         configureFederation();
      }
   }
   /**
@@ -717,11 +1437,15 @@
    */
   @Override
   public void contextInitialized(ServletContextEvent contextEvent) {
      servletContext = contextEvent.getServletContext();
      settingsModel = loadSettingModels();
      if (settings == null) {
         // Gitblit WAR is running in a servlet container
         WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext());
         configureContext(webxmlSettings);
         configureContext(webxmlSettings, true);
      }
      serverStatus.servletContainer = servletContext.getServerInfo();
   }
   /**
@@ -731,5 +1455,6 @@
   @Override
   public void contextDestroyed(ServletContextEvent contextEvent) {
      logger.info("Gitblit context destroyed by servlet container.");
      scheduledExecutor.shutdownNow();
   }
}