Merge "Add support for names-factory provided by a plugins" into stable-3.5
diff --git a/BUILD b/BUILD
index 537b627..027dcb0 100644
--- a/BUILD
+++ b/BUILD
@@ -11,6 +11,7 @@
 load("@rules_antlr//antlr:antlr4.bzl", "antlr")
 
 plugin_name = "task"
+test_factory_provider_plugin_name = "names-factory-provider"
 
 java_plugin(
     name = "auto-value-plugin",
@@ -75,12 +76,23 @@
     deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [plugin_name],
 )
 
+gerrit_plugin(
+    name = test_factory_provider_plugin_name,
+    dir_name = plugin_name,
+    srcs = ["src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java"] + glob(["src/test/java/**/names_factory_provider/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: " + test_factory_provider_plugin_name,
+        "Gerrit-Module: com.googlesource.gerrit.plugins.names_factory_provider.Module",
+        "Implementation-Title: Names Factory Provider",
+    ],
+)
+
 sh_test(
     name = "docker-tests",
     size = "medium",
     srcs = ["test/docker/run.sh"],
-    args = ["--task-plugin-jar", "$(location :task)"],
-    data = [plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]),
+    args = ["--task-plugin-jar", "$(location :task)", "--names-factory-provider-plugin-jar", "$(location :names-factory-provider)"],
+    data = [plugin_name, test_factory_provider_plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]),
     local = True,
     tags = ["docker"],
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index d9f98ca..f70a5dd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -27,6 +28,7 @@
 import com.google.gerrit.sshd.commands.Query;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
+import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
 import java.util.ArrayList;
 import java.util.List;
 import org.kohsuke.args4j.Option;
@@ -55,6 +57,8 @@
       bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
       DynamicSet.bind(binder(), WebUiPlugin.class)
           .toInstance(new JavaScriptPlugin("gr-task-plugin.js"));
+
+      DynamicMap.mapOf(binder(), PluginProvidedTaskNamesFactory.class);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index 53897b5..1859b0d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -33,7 +33,8 @@
 public class TaskConfig extends AbstractVersionedMetaData {
   public enum NamesFactoryType {
     CHANGE,
-    STATIC;
+    STATIC,
+    PLUGIN;
 
     public static NamesFactoryType getNamesFactoryType(String str) {
       for (NamesFactoryType type : NamesFactoryType.values()) {
@@ -164,12 +165,19 @@
     public String changes;
     public List<String> names;
     public String type;
+    public String plugin;
+    public String provider;
+    public List<String> args;
 
     public NamesFactory(SubSectionKey s) {
       super(s);
       changes = getString(s, KEY_CHANGES, null);
       names = getStringList(s, KEY_NAME);
       type = getString(s, KEY_TYPE, null);
+
+      plugin = getString(s, KEY_PLUGIN);
+      provider = getString(s, KEY_PROVIDER);
+      args = getStringList(s, KEY_ARG);
     }
   }
 
@@ -194,6 +202,7 @@
   public static final String SECTION_TASK = TaskKey.CONFIG_SECTION;
   public static final String SECTION_TASKS_FACTORY = TaskKey.CONFIG_TASKS_FACTORY;
   public static final String KEY_APPLICABLE = "applicable";
+  public static final String KEY_ARG = "arg";
   public static final String KEY_CHANGES = "changes";
   public static final String KEY_DUPLICATE_KEY = "duplicate-key";
   public static final String KEY_EXPORT_PREFIX = "export-";
@@ -204,8 +213,10 @@
   public static final String KEY_NAME = "name";
   public static final String KEY_NAMES_FACTORY = "names-factory";
   public static final String KEY_PASS = "pass";
+  public static final String KEY_PLUGIN = "plugin";
   public static final String KEY_PRELOAD_TASK = "preload-task";
   public static final String KEY_PROPERTIES_PREFIX = "set-";
+  public static final String KEY_PROVIDER = "provider";
   public static final String KEY_READY_HINT = "ready-hint";
   public static final String KEY_SUBTASK = "subtask";
   public static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index 5586774..bca5ccc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -21,10 +21,12 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -38,6 +40,7 @@
 import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactoryType;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
 import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory;
+import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
 import com.googlesource.gerrit.plugins.task.properties.Properties;
 import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
 import com.googlesource.gerrit.plugins.task.statistics.HitHashMapOfCollection;
@@ -96,6 +99,7 @@
   protected final Preloader preloader;
   protected final TaskConfigCache taskConfigCache;
   protected final TaskExpression.Factory taskExpressionFactory;
+  protected final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
   protected final NodeList root = new NodeList();
   protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
   protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
@@ -120,7 +124,8 @@
       PredicateCache predicateCache,
       TaskExpression.Factory taskExpressionFactory,
       Preloader.Factory preloaderFactory,
-      @Assisted TaskConfigCache taskConfigCache) {
+      @Assisted TaskConfigCache taskConfigCache,
+      DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
     this.accountResolver = accountResolver;
     this.allUsers = allUsers;
     this.user = user != null ? user : anonymousUser;
@@ -131,6 +136,7 @@
     this.taskConfigCache = taskConfigCache;
     this.taskExpressionFactory = taskExpressionFactory;
     this.preloader = preloaderFactory.create(taskConfigCache);
+    this.dynamicBeans = dynamicBeans;
   }
 
   public List<Node> getRootNodes(ChangeData changeData)
@@ -438,6 +444,9 @@
                 case CHANGE:
                   addChangeTypeTasks(tasksFactory, namesFactory);
                   continue;
+                case PLUGIN:
+                  addPluginTypeTasks(tasksFactory, namesFactory);
+                  continue;
               }
             }
           }
@@ -473,12 +482,39 @@
             return;
           }
         } catch (StorageException e) {
-          log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes);
+          log.atSevere().withCause(e).log(
+              "Running changes query '%s' failed", namesFactory.changes);
         } catch (QueryParseException | ConfigInvalidException e) {
         }
         addInvalidNode();
       }
 
+      protected void addPluginTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+          throws IOException {
+        if (namesFactory.plugin != null && namesFactory.provider != null) {
+          List<String> names;
+          try {
+            PluginProvidedTaskNamesFactory providedTaskNamesFactory =
+                PluginProvidedTaskNamesFactory.getProxyInstance(
+                    dynamicBeans, namesFactory.plugin, namesFactory.provider);
+            names = providedTaskNamesFactory.getNames(getChangeData(), namesFactory.args);
+          } catch (Exception e) {
+            log.atSevere().withCause(e).log("Failed to get plugin provided task names");
+            addInvalidNode();
+            return;
+          }
+          for (String name : names) {
+            try {
+              addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name)));
+            } catch (ConfigInvalidException e) {
+              addInvalidNode();
+            }
+          }
+          return;
+        }
+        addInvalidNode();
+      }
+
       public void addPreloaded(List<Task> defs) {
         nodes.addAll(factory.createFromPreloaded(defs));
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
new file mode 100644
index 0000000..949db10
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2024 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.task.extensions;
+
+import com.google.common.reflect.Reflection;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.lang.reflect.Method;
+import java.util.List;
+
+public interface PluginProvidedTaskNamesFactory extends DynamicBean {
+  static PluginProvidedTaskNamesFactory getProxyInstance(
+      DynamicMap<DynamicBean> dynamicBeans, String pluginName, String exportName)
+      throws IllegalArgumentException {
+    Object bean = dynamicBeans.get(pluginName, exportName);
+    if (bean == null) {
+      throw new IllegalArgumentException(
+          String.format(
+              "provider '%s' not found. Is plugin '%s' installed?", exportName, pluginName));
+    }
+    return getProxyInstance(PluginProvidedTaskNamesFactory.class, bean);
+  }
+
+  static <T> T getProxyInstance(Class<T> classz, Object bean)
+      throws ClassCastException, IllegalArgumentException {
+    return Reflection.newProxy(
+        classz,
+        (Object proxy, Method method, Object[] args) ->
+            bean.getClass()
+                .getMethod(method.getName(), method.getParameterTypes())
+                .invoke(bean, args));
+  }
+
+  List<String> getNames(ChangeData change, List<String> args) throws Exception;
+}
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 4860e0b..4cf8c71 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -351,37 +351,23 @@
 Names-Factory
 -------------
 A names-factory section defines a collection of name keys which are used to
-generate the names for task definitions.  The section should contain a "type"
-key that specifies the type.
+generate the names for task definitions. A names-factory section is referenced
+by a names-factory key in a "tasks-factory" section. This section should contain
+a `type` key that specifies the type.
 
-A names-factory section is referenced by a names-factory key in a "tasks-factory"
-section.  A sample task.config which defines a names-factory section might look like
-this:
+`type`
 
-```
-[names-factory "static names factory list"]
-    name = my a task
-    name = my b task
-    type = static
-```
+: This key is mandatory and defines the type of the names-factory section. The
+type must be one of: `static`, `change`, or `plugin`.
 
-The following keys may be defined in any names-factory section:
+**static type**
 
-`changes`
-
-: This key defines a query that is used to fetch change numbers which will be used
-as the names of the task(s).
-
-Example:
-```
-    changes = change:1 OR change:2
-```
+: One or more `name` key(s) are required.
 
 `name`
 
-: This key defines the name of the tasks.  This key may be used several times
-in order to define more than one task. The name key can only be used along with
-names-factory of type `static`.
+: This key defines the name of the tasks. It can be used several times in order
+to define more than one task.
 
 Example:
 ```
@@ -389,19 +375,89 @@
     name = 12345
 ```
 
-`type`
+Here is an example which defines a names-factory of `static` type.
 
-: This key defines the type of the names-factory section.  The type
-can be either `static` or `change`. For names-factory of type `static`,
-`name` key(s) should be defined where as names-factory of type `change`
-needs a `change` key to be defined.
+```
+[names-factory "static names factory"]
+    type = static
+    name = task A
+    name = task B
+```
+
+**change type**
+
+: The `changes` key is required.
+
+`changes`
+
+: This key defines a query that is used to fetch change numbers which will be
+used as the names of the task(s).
 
 Example:
 ```
-    type = static
-    type = change
+    changes = change:1 OR change:2
 ```
 
+Here is an example which defines a names-factory of `change` type.
+
+```
+[names-factory "changes names factory"]
+    type = change
+    changes = topic:sample AND status:open
+```
+
+**plugin type**
+
+: The `plugin` and `provider` keys are required, whereas any `arg` key(s)
+are optional. The provider class should implement the `PluginProvidedTaskNamesFactory`
+interface. The collection of name strings returned by the `getNames()` method will
+be used to generate the names for task definitions.
+
+`arg`
+
+: This key defines an argument that will be passed down to the provider method
+`getNames()`. This key may be used several times in order to define a list of
+arguments.
+
+Example:
+```
+    arg = foo
+```
+
+`plugin`
+
+: This key defines the name of a plugin. The plugin is expected to register
+(bind) a class which implements the `PluginProvidedTaskNamesFactory` interface.
+
+Example:
+```
+    plugin = foobar
+```
+
+`provider`
+
+: This key defines the exported name used to bind the class which implements the
+`PluginProvidedTaskNamesFactory` interface provided by the plugin.
+
+Example:
+```
+    provider = foobar_provider
+```
+
+Here is an example which defines a names-factory of `plugin` type.
+
+```
+[names-factory "plugin names factory"]
+    type = plugin
+    plugin = names_factory_provider
+    provider = foobar_provider
+    arg = myarg1
+    arg = myarg2
+```
+
+A plugin `names_factory_provider` created exclusively for use in the test framework
+can also be viewed as a reference.
+
 External Entries
 ----------------
 A name for external task files on other projects and branches may be given
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index 493a699..54ef4fb 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -784,6 +784,42 @@
    ]
 }
 
+[root "Root tasks-factory PLUGIN"]
+  applicable = status:new
+  subtasks-factory = tasks-factory plugin
+
+[tasks-factory "tasks-factory plugin"]
+  names-factory = names-factory plugin list
+  fail = True
+
+[names-factory "names-factory plugin list"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+  arg = baz
+  arg = qux
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory PLUGIN",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-test-baz",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-test-qux",
+         "status" : "FAIL"
+      }
+   ]
+}
+
 [root "Root tasks-factory static (empty name)"]
   subtasks-factory = tasks-factory static (empty name)
   # Grouping task since it has no pass criteria, not output since it has no subtasks
@@ -1373,6 +1409,36 @@
    ]
 }
 
+[root "Root Properties tasks-factory PLUGIN"]
+  subtasks-factory = tasks-factory PLUGIN Properties
+
+[tasks-factory "tasks-factory PLUGIN Properties"]
+  set-welcome-message = Welcome to the party
+  names-factory = names-factory plugin provided list
+  fail-hint = ${welcome-message} Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
+  fail = True
+
+[names-factory "names-factory plugin provided list"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Properties tasks-factory PLUGIN",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "hint" : "Welcome to the party Name(foobar-test) Change Number(_change_number) Change Id(_change_id) Change Project(_change_project) Change Branch(_change_branch) Change Status(_change_status) Change Topic(_change_topic)",
+         "name" : "foobar-test",
+         "status" : "FAIL"
+      }
+   ]
+}
+
 [root "Root tasks-factory _name Property Reference"]
   subtasks-factory = Properties tasks-factory _name Property Reference
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
new file mode 100644
index 0000000..0ea2535
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2024 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.names_factory_provider;
+
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class FoobarProvider implements PluginProvidedTaskNamesFactory {
+  public static final String DELIMITER = "-";
+
+  @Override
+  public List<String> getNames(ChangeData changeData, List<String> args) throws Exception {
+    String name = String.join(DELIMITER, "foobar", changeData.project().get());
+    if (args == null || args.isEmpty()) {
+      return List.of(name);
+    }
+    return new ArrayList<>(
+        args.stream().map(x -> String.join(DELIMITER, name, x)).collect(Collectors.toList()));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/Module.java b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/Module.java
new file mode 100644
index 0000000..174a981
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/Module.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2024 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.names_factory_provider;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.inject.AbstractModule;
+
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(DynamicOptions.DynamicBean.class)
+        .annotatedWith(Exports.named("foobar_provider"))
+        .to(FoobarProvider.class);
+  }
+}
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
index a228122..57cb5f2 100755
--- a/test/docker/docker-compose.yaml
+++ b/test/docker/docker-compose.yaml
@@ -7,6 +7,7 @@
       args:
         - GERRIT_WAR
         - TASK_PLUGIN_JAR
+        - NAMES_FACTORY_PROVIDER_PLUGIN_JAR
     networks:
       - gerrit-net
     volumes:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index a407854..8bf8554 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -7,5 +7,6 @@
 
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
+RUN cp /tmp/names-factory-provider.jar "$GERRIT_SITE/plugins/names-factory-provider.jar"
 RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
 RUN chmod 777 "$GERRIT_SITE/git"
diff --git a/test/docker/run.sh b/test/docker/run.sh
index 24bda2f..0335cfa 100755
--- a/test/docker/run.sh
+++ b/test/docker/run.sh
@@ -24,21 +24,26 @@
 usage() { # [error_message]
     cat <<-EOF
 Usage:
-    $MYPROG [--task-plugin-jar|-t <FILE_PATH>] [--gerrit-war|-g <FILE_PATH>]
+    $MYPROG [--task-plugin-jar|-t <FILE_PATH>]
+            [--names-factory-provider-plugin-jar|-f <FILE_PATH>]
+            [--gerrit-war|-g <FILE_PATH>]
 
     This tool runs the plugin functional tests in a Docker environment built
     from the gerritcodereview/gerrit base Docker image.
 
-    The task plugin JAR and optionally a Gerrit WAR are expected to be in the
-    $ARTIFACTS dir;
-    however, the --task-plugin-jar and --gerrit-war switches may be used as
-    helpers to specify which files to copy there.
+    The task plugin JAR, names-factory-provider plugin JAR and optionally a
+    Gerrit WAR are expected to be in the $ARTIFACTS dir;
+    however, the --task-plugin-jar, --names-factory-provider-plugin-jar and
+    --gerrit-war switches may be used as helpers to specify which files to
+    copy there.
 
     Options:
     --help|-h
-    --gerrit-war|-g            path to Gerrit WAR file
-    --task-plugin-jar|-t       path to task plugin JAR file
-    --preserve                 To preserve the docker setup for debugging
+    --gerrit-war|-g                           path to Gerrit WAR file
+    --task-plugin-jar|-t                      path to task plugin JAR file
+    --names-factory-provider-plugin-jar|-f    path to names-factory-provider plugin JAR file.
+                                              It can be generated using 'bazel build names-factory-provider'
+    --preserve                                To preserve the docker setup for debugging
 
 EOF
 
@@ -126,13 +131,14 @@
 COMPOSE_ARGS=()
 while (( "$#" )) ; do
     case "$1" in
-        --help|-h)                usage ;;
-        --gerrit-war|-g)          shift ; GERRIT_WAR=$1 ;;
-        --task-plugin-jar|-t)     shift ; TASK_PLUGIN_JAR=$1 ;;
-        --preserve)               PRESERVE="true" ;;
-        --retest)                 RETEST="true" ;;
-        --compose-arg)            shift ; COMPOSE_ARGS+=("$1") ;;
-        *)                        usage "invalid argument $1" ;;
+        --help|-h)                                usage ;;
+        --gerrit-war|-g)                          shift ; GERRIT_WAR=$1 ;;
+        --task-plugin-jar|-t)                     shift ; TASK_PLUGIN_JAR=$1 ;;
+        --names-factory-provider-plugin-jar|-f)   shift ; NAMES_FACTORY_PROVIDER_PLUGIN_JAR=$1 ;;
+        --preserve)                               PRESERVE="true" ;;
+        --retest)                                 RETEST="true" ;;
+        --compose-arg)                            shift ; COMPOSE_ARGS+=("$1") ;;
+        *)                                        usage "invalid argument $1" ;;
     esac
     shift
 done
@@ -150,6 +156,13 @@
     [ -n "$TASK_PLUGIN_JAR" ] && die "$MISSING, check for copy failure?"
     usage "$MISSING, did you forget --task-plugin-jar?"
 fi
+[ -n "$NAMES_FACTORY_PROVIDER_PLUGIN_JAR" ] && cp -f -- "$NAMES_FACTORY_PROVIDER_PLUGIN_JAR" \
+    "$ARTIFACTS/names-factory-provider.jar"
+if [ ! -e "$ARTIFACTS/names-factory-provider.jar" ] ; then
+    MISSING="Missing $ARTIFACTS/names-factory-provider.jar"
+    [ -n "$NAMES_FACTORY_PROVIDER_PLUGIN_JAR" ] && die "$MISSING, check for copy failure?"
+    usage "$MISSING, did you forget --names-factory-provider-plugin-jar?"
+fi
 [ -n "$GERRIT_WAR" ] && cp -f -- "$GERRIT_WAR" "$ARTIFACTS/gerrit.war"
 ( trap cleanup EXIT SIGTERM
     progress "Building docker images" build_images
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index 9794a6b..1abbd7e 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -44,6 +44,7 @@
 ssh -p 29418 "$GERRIT_HOST" gerrit set-account --http-password "$PASSWORD" "$USER"
 
 is_plugin_loaded "task" || die "Task plugin is not installed"
+is_plugin_loaded "names-factory-provider" || die "names-factory-provider plugin is not installed"
 
 NON_SECRET_USER="non_secret_user"
 UNTRUSTED_USER="untrusted_user"
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
index fb5ec6d..4d0c6c3 100644
--- a/tools/workspace_status.py
+++ b/tools/workspace_status.py
@@ -29,3 +29,4 @@
 
 
 print("STABLE_BUILD_TASK_LABEL %s" % revision())
+print("STABLE_BUILD_NAMES-FACTORY-PROVIDER_LABEL %s" % revision())