Support to edit project plugin configuration parameters in UI
Plugins can define project configuration parameters that project
owners are allowed to edit through the ProjectInfoScreen. This makes
it much easier for project owners to change the project specific plugin
configuration.
With this change only string parameters without inheritance are
supported. Support for inheritance and other parameter types is added
by follow-up changes.
Change-Id: Iab3bc2ea2eb6dd6d0508f0049f43f7b9b21f58c8
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 42c328d..16e901a 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -679,6 +679,31 @@
`refs/meta/config` branch, editing the `project.config` file and
pushing the commit back.
+Plugin configuration values that are stored in the `project.config`
+file can be exposed in the ProjectInfoScreen to allow project owners
+to see and edit them from the UI.
+
+For this an instance of `ProjectConfigEntry` needs to be bound for each
+parameter. The export name must be a valid Git variable name. The
+variable name is case-insensitive, allows only alphanumeric characters
+and '-', and must start with an alphabetic character.
+
+The example below shows how the parameter `plugin.helloworld.language`
+is bound to be editable from the WebUI. "Preferred Language" is
+provided as display name and "en" is set as default value.
+
+[source,java]
+----
+class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(ProjectConfigEntry.class)
+ .annotatedWith(Exports.named("language"))
+ .toInstance(new ProjectConfigEntry("Preferred Language", "en"));
+ }
+}
+----
+
[[project-specific-configuration]]
== Project Specific Configuration in own config file
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 1050fb1..f8cc723 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -460,6 +460,15 @@
"submit_type": "MERGE_IF_NECESSARY",
"state": "ACTIVE",
"commentlinks": {},
+ "plugin_config": {
+ "helloworld": {
+ "language": {
+ "display_name": "Preferred Language",
+ "type": "STRING",
+ "value": "en"
+ }
+ }
+ },
"actions": {
"cookbook~hello-project": {
"method": "POST",
@@ -1211,6 +1220,10 @@
|`theme` |optional|
The theme that is configured for the project as a link:#theme-info[
ThemeInfo] entity.
+|`plugin_config` |optional|
+Plugin configuration as map which maps the plugin name to a map of
+parameter names to link:#config-parameter-info[ConfigParameterInfo]
+entities.
|`actions` |optional|
Actions the caller might be able to perform on this project. The
information is a map of view names to
@@ -1265,8 +1278,28 @@
The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
Not set if the project state is `ACTIVE`. +
If not set, the project state is not updated.
+|`plugin_config_values` |optional|
+Plugin configuration values as map which maps the plugin name to a map
+of parameter names to values.
|=========================================
+[[config-parameter-info]]
+ConfigParameterInfo
+~~~~~~~~~~~~~~~~~~~
+The `ConfigParameterInfo` entity describes a project configuration
+parameter.
+
+[options="header",width="50%",cols="1,^2,4"]
+|===============================
+|Field Name ||Description
+|`display_name` |optional|
+The display name of the configuration parameter.
+|`type` ||
+The type of the configuration parameter, can be `STRING`.
+|`value` |optional|
+The value of the configuration parameter as string.
+|===============================
+
[[dashboard-info]]
=== DashboardInfo
The `DashboardInfo` entity contains information about a project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index 6b1269d..19ae1f3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -28,4 +28,5 @@
String effectiveMaxObjectSizeLimit(String effectiveMaxObjectSizeLimit);
String globalMaxObjectSizeLimit(String globalMaxObjectSizeLimit);
+ String pluginProjectOptionsTitle(String pluginName);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index cb3784f..4c69cf8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -7,3 +7,4 @@
deletedSection = Section {0} was deleted
effectiveMaxObjectSizeLimit = effective: {0}
globalMaxObjectSizeLimit = The global max object size limit is set to {0}. The limit cannot be increased on project level.
+pluginProjectOptionsTitle = {0} Plugin Options
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 58f28ce..9a670a0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -22,11 +22,13 @@
import com.google.gerrit.client.change.Resources;
import com.google.gerrit.client.download.DownloadPanel;
import com.google.gerrit.client.projects.ConfigInfo;
+import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterInfo;
import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
import com.google.gerrit.client.projects.ProjectApi;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.client.ui.SmallHeading;
@@ -41,18 +43,26 @@
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.globalkey.client.NpTextBox;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
public class ProjectInfoScreen extends ProjectScreen {
private boolean isOwner;
private LabeledWidgetsGrid grid;
+ private Panel pluginOptionsPanel;
private LabeledWidgetsGrid actionsGrid;
// Section: Project Options
@@ -62,6 +72,7 @@
private ListBox contentMerge;
private NpTextBox maxObjectSizeLimit;
private Label effectiveMaxObjectSizeLimit;
+ private Map<String, Map<String, FocusWidget>> pluginConfigWidgets;
// Section: Contributor Agreements
private ListBox contributorAgreements;
@@ -93,10 +104,12 @@
initDescription();
grid = new LabeledWidgetsGrid();
+ pluginOptionsPanel = new FlowPanel();
actionsGrid = new LabeledWidgetsGrid();
initProjectOptions();
initAgreements();
add(grid);
+ add(pluginOptionsPanel);
add(saveProject);
add(actionsGrid);
}
@@ -140,6 +153,14 @@
signedOffBy.setEnabled(isOwner);
requireChangeID.setEnabled(isOwner);
maxObjectSizeLimit.setEnabled(isOwner);
+
+ if (pluginConfigWidgets != null) {
+ for (Map<String, FocusWidget> widgetMap : pluginConfigWidgets.values()) {
+ for (FocusWidget widget : widgetMap.values()) {
+ widget.setEnabled(isOwner);
+ }
+ }
+ }
}
private void initDescription() {
@@ -323,9 +344,46 @@
}
saveProject.setEnabled(false);
+ initPluginOptions(result);
initProjectActions(result);
}
+ private void initPluginOptions(ConfigInfo info) {
+ pluginOptionsPanel.clear();
+ pluginConfigWidgets = new HashMap<String, Map<String, FocusWidget>>();
+
+ for (String pluginName : info.pluginConfig().keySet()) {
+ Map<String, FocusWidget> widgetMap = new HashMap<String, FocusWidget>();
+ pluginConfigWidgets.put(pluginName, widgetMap);
+ LabeledWidgetsGrid g = new LabeledWidgetsGrid();
+ g.addHeader(new SmallHeading(Util.M.pluginProjectOptionsTitle(pluginName)));
+ pluginOptionsPanel.add(g);
+ NativeMap<ConfigParameterInfo> pluginConfig =
+ info.pluginConfig(pluginName);
+ pluginConfig.copyKeysIntoChildren("name");
+ for (ConfigParameterInfo param : Natives.asList(pluginConfig.values())) {
+ FocusWidget w;
+ if ("STRING".equals(param.type())) {
+ w = renderTextBox(g, param);
+ } else {
+ continue;
+ }
+ widgetMap.put(param.name(), w);
+ }
+ }
+
+ enableForm();
+ }
+
+ private TextBox renderTextBox(LabeledWidgetsGrid g, ConfigParameterInfo param) {
+ NpTextBox textBox = new NpTextBox();
+ textBox.setValue(param.value());
+ g.add(param.displayName() != null
+ ? param.displayName() : param.name(), textBox);
+ saveEnabler.listenTo(textBox);
+ return textBox;
+ }
+
private void initProjectActions(ConfigInfo info) {
actionsGrid.clear(true);
actionsGrid.removeAllRows();
@@ -355,7 +413,7 @@
maxObjectSizeLimit.getText().trim(),
Project.SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
Project.State.valueOf(state.getValue(state.getSelectedIndex())),
- new GerritCallback<ConfigInfo>() {
+ getPluginConfigValues(), new GerritCallback<ConfigInfo>() {
@Override
public void onSuccess(ConfigInfo result) {
enableForm();
@@ -370,6 +428,23 @@
});
}
+ private Map<String, Map<String, String>> getPluginConfigValues() {
+ Map<String, Map<String, String>> pluginConfigValues =
+ new HashMap<String, Map<String, String>>(pluginConfigWidgets.size());
+ for (Entry<String, Map<String, FocusWidget>> e : pluginConfigWidgets.entrySet()) {
+ Map<String, String> values =
+ new HashMap<String, String>(e.getValue().size());
+ pluginConfigValues.put(e.getKey(), values);
+ for (Entry<String, FocusWidget> e2 : e.getValue().entrySet()) {
+ FocusWidget widget = e2.getValue();
+ if (widget instanceof TextBox) {
+ values.put(e2.getKey(), ((TextBox) widget).getValue().trim());
+ }
+ }
+ }
+ return pluginConfigValues;
+ }
+
public class ProjectDownloadPanel extends DownloadPanel {
public ProjectDownloadPanel(String project, boolean isAllowsAnonymous) {
super(project, null, isAllowsAnonymous);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index c7177e9..e4c99e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -49,6 +49,12 @@
return SubmitType.valueOf(submit_typeRaw());
}
+ public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
+ /*-{ return this.plugin_config || {}; }-*/;
+
+ public final native NativeMap<ConfigParameterInfo> pluginConfig(String p)
+ /*-{ return this.plugin_config[p]; }-*/;
+
public final native NativeMap<ActionInfo> actions()
/*-{ return this.actions; }-*/;
@@ -138,4 +144,14 @@
protected MaxObjectSizeLimitInfo() {
}
}
+
+ public static class ConfigParameterInfo extends JavaScriptObject {
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native String displayName() /*-{ return this.display_name; }-*/;
+ public final native String type() /*-{ return this.type; }-*/;
+ public final native String value() /*-{ return this.value; }-*/;
+
+ protected ConfigParameterInfo() {
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index adc2a64..c7d3bc2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -15,6 +15,7 @@
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Project;
@@ -24,6 +25,8 @@
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.rpc.AsyncCallback;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
public class ProjectApi {
@@ -81,7 +84,9 @@
InheritableBoolean useContributorAgreements,
InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
InheritableBoolean requireChangeId, String maxObjectSizeLimit,
- SubmitType submitType, Project.State state, AsyncCallback<ConfigInfo> cb) {
+ SubmitType submitType, Project.State state,
+ Map<String, Map<String, String>> pluginConfigValues,
+ AsyncCallback<ConfigInfo> cb) {
ConfigInput in = ConfigInput.create();
in.setDescription(description);
in.setUseContributorAgreements(useContributorAgreements);
@@ -91,6 +96,8 @@
in.setMaxObjectSizeLimit(maxObjectSizeLimit);
in.setSubmitType(submitType);
in.setState(state);
+ in.setPluginConfigValues(pluginConfigValues);
+
project(name).view("config").put(in, cb);
}
@@ -214,6 +221,33 @@
}
private final native void setStateRaw(String s)
/*-{ if(s)this.state=s; }-*/;
+
+ final void setPluginConfigValues(Map<String, Map<String, String>> pluginConfigValues) {
+ if (!pluginConfigValues.isEmpty()) {
+ NativeMap<StringMap> configValues = NativeMap.create().cast();
+ for (Entry<String, Map<String, String>> e : pluginConfigValues.entrySet()) {
+ StringMap values = StringMap.create();
+ configValues.put(e.getKey(), values);
+ for (Entry<String, String> e2 : e.getValue().entrySet()) {
+ values.put(e2.getKey(), e2.getValue());
+ }
+ }
+ setPluginConfigValuesRaw(configValues);
+ }
+ }
+ private final native void setPluginConfigValuesRaw(NativeMap<StringMap> v)
+ /*-{ this.plugin_config_values=v; }-*/;
+ }
+
+ private static class StringMap extends JavaScriptObject {
+ static StringMap create() {
+ return (StringMap) createObject();
+ }
+
+ protected StringMap() {
+ }
+
+ public final native void put(String n, String v) /*-{ this[n] = v; }-*/;
}
private static class BranchInput extends JavaScriptObject {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 9f72f08..290a223 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -269,6 +269,7 @@
DynamicSet.setOf(binder(), MessageOfTheDay.class);
DynamicMap.mapOf(binder(), DownloadScheme.class);
DynamicMap.mapOf(binder(), DownloadCommand.class);
+ DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
bind(AnonymousUser.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
new file mode 100644
index 0000000..29985ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 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.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public class ProjectConfigEntry {
+ public enum Type {
+ STRING
+ }
+
+ private final String displayName;
+ private final String defaultValue;
+ private final Type type;
+
+ public ProjectConfigEntry(String displayName, String defaultValue) {
+ this.displayName = displayName;
+ this.defaultValue = defaultValue;
+ this.type = Type.STRING;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
+ public Type getType() {
+ return type;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 9b9b517..096ee1a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -18,17 +18,22 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicMap.Entry;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.actions.ActionInfo;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.extensions.webui.UiActions;
import com.google.gerrit.server.git.TransferConfig;
import com.google.inject.util.Providers;
import java.util.Map;
+import java.util.TreeMap;
public class ConfigInfo {
public final String kind = "gerritcodereview#project_config";
@@ -41,6 +46,7 @@
public MaxObjectSizeLimitInfo maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
+ public Map<String, Map<String, ConfigParameterInfo>> pluginConfig;
public Map<String, ActionInfo> actions;
public Map<String, CommentLinkInfo> commentlinks;
@@ -48,6 +54,8 @@
public ConfigInfo(ProjectControl control,
TransferConfig config,
+ DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+ PluginConfigFactory cfgFactory,
DynamicMap<RestView<ProjectResource>> views) {
ProjectState projectState = control.getProjectState();
Project p = control.getProject();
@@ -103,6 +111,9 @@
this.commentlinks.put(cl.name, cl);
}
+ pluginConfig =
+ getPluginConfig(control.getProjectState(), pluginConfigEntries, cfgFactory);
+
actions = Maps.newTreeMap();
for (UiAction.Description d : UiActions.from(
views, new ProjectResource(control),
@@ -112,6 +123,28 @@
this.theme = projectState.getTheme();
}
+ private Map<String, Map<String, ConfigParameterInfo>> getPluginConfig(
+ ProjectState project, DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+ PluginConfigFactory cfgFactory) {
+ TreeMap<String, Map<String, ConfigParameterInfo>> pluginConfig = new TreeMap<>();
+ for (Entry<ProjectConfigEntry> e : pluginConfigEntries) {
+ PluginConfig cfg =
+ cfgFactory.getFromProjectConfig(project, e.getPluginName());
+ ProjectConfigEntry configEntry = e.getProvider().get();
+ ConfigParameterInfo p = new ConfigParameterInfo();
+ p.displayName = configEntry.getDisplayName();
+ p.type = configEntry.getType();
+ p.value = cfg.getString(e.getExportName(), configEntry.getDefaultValue());
+ Map<String, ConfigParameterInfo> pc = pluginConfig.get(e.getPluginName());
+ if (pc == null) {
+ pc = new TreeMap<>();
+ pluginConfig.put(e.getPluginName(), pc);
+ }
+ pc.put(e.getExportName(), p);
+ }
+ return !pluginConfig.isEmpty() ? pluginConfig : null;
+ }
+
public static class InheritedBooleanInfo {
public Boolean value;
public InheritableBoolean configuredValue;
@@ -123,4 +156,10 @@
public String configuredValue;
public String inheritedValue;
}
+
+ public static class ConfigParameterInfo {
+ public String displayName;
+ public ProjectConfigEntry.Type type;
+ public String value;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index 6f78651..1ee1d00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -18,6 +18,8 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.TransferConfig;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -25,18 +27,25 @@
public class GetConfig implements RestReadView<ProjectResource> {
private final TransferConfig config;
+ private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+ private final PluginConfigFactory cfgFactory;
private final DynamicMap<RestView<ProjectResource>> views;
@Inject
public GetConfig(TransferConfig config,
+ DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+ PluginConfigFactory cfgFactory,
DynamicMap<RestView<ProjectResource>> views,
Provider<CurrentUser> currentUser) {
this.config = config;
+ this.pluginConfigEntries = pluginConfigEntries;
+ this.cfgFactory = cfgFactory;
this.views = views;
}
@Override
public ConfigInfo apply(ProjectResource resource) {
- return new ConfigInfo(resource.getControl(), config, views);
+ return new ConfigInfo(resource.getControl(), config,
+ pluginConfigEntries, cfgFactory, views);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index cb25af3..048ae98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -25,6 +26,9 @@
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.TransferConfig;
@@ -34,10 +38,16 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.Map;
+import java.util.Map.Entry;
public class PutConfig implements RestModifyView<ProjectResource, Input> {
+ private static final Logger log = LoggerFactory.getLogger(PutConfig.class);
+
public static class Input {
public String description;
public InheritableBoolean useContributorAgreements;
@@ -47,6 +57,7 @@
public String maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
+ public Map<String, Map<String, String>> pluginConfigValues;
}
private final MetaDataUpdate.User metaDataUpdateFactory;
@@ -54,6 +65,8 @@
private final Provider<CurrentUser> self;
private final ProjectState.Factory projectStateFactory;
private final TransferConfig config;
+ private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+ private final PluginConfigFactory cfgFactory;
private final DynamicMap<RestView<ProjectResource>> views;
private final Provider<CurrentUser> currentUser;
@@ -63,6 +76,8 @@
Provider<CurrentUser> self,
ProjectState.Factory projectStateFactory,
TransferConfig config,
+ DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+ PluginConfigFactory cfgFactory,
DynamicMap<RestView<ProjectResource>> views,
Provider<CurrentUser> currentUser) {
this.metaDataUpdateFactory = metaDataUpdateFactory;
@@ -70,6 +85,8 @@
this.self = self;
this.projectStateFactory = projectStateFactory;
this.config = config;
+ this.pluginConfigEntries = pluginConfigEntries;
+ this.cfgFactory = cfgFactory;
this.views = views;
this.currentUser = currentUser;
}
@@ -126,6 +143,10 @@
p.setState(input.state);
}
+ if (input.pluginConfigValues != null) {
+ setPluginConfigValues(projectConfig, input.pluginConfigValues);
+ }
+
md.setMessage("Modified project settings\n");
try {
projectConfig.commit(md);
@@ -143,7 +164,7 @@
ProjectState state = projectStateFactory.create(projectConfig);
return new ConfigInfo(
state.controlFor(currentUser.get()),
- config, views);
+ config, pluginConfigEntries, cfgFactory, views);
} catch (ConfigInvalidException err) {
throw new ResourceConflictException("Cannot read project " + projectName, err);
} catch (IOException err) {
@@ -152,4 +173,39 @@
md.close();
}
}
+
+ private void setPluginConfigValues(ProjectConfig projectConfig,
+ Map<String, Map<String, String>> pluginConfigValues)
+ throws BadRequestException {
+ for (Entry<String, Map<String, String>> e : pluginConfigValues.entrySet()) {
+ String pluginName = e.getKey();
+ PluginConfig cfg = projectConfig.getPluginConfig(pluginName);
+ for (Entry<String, String> v : e.getValue().entrySet()) {
+ ProjectConfigEntry projectConfigEntry =
+ pluginConfigEntries.get(pluginName, v.getKey());
+ if (projectConfigEntry != null) {
+ if (!isValidParameterName(v.getKey())) {
+ log.warn(String.format(
+ "Parameter name '%s' must match '^[a-zA-Z0-9]+[a-zA-Z0-9-]*$'", v.getKey()));
+ continue;
+ }
+ if (v.getValue() != null) {
+ cfg.setString(v.getKey(), v.getValue());
+ } else {
+ cfg.unset(v.getKey());
+ }
+ } else {
+ throw new BadRequestException(String.format(
+ "The config parameter '%s' of plugin '%s' does not exist.",
+ v.getKey(), pluginName));
+ }
+ }
+ }
+ }
+
+ private static boolean isValidParameterName(String name) {
+ return CharMatcher.JAVA_LETTER_OR_DIGIT
+ .or(CharMatcher.is('-'))
+ .matchesAllOf(name) && !name.startsWith("-");
+ }
}