Merge "Add support for user-specific URL aliases"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 24f34af..26072e3 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1792,6 +1792,11 @@
 administrators can map plugin screens into the Gerrit URL namespace or
 even replace Gerrit screens by plugin screens.
 
+Plugins may also programatically add URL aliases in the preferences of
+of a user. This way certain screens can be replaced for certain users.
+E.g. the plugin may offer a user preferences setting for choosing a
+screen that then sets/unsets a URL alias for the user.
+
 [[settings-screen]]
 == Plugin Settings Screen
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 096cf04..8268e6c 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1638,6 +1638,9 @@
 |`my`                           ||
 The menu items of the `MY` top menu as a list of
 link:rest-api-config.html#top-menu-item-info[TopMenuItemInfo] entities.
+|`url_aliases`                  |optional|
+A map of URL path pairs, where the first URL path is an alias for the
+second URL path.
 |============================================
 
 [[preferences-input]]
@@ -1684,6 +1687,9 @@
 |`my`                           |optional|
 The menu items of the `MY` top menu as a list of
 link:rest-api-config.html#top-menu-item-info[TopMenuItemInfo] entities.
+|`url_aliases`                  |optional|
+A map of URL path pairs, where the first URL path is an alias for the
+second URL path.
 |============================================
 
 [[query-limit-info]]
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
index 4482fd0..808726e 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.client.info;
 
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
@@ -24,7 +27,9 @@
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class AccountPreferencesInfo extends JavaScriptObject {
   public static AccountPreferencesInfo create() {
@@ -197,6 +202,26 @@
   final native void initMy() /*-{ this.my = []; }-*/;
   final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
 
+  public final Map<String, String> urlAliases() {
+    Map<String, String> urlAliases = new HashMap<>();
+    for (String k : Natives.keys(_urlAliases())) {
+      urlAliases.put(k, urlAliasToken(k));
+    }
+    return urlAliases;
+  }
+
+  private final native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
+  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+
+  public final void setUrlAliases(Map<String, String> urlAliases) {
+    initUrlAliases();
+    for (Map.Entry<String, String> e : urlAliases.entrySet()) {
+      putUrlAlias(e.getKey(), e.getValue());
+    }
+  }
+  private final native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
+  private final native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
+
   protected AccountPreferencesInfo() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index fb85f62..d52f2fc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -391,6 +391,7 @@
     myAccount = AccountInfo.create(0, null, null, null);
     myAccountDiffPref = null;
     myPrefs = AccountPreferencesInfo.createDefault();
+    urlAliasMatcher.clearUserAliases();
     xGerritAuth = null;
     refreshMenuBar();
 
@@ -880,6 +881,7 @@
       siteFooter.setVisible(myPrefs.showSiteHeader());
     }
     FormatUtil.setPreferences(myPrefs);
+    urlAliasMatcher.updateUserAliases(myPrefs.urlAliases());
   }
 
   private static void getDocIndex(final AsyncCallback<DocInfo> cb) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
index adaee55..f4ba870 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
@@ -20,18 +20,41 @@
 import java.util.Map;
 
 public class UrlAliasMatcher {
+  private final Map<RegExp, String> userUrlAliases;
   private final Map<RegExp, String> globalUrlAliases;
 
   UrlAliasMatcher(Map<String, String> globalUrlAliases) {
-    this.globalUrlAliases = new HashMap<>();
-    if (globalUrlAliases != null) {
-      for (Map.Entry<String, String> e : globalUrlAliases.entrySet()) {
-        this.globalUrlAliases.put(RegExp.compile(e.getKey()), e.getValue());
+    this.globalUrlAliases = compile(globalUrlAliases);
+    this.userUrlAliases = new HashMap<>();
+  }
+
+  private static Map<RegExp, String> compile(Map<String, String> urlAliases) {
+    Map<RegExp, String> compiledUrlAliases = new HashMap<>();
+    if (urlAliases != null) {
+      for (Map.Entry<String, String> e : urlAliases.entrySet()) {
+        compiledUrlAliases.put(RegExp.compile(e.getKey()), e.getValue());
       }
     }
+    return compiledUrlAliases;
+  }
+
+  void clearUserAliases() {
+    this.userUrlAliases.clear();
+  }
+
+  void updateUserAliases(Map<String, String> userUrlAliases) {
+    clearUserAliases();
+    this.userUrlAliases.putAll(compile(userUrlAliases));
   }
 
   public String replace(String token) {
+    for (Map.Entry<RegExp, String> e : userUrlAliases.entrySet()) {
+      RegExp pat = e.getKey();
+      if (pat.exec(token) != null) {
+        return pat.replace(token, e.getValue());
+      }
+    }
+
     for (Map.Entry<RegExp, String> e : globalUrlAliases.entrySet()) {
       RegExp pat = e.getKey();
       if (pat.exec(token) != null) {
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 28c3be6..1967a2b 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
@@ -44,7 +44,9 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @Singleton
 public class GetPreferences implements RestReadView<AccountResource> {
@@ -54,6 +56,9 @@
   public static final String KEY_URL = "url";
   public static final String KEY_TARGET = "target";
   public static final String KEY_ID = "id";
+  public static final String URL_ALIAS = "urlAlias";
+  public static final String KEY_MATCH = "match";
+  public static final String KEY_TOKEN = "token";
 
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
@@ -110,6 +115,7 @@
     ReviewCategoryStrategy reviewCategoryStrategy;
     DiffView diffView;
     List<TopMenu.MenuItem> my;
+    Map<String, String> urlAliases;
 
     public PreferenceInfo(AccountGeneralPreferences p,
         VersionedAccountPreferences v, Repository allUsers) {
@@ -129,12 +135,12 @@
         reviewCategoryStrategy = p.getReviewCategoryStrategy();
         diffView = p.getDiffView();
       }
-      my = my(v, allUsers);
+      loadFromAllUsers(v, allUsers);
     }
 
-    private List<TopMenu.MenuItem> my(VersionedAccountPreferences v,
+    private void loadFromAllUsers(VersionedAccountPreferences v,
         Repository allUsers) {
-      List<TopMenu.MenuItem> my = my(v);
+      my = my(v);
       if (my.isEmpty() && !v.isDefaults()) {
         try {
           VersionedAccountPreferences d = VersionedAccountPreferences.forDefault();
@@ -153,7 +159,8 @@
         my.add(new TopMenu.MenuItem("Starred Changes", "#/q/is:starred", null));
         my.add(new TopMenu.MenuItem("Groups", "#/groups/self", null));
       }
-      return my;
+
+      urlAliases = urlAliases(v);
     }
 
     private List<TopMenu.MenuItem> my(VersionedAccountPreferences v) {
@@ -175,5 +182,15 @@
       String val = cfg.getString(MY, subsection, key);
       return !Strings.isNullOrEmpty(val) ? val : defaultValue;
     }
+
+    private static Map<String, String> urlAliases(VersionedAccountPreferences v) {
+      HashMap<String, String> urlAliases = new HashMap<>();
+      Config cfg = v.getConfig();
+      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+        urlAliases.put(cfg.getString(URL_ALIAS, subsection, KEY_MATCH),
+           cfg.getString(URL_ALIAS, subsection, KEY_TOKEN));
+      }
+      return !urlAliases.isEmpty() ? urlAliases : null;
+    }
   }
 }
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 d75c5a2..b927596 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
@@ -15,9 +15,12 @@
 package com.google.gerrit.server.account;
 
 import static com.google.gerrit.server.account.GetPreferences.KEY_ID;
+import static com.google.gerrit.server.account.GetPreferences.KEY_MATCH;
 import static com.google.gerrit.server.account.GetPreferences.KEY_TARGET;
+import static com.google.gerrit.server.account.GetPreferences.KEY_TOKEN;
 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.URL_ALIAS;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -48,6 +51,8 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 @Singleton
 public class SetPreferences implements RestModifyView<AccountResource, Input> {
@@ -67,6 +72,7 @@
     public ReviewCategoryStrategy reviewCategoryStrategy;
     public DiffView diffView;
     public List<TopMenu.MenuItem> my;
+    public Map<String, String> urlAliases;
   }
 
   private final Provider<CurrentUser> self;
@@ -164,11 +170,11 @@
       db.get().accounts().update(Collections.singleton(a));
       db.get().commit();
       storeMyMenus(versionedPrefs, i.my);
+      storeUrlAliases(versionedPrefs, i.urlAliases);
       versionedPrefs.commit(md);
       cache.evict(accountId);
       return new GetPreferences.PreferenceInfo(
-          p, versionedPrefs,
-          md.getRepository());
+          p, versionedPrefs, md.getRepository());
     } finally {
       md.close();
       db.get().rollback();
@@ -202,4 +208,21 @@
       cfg.unsetSection(section, subsection);
     }
   }
+
+  public static void storeUrlAliases(VersionedAccountPreferences prefs,
+      Map<String, String> urlAliases) {
+    if (urlAliases != null) {
+      Config cfg = prefs.getConfig();
+      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+        cfg.unsetSection(URL_ALIAS, subsection);
+      }
+
+      int i = 1;
+      for (Entry<String, String> e : urlAliases.entrySet()) {
+        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_MATCH, e.getKey());
+        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_TOKEN, e.getValue());
+        i++;
+      }
+    }
+  }
 }
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index e49010b..d2f6bc3 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit e49010bbbed9d941c35a9f1eed1178cd909c7e34
+Subproject commit d2f6bc3511185729d3ecc3b3df25b1e9cebe2b2d