Merge "Rewrite my menu preferences to avoid ProjectState caching"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index f991ba2..401846a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -28,22 +28,27 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
 public class GetPreferences implements RestReadView<AccountResource> {
-  public static final String PREFERENCES = "preferences.config";
+  private static final Logger log = LoggerFactory.getLogger(GetPreferences.class);
+
   public static final String MY = "my";
   public static final String KEY_URL = "url";
   public static final String KEY_TARGET = "target";
@@ -51,19 +56,26 @@
 
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
-  private final ProjectState allUsers;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
 
   @Inject
   GetPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db,
-      ProjectCache projectCache) {
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
     this.db = db;
-    this.allUsers = projectCache.getAllUsers();
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
   }
 
   @Override
   public PreferenceInfo apply(AccountResource rsrc)
-      throws AuthException, ResourceNotFoundException, OrmException {
+      throws AuthException,
+      ResourceNotFoundException,
+      OrmException,
+      IOException,
+      ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("restricted to administrator");
@@ -72,8 +84,16 @@
     if (a == null) {
       throw new ResourceNotFoundException();
     }
-    return new PreferenceInfo(a.getGeneralPreferences(),
-        rsrc.getUser().getAccountId(), allUsers);
+
+    Repository git = gitMgr.openRepository(allUsersName);
+    try {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forUser(rsrc.getUser().getAccountId());
+      p.load(git);
+      return new PreferenceInfo(a.getGeneralPreferences(), p, git);
+    } finally {
+      git.close();
+    }
   }
 
   public static class PreferenceInfo {
@@ -96,13 +116,8 @@
     ChangeScreen changeScreen;
     List<TopMenu.MenuItem> my;
 
-    public PreferenceInfo(AccountGeneralPreferences p, Account.Id accountId,
-        ProjectState allUsers) {
-      this(p, RefNames.refsUsers(accountId), allUsers);
-    }
-
-    public PreferenceInfo(AccountGeneralPreferences p, String ref,
-        ProjectState allUsers) {
+    public PreferenceInfo(AccountGeneralPreferences p,
+        VersionedAccountPreferences v, Repository allUsers) {
       if (p != null) {
         changesPerPage = p.getMaximumPageSize();
         showSiteHeader = p.isShowSiteHeader() ? true : null;
@@ -120,13 +135,20 @@
         diffView = p.getDiffView();
         changeScreen = p.getChangeScreen();
       }
-      my = my(ref, allUsers);
+      my = my(v, allUsers);
     }
 
-    private List<TopMenu.MenuItem> my(String ref, ProjectState allUsers) {
-      List<TopMenu.MenuItem> my = my(allUsers, ref);
-      if (my.isEmpty() && !ref.equals(RefNames.REFS_USER + "default")) {
-        my = my(allUsers, RefNames.REFS_USER + "default");
+    private List<TopMenu.MenuItem> my(VersionedAccountPreferences v,
+        Repository allUsers) {
+      List<TopMenu.MenuItem> my = my(v);
+      if (my.isEmpty() && !v.isDefaults()) {
+        try {
+          VersionedAccountPreferences d = VersionedAccountPreferences.forDefault();
+          d.load(allUsers);
+          my = my(d);
+        } catch (ConfigInvalidException | IOException e) {
+          log.warn("cannot read default preferences", e);
+        }
       }
       if (my.isEmpty()) {
         my.add(new TopMenu.MenuItem("Changes", "#/dashboard/self", null));
@@ -138,9 +160,9 @@
       return my;
     }
 
-    private List<TopMenu.MenuItem> my(ProjectState allUsers, String ref) {
+    private List<TopMenu.MenuItem> my(VersionedAccountPreferences v) {
       List<TopMenu.MenuItem> my = new ArrayList<>();
-      Config cfg = allUsers.getConfig(PREFERENCES, ref).get();
+      Config cfg = v.getConfig();
       for (String subsection : cfg.getSubsections(MY)) {
         String url = my(cfg, subsection, KEY_URL, "#/");
         String target = my(cfg, subsection, KEY_TARGET,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index baf8eda..df985ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -18,7 +18,6 @@
 import static com.google.gerrit.server.account.GetPreferences.KEY_TARGET;
 import static com.google.gerrit.server.account.GetPreferences.KEY_URL;
 import static com.google.gerrit.server.account.GetPreferences.MY;
-import static com.google.gerrit.server.account.GetPreferences.PREFERENCES;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -34,18 +33,16 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectLevelConfig;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
@@ -76,22 +73,22 @@
   private final AccountCache cache;
   private final ReviewDb db;
   private final MetaDataUpdate.User metaDataUpdateFactory;
-  private final ProjectState allUsers;
+  private final AllUsersName allUsersName;
 
   @Inject
   SetPreferences(Provider<CurrentUser> self, AccountCache cache, ReviewDb db,
-      MetaDataUpdate.User metaDataUpdateFactory, ProjectCache projectCache) {
+      MetaDataUpdate.User metaDataUpdateFactory, AllUsersName allUsersName) {
     this.self = self;
     this.cache = cache;
     this.db = db;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
-    this.allUsers = projectCache.getAllUsers();
+    this.allUsersName = allUsersName;
   }
 
   @Override
   public GetPreferences.PreferenceInfo apply(AccountResource rsrc, Input i)
       throws AuthException, ResourceNotFoundException, OrmException,
-      IOException {
+      IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("restricted to administrator");
@@ -102,6 +99,8 @@
 
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountGeneralPreferences p;
+    VersionedAccountPreferences versionedPrefs;
+    MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
     db.accounts().beginTransaction(accountId);
     try {
       Account a = db.accounts().get(accountId);
@@ -109,6 +108,9 @@
         throw new ResourceNotFoundException();
       }
 
+      versionedPrefs = VersionedAccountPreferences.forUser(accountId);
+      versionedPrefs.load(md);
+
       p = a.getGeneralPreferences();
       if (p == null) {
         p = new AccountGeneralPreferences();
@@ -163,24 +165,21 @@
 
       db.accounts().update(Collections.singleton(a));
       db.commit();
-      storeMyMenus(accountId, i.my);
+      storeMyMenus(versionedPrefs, i.my);
+      versionedPrefs.commit(md);
       cache.evict(accountId);
+      return new GetPreferences.PreferenceInfo(
+          p, versionedPrefs,
+          md.getRepository());
     } finally {
+      md.close();
       db.rollback();
     }
-    return new GetPreferences.PreferenceInfo(p, accountId, allUsers);
   }
 
-  private void storeMyMenus(Account.Id accountId, List<TopMenu.MenuItem> my)
-      throws IOException {
-    storeMyMenus(RefNames.refsUsers(accountId), my);
-  }
-
-  public void storeMyMenus(String ref, List<TopMenu.MenuItem> my)
-      throws IOException {
-    ProjectLevelConfig prefsCfg =
-        allUsers.getConfig(PREFERENCES, ref);
-    Config cfg = prefsCfg.get();
+  public static void storeMyMenus(VersionedAccountPreferences prefs,
+      List<TopMenu.MenuItem> my) {
+    Config cfg = prefs.getConfig();
     if (my != null) {
       unsetSection(cfg, MY);
       for (TopMenu.MenuItem item : my) {
@@ -189,10 +188,6 @@
         set(cfg, item.name, KEY_ID, item.id);
       }
     }
-    MetaDataUpdate md =
-        metaDataUpdateFactory.create(allUsers.getProject().getNameKey());
-    md.setMessage("Updated preferences\n");
-    prefsCfg.commit(md);
   }
 
   private static void set(Config cfg, String section, String key, String val) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
new file mode 100644
index 0000000..c4d4b06
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2014 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.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+/** Preferences for user accounts. */
+public class VersionedAccountPreferences extends VersionedMetaData {
+  private static final String REFS_USER_DEFAULT = RefNames.REFS_USER + "default";
+  private static final String PREFERENCES = "preferences.config";
+
+  public static VersionedAccountPreferences forUser(Account.Id id) {
+    return new VersionedAccountPreferences(RefNames.refsUsers(id));
+  }
+
+  public static VersionedAccountPreferences forDefault() {
+    return new VersionedAccountPreferences(REFS_USER_DEFAULT);
+  }
+
+  private final String ref;
+  private Config cfg;
+
+  private VersionedAccountPreferences(String ref) {
+    this.ref = ref;
+  }
+
+  public boolean isDefaults() {
+    return REFS_USER_DEFAULT.equals(getRefName());
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public Config getConfig() {
+    return cfg;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    cfg = readConfig(PREFERENCES);
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated preferences\n");
+    }
+    saveConfig(PREFERENCES, cfg);
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
index 8cd7c19..f6c1d1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
@@ -15,22 +15,38 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GetPreferences.PreferenceInfo;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
 public class GetPreferences implements RestReadView<ConfigResource> {
-  private final ProjectState allUsers;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
 
   @Inject
-  public GetPreferences(ProjectCache projectCache) {
-    this.allUsers = projectCache.getAllUsers();
+  public GetPreferences(AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
   }
 
   @Override
-  public PreferenceInfo apply(ConfigResource rsrc) {
-    return new PreferenceInfo(null, RefNames.REFS_USER + "default", allUsers);
+  public PreferenceInfo apply(ConfigResource rsrc)
+      throws IOException, ConfigInvalidException {
+    Repository git = gitMgr.openRepository(allUsersName);
+    try {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forDefault();
+      p.load(git);
+      return new PreferenceInfo(null, p, git);
+    } finally {
+      git.close();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index f93e908..e6f5253 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -18,29 +18,31 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.GetPreferences.PreferenceInfo;
 import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 public class SetPreferences implements RestModifyView<ConfigResource, Input> {
-  private final Provider<com.google.gerrit.server.account.SetPreferences> setPreferences;
-  private final Provider<GetPreferences> getDefaultPreferences;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
 
   @Inject
-  SetPreferences(
-      Provider<com.google.gerrit.server.account.SetPreferences> setPreferences,
-      Provider<GetPreferences> getDefaultPreferences) {
-    this.setPreferences = setPreferences;
-    this.getDefaultPreferences = getDefaultPreferences;
+  SetPreferences(MetaDataUpdate.User metaDataUpdateFactory,
+      AllUsersName allUsersName) {
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
   }
 
   @Override
   public Object apply(ConfigResource rsrc, Input i) throws BadRequestException,
-      IOException {
+      IOException, ConfigInvalidException {
     if (i.changesPerPage != null || i.showSiteHeader != null
         || i.useFlashClipboard != null || i.downloadScheme != null
         || i.downloadCommand != null || i.copySelfOnEmail != null
@@ -53,9 +55,17 @@
         || i.changeScreen != null) {
       throw new BadRequestException("unsupported option");
     }
-    if (i.my != null) {
-      setPreferences.get().storeMyMenus(RefNames.REFS_USER + "default", i.my);
+
+    VersionedAccountPreferences p;
+    MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
+    try {
+      p = VersionedAccountPreferences.forDefault();
+      p.load(md);
+      com.google.gerrit.server.account.SetPreferences.storeMyMenus(p, i.my);
+      p.commit(md);
+      return new PreferenceInfo(null, p, md.getRepository());
+    } finally {
+      md.close();
     }
-    return getDefaultPreferences.get().apply(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index e8b4b6a..b48028e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -137,7 +137,7 @@
     return projectName;
   }
 
-  Repository getRepository() {
+  public Repository getRepository() {
     return db;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java
index b141a46..b4f41a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java
@@ -29,23 +29,17 @@
 /** Configuration file in the projects refs/meta/config branch. */
 public class ProjectLevelConfig extends VersionedMetaData {
   private final String fileName;
-  private final String ref;
   private final ProjectState project;
   private Config cfg;
 
   public ProjectLevelConfig(String fileName, ProjectState project) {
-    this(fileName, RefNames.REFS_CONFIG, project);
-  }
-
-  public ProjectLevelConfig(String fileName, String ref, ProjectState project) {
     this.fileName = fileName;
     this.project = project;
-    this.ref = ref;
   }
 
   @Override
   protected String getRefName() {
-    return ref;
+    return RefNames.REFS_CONFIG;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 88f4d1f..69f76e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -87,7 +87,7 @@
   private final List<CommentLinkInfo> commentLinks;
 
   private final ProjectConfig config;
-  private final Map<ConfigKey, ProjectLevelConfig> configs;
+  private final Map<String, ProjectLevelConfig> configs;
   private final Set<AccountGroup.UUID> localOwners;
 
   /** Prolog rule state. */
@@ -223,16 +223,11 @@
   }
 
   public ProjectLevelConfig getConfig(String fileName) {
-    return getConfig(fileName, RefNames.REFS_CONFIG);
-  }
-
-  public ProjectLevelConfig getConfig(String fileName, String ref) {
-    ConfigKey cfgKey = new ConfigKey(fileName, ref);
-    if (configs.containsKey(cfgKey)) {
-      return configs.get(cfgKey);
+    if (configs.containsKey(fileName)) {
+      return configs.get(fileName);
     }
 
-    ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, ref, this);
+    ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
     try {
       Repository git = gitMgr.openRepository(getProject().getNameKey());
       try {
@@ -246,7 +241,7 @@
       log.warn("Failed to load " + fileName + " for " + getProject().getName(), e);
     }
 
-    configs.put(cfgKey, cfg);
+    configs.put(fileName, cfg);
     return cfg;
   }
 
@@ -505,26 +500,4 @@
     }
     return false;
   }
-
-  private static class ConfigKey {
-    final String file;
-    final String ref;
-
-    ConfigKey(String file, String ref) {
-      this.file = file;
-      this.ref = ref;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      return other instanceof ConfigKey
-          && file.equals(((ConfigKey) other).file)
-          && ref.equals(((ConfigKey) other).ref);
-    }
-
-    @Override
-    public int hashCode() {
-      return file.hashCode() * 31 + ref.hashCode();
-    }
-  }
 }