Plugin config: Add support for sections

Add ARRAY ProjectConfigEntry type to handle the whole section:

  [plugin "reviewers"]
    reviewer = john.doe@example.com
    reviewer = jane.doe@example.com
    reviewer = QAGroup

Change-Id: I891ff6efc3423bf84f07ce4dc21853d707402b9d
This commit is contained in:
David Ostrovsky 2014-02-01 19:13:27 +01:00 committed by David Ostrovsky
parent 6a88c2fcb8
commit c6dd217351
12 changed files with 244 additions and 57 deletions

View File

@ -1299,16 +1299,18 @@ The description of the configuration parameter.
|`warning` |optional|
Warning message for the configuration parameter.
|`type` ||
The type of the configuration parameter, can be `STRING`, `INT`,
`LONG`, `BOOLEAN` or `LIST`.
The type of the configuration parameter. Can be `STRING`, `INT`,
`LONG`, `BOOLEAN`, `LIST` or `ARRAY`.
|`value` |optional|
The value of the configuration parameter as string. If the parameter
is inheritable this is the effective value which is deduced from
`configured_value` and `inherited_value`.
|`values` |optional|
The list of values. Only set if the `type` is `ARRAY`.
`editable` |`false` if not set|
Whether the value is editable.
|`permitted_values`|optional|
The list of permitted values, only set if the `type` is `LIST`.
The list of permitted values. Only set if the `type` is `LIST`.
|`inheritable` |`false` if not set|
Whether the configuration parameter can be inherited.
|`configured_value`|optional|

View File

@ -23,6 +23,7 @@ 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.ConfigParameterValue;
import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
import com.google.gerrit.client.projects.ProjectApi;
import com.google.gerrit.client.rpc.CallbackGroup;
@ -366,17 +367,23 @@ public class ProjectInfoScreen extends ProjectScreen {
pluginConfig.copyKeysIntoChildren("name");
for (ConfigParameterInfo param : Natives.asList(pluginConfig.values())) {
FocusWidget w;
if ("STRING".equals(param.type())) {
w = renderTextBox(g, param, false);
} else if ("INT".equals(param.type()) || "LONG".equals(param.type())) {
w = renderTextBox(g, param, true);
} else if ("BOOLEAN".equals(param.type())) {
w = renderCheckBox(g, param);
} else if ("LIST".equals(param.type())
&& param.permittedValues() != null) {
w = renderListBox(g, param);
} else {
continue;
switch (param.type()) {
case "STRING":
case "INT":
case "LONG":
w = renderTextBox(g, param);
break;
case "BOOLEAN":
w = renderCheckBox(g, param);
break;
case "LIST":
w = renderListBox(g, param);
break;
case "ARRAY":
w = renderTextArea(g, param);
break;
default:
throw new UnsupportedOperationException("unsupported widget type");
}
if (param.editable()) {
widgetMap.put(param.name(), w);
@ -390,8 +397,10 @@ public class ProjectInfoScreen extends ProjectScreen {
}
private TextBox renderTextBox(LabeledWidgetsGrid g,
ConfigParameterInfo param, boolean numbersOnly) {
NpTextBox textBox = numbersOnly ? new NpIntTextBox() : new NpTextBox();
ConfigParameterInfo param) {
NpTextBox textBox = param.type().equals("STRING")
? new NpTextBox()
: new NpIntTextBox();
if (param.inheritable()) {
textBox.setValue(param.configuredValue());
Label inheritedLabel =
@ -434,6 +443,9 @@ public class ProjectInfoScreen extends ProjectScreen {
private ListBox renderListBox(LabeledWidgetsGrid g,
ConfigParameterInfo param) {
if (param.permittedValues() == null) {
return null;
}
ListBox listBox = new ListBox();
if (param.inheritable()) {
listBox.addItem(
@ -485,6 +497,26 @@ public class ProjectInfoScreen extends ProjectScreen {
return listBox;
}
private NpTextArea renderTextArea(LabeledWidgetsGrid g,
ConfigParameterInfo param) {
NpTextArea txtArea = new NpTextArea();
txtArea.setVisibleLines(4);
txtArea.setCharacterWidth(40);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < param.values().length(); i++) {
String v = param.values().get(i);
sb.append(v).append("\n");
}
txtArea.setText(sb.toString());
if (param.editable()) {
saveEnabler.listenTo(txtArea);
} else {
txtArea.setEnabled(false);
}
addWidget(g, txtArea, param);
return txtArea;
}
private void addWidget(LabeledWidgetsGrid g, Widget w, ConfigParameterInfo param) {
if (param.description() != null || param.warning() != null) {
HorizontalPanel p = new HorizontalPanel();
@ -554,26 +586,34 @@ public class ProjectInfoScreen extends ProjectScreen {
});
}
private Map<String, Map<String, String>> getPluginConfigValues() {
Map<String, Map<String, String>> pluginConfigValues =
private Map<String, Map<String, ConfigParameterValue>> getPluginConfigValues() {
Map<String, Map<String, ConfigParameterValue>> pluginConfigValues =
new HashMap<>(pluginConfigWidgets.size());
for (Entry<String, Map<String, FocusWidget>> e : pluginConfigWidgets.entrySet()) {
Map<String, String> values =
new HashMap<String, String>(e.getValue().size());
Map<String, ConfigParameterValue> values = new HashMap<>(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());
values.put(e2.getKey(), ConfigParameterValue.create()
.value(((TextBox) widget).getValue().trim()));
} else if (widget instanceof CheckBox) {
values.put(e2.getKey(), Boolean.toString(((CheckBox) widget).getValue()));
values.put(e2.getKey(), ConfigParameterValue.create()
.value(Boolean.toString(((CheckBox) widget).getValue())));
} else if (widget instanceof ListBox) {
ListBox listBox = (ListBox) widget;
// the inherited value is at index 0,
// if it is selected no value should be set on this project
String value = listBox.getSelectedIndex() > 0
? listBox.getValue(listBox.getSelectedIndex()) : null;
values.put(e2.getKey(), value);
values.put(e2.getKey(), ConfigParameterValue.create()
.value(value));
} else if (widget instanceof NpTextArea) {
String text = ((NpTextArea) widget).getText().trim();
values.put(e2.getKey(), ConfigParameterValue.create()
.values(text.split("\n")));
} else {
throw new UnsupportedOperationException("unsupported widget type");
}
}
}

View File

@ -158,8 +158,35 @@ public class ConfigInfo extends JavaScriptObject {
public final native String configuredValue() /*-{ return this.configured_value; }-*/;
public final native String inheritedValue() /*-{ return this.inherited_value; }-*/;
public final native JsArrayString permittedValues() /*-{ return this.permitted_values; }-*/;
public final native JsArrayString values() /*-{ return this.values; }-*/;
protected ConfigParameterInfo() {
}
}
public static class ConfigParameterValue extends JavaScriptObject {
final native void init() /*-{ this.values = []; }-*/;
final native void add_value(String v) /*-{ this.values.push(v); }-*/;
final native void set_value(String v) /*-{ if(v)this.value = v; }-*/;
public static ConfigParameterValue create() {
ConfigParameterValue v = createObject().cast();
return v;
}
public final ConfigParameterValue values(String[] values) {
init();
for (String v : values) {
add_value(v);
}
return this;
}
public final ConfigParameterValue value(String v) {
set_value(v);
return this;
}
protected ConfigParameterValue() {
}
}
}

View File

@ -14,6 +14,7 @@
package com.google.gerrit.client.projects;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
@ -85,7 +86,7 @@ public class ProjectApi {
InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
InheritableBoolean requireChangeId, String maxObjectSizeLimit,
SubmitType submitType, Project.State state,
Map<String, Map<String, String>> pluginConfigValues,
Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
AsyncCallback<ConfigInfo> cb) {
ConfigInput in = ConfigInput.create();
in.setDescription(description);
@ -222,32 +223,32 @@ public class ProjectApi {
private final native void setStateRaw(String s)
/*-{ if(s)this.state=s; }-*/;
final void setPluginConfigValues(Map<String, Map<String, String>> pluginConfigValues) {
final void setPluginConfigValues(Map<String, Map<String, ConfigParameterValue>> pluginConfigValues) {
if (!pluginConfigValues.isEmpty()) {
NativeMap<StringMap> configValues = NativeMap.create().cast();
for (Entry<String, Map<String, String>> e : pluginConfigValues.entrySet()) {
StringMap values = StringMap.create();
NativeMap<ConfigParameterValueMap> configValues = NativeMap.create().cast();
for (Entry<String, Map<String, ConfigParameterValue>> e : pluginConfigValues.entrySet()) {
ConfigParameterValueMap values = ConfigParameterValueMap.create();
configValues.put(e.getKey(), values);
for (Entry<String, String> e2 : e.getValue().entrySet()) {
for (Entry<String, ConfigParameterValue> e2 : e.getValue().entrySet()) {
values.put(e2.getKey(), e2.getValue());
}
}
setPluginConfigValuesRaw(configValues);
}
}
private final native void setPluginConfigValuesRaw(NativeMap<StringMap> v)
private final native void setPluginConfigValuesRaw(NativeMap<ConfigParameterValueMap> v)
/*-{ this.plugin_config_values=v; }-*/;
}
private static class StringMap extends JavaScriptObject {
static StringMap create() {
return (StringMap) createObject();
private static class ConfigParameterValueMap extends JavaScriptObject {
static ConfigParameterValueMap create() {
return createObject().cast();
}
protected StringMap() {
protected ConfigParameterValueMap() {
}
public final native void put(String n, String v) /*-{ this[n] = v; }-*/;
public final native void put(String n, ConfigParameterValue v) /*-{ this[n] = v; }-*/;
}
private static class BranchInput extends JavaScriptObject {

View File

@ -26,6 +26,7 @@ import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.ProvisionException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@ -40,7 +41,7 @@ import java.util.List;
@ExtensionPoint
public class ProjectConfigEntry {
public enum Type {
STRING, INT, LONG, BOOLEAN, LIST
STRING, INT, LONG, BOOLEAN, LIST, ARRAY
}
private final String displayName;
@ -145,7 +146,7 @@ public class ProjectConfigEntry {
}), inheritable, description);
}
private ProjectConfigEntry(String displayName, String defaultValue,
public ProjectConfigEntry(String displayName, String defaultValue,
Type type, List<String> permittedValues, boolean inheritable,
String description) {
this.displayName = displayName;
@ -154,6 +155,10 @@ public class ProjectConfigEntry {
this.permittedValues = permittedValues;
this.inheritable = inheritable;
this.description = description;
if (type == Type.ARRAY && inheritable) {
throw new ProvisionException(
"ARRAY doesn't support inheritable values");
}
}
public String getDisplayName() {

View File

@ -27,6 +27,7 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTF
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
@ -147,6 +148,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -891,6 +893,13 @@ public class ReceiveCommits {
projectControl.getProjectState().getConfig()
.getPluginConfig(e.getPluginName())
.getString(e.getExportName());
if (configEntry.getType() == ProjectConfigEntry.Type.ARRAY) {
List<String> l =
Arrays.asList(projectControl.getProjectState()
.getConfig().getPluginConfig(e.getPluginName())
.getStringList(e.getExportName()));
oldValue = Joiner.on("\n").join(l);
}
if ((value == null ? oldValue != null : !value.equals(oldValue)) &&
!configEntry.isEditable(projectControl.getProjectState())) {

View File

@ -33,6 +33,7 @@ import com.google.gerrit.server.extensions.webui.UiActions;
import com.google.gerrit.server.git.TransferConfig;
import com.google.inject.util.Providers;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@ -153,7 +154,11 @@ public class ConfigInfo {
p.configuredValue = configuredValue;
p.inheritedValue = getInheritedValue(project, cfgFactory, e);
} else {
p.value = configuredValue != null ? configuredValue : configEntry.getDefaultValue();
if (configEntry.getType() == ProjectConfigEntry.Type.ARRAY) {
p.values = Arrays.asList(cfg.getStringList(e.getExportName()));
} else {
p.value = configuredValue != null ? configuredValue : configEntry.getDefaultValue();
}
}
Map<String, ConfigParameterInfo> pc = pluginConfig.get(e.getPluginName());
if (pc == null) {
@ -204,5 +209,6 @@ public class ConfigInfo {
public String configuredValue;
public String inheritedValue;
public List<String> permittedValues;
public List<String> values;
}
}

View File

@ -37,6 +37,7 @@ import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.project.CreateProject.Input;
import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
import com.google.gerrit.server.project.PutConfig.ConfigValue;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
@ -65,7 +66,7 @@ public class CreateProject implements RestModifyView<TopLevelResource, Input> {
public InheritableBoolean useContentMerge;
public InheritableBoolean requireChangeId;
public String maxObjectSizeLimit;
public Map<String, Map<String, String>> pluginConfigValues;
public Map<String, Map<String, ConfigValue>> pluginConfigValues;
}
public static interface Factory {

View File

@ -15,6 +15,7 @@
package com.google.gerrit.server.project;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
@ -44,12 +45,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
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 ConfigValue {
public String value;
public List<String> values;
}
public static class Input {
public String description;
public InheritableBoolean useContributorAgreements;
@ -59,7 +65,7 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
public String maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
public Map<String, Map<String, String>> pluginConfigValues;
public Map<String, Map<String, ConfigValue>> pluginConfigValues;
}
private final MetaDataUpdate.User metaDataUpdateFactory;
@ -180,12 +186,12 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
}
private void setPluginConfigValues(ProjectState projectState,
ProjectConfig projectConfig, Map<String, Map<String, String>> pluginConfigValues)
ProjectConfig projectConfig, Map<String, Map<String, ConfigValue>> pluginConfigValues)
throws BadRequestException {
for (Entry<String, Map<String, String>> e : pluginConfigValues.entrySet()) {
for (Entry<String, Map<String, ConfigValue>> e : pluginConfigValues.entrySet()) {
String pluginName = e.getKey();
PluginConfig cfg = projectConfig.getPluginConfig(pluginName);
for (Entry<String, String> v : e.getValue().entrySet()) {
for (Entry<String, ConfigValue> v : e.getValue().entrySet()) {
ProjectConfigEntry projectConfigEntry =
pluginConfigEntries.get(pluginName, v.getKey());
if (projectConfigEntry != null) {
@ -195,32 +201,41 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
continue;
}
String oldValue = cfg.getString(v.getKey());
if (Strings.emptyToNull(v.getValue()) != null) {
if (!v.getValue().equals(oldValue)) {
String value = v.getValue().value;
if (projectConfigEntry.getType() == ProjectConfigEntry.Type.ARRAY) {
List<String> l = Arrays.asList(cfg.getStringList(v.getKey()));
oldValue = Joiner.on("\n").join(l);
value = Joiner.on("\n").join(v.getValue().values);
}
if (Strings.emptyToNull(value) != null) {
if (!value.equals(oldValue)) {
validateProjectConfigEntryIsEditable(projectConfigEntry,
projectState, e.getKey(), pluginName);
try {
switch (projectConfigEntry.getType()) {
case BOOLEAN:
boolean newBooleanValue = Boolean.parseBoolean(v.getValue());
boolean newBooleanValue = Boolean.parseBoolean(value);
cfg.setBoolean(v.getKey(), newBooleanValue);
break;
case INT:
int newIntValue = Integer.parseInt(v.getValue());
int newIntValue = Integer.parseInt(value);
cfg.setInt(v.getKey(), newIntValue);
break;
case LONG:
long newLongValue = Long.parseLong(v.getValue());
long newLongValue = Long.parseLong(value);
cfg.setLong(v.getKey(), newLongValue);
break;
case LIST:
if (!projectConfigEntry.getPermittedValues().contains(v.getValue())) {
if (!projectConfigEntry.getPermittedValues().contains(value)) {
throw new BadRequestException(String.format(
"The value '%s' is not permitted for parameter '%s' of plugin '"
+ pluginName + "'", v.getValue(), v.getKey()));
+ pluginName + "'", value, v.getKey()));
}
case STRING:
cfg.setString(v.getKey(), v.getValue());
cfg.setString(v.getKey(), value);
break;
case ARRAY:
cfg.setStringList(v.getKey(), v.getValue().values);
break;
default:
log.warn(String.format(

View File

@ -38,3 +38,17 @@ java_sources(
srcs = SRCS,
visibility = ['PUBLIC'],
)
java_test(
name = 'sshd_tests',
srcs = glob(
['src/test/java/**/*.java'],
),
deps = [
':sshd',
'//gerrit-server:server',
'//lib:guava',
'//lib:junit',
],
source_under_test = [':sshd'],
)

View File

@ -14,7 +14,9 @@
package com.google.gerrit.sshd.commands;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.ProjectCreationFailedException;
@ -28,6 +30,7 @@ import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.project.CreateProject;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.PutConfig.ConfigValue;
import com.google.gerrit.server.project.SuggestParentCandidates;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
@ -189,21 +192,29 @@ final class CreateProjectCommand extends SshCommand {
}
}
private Map<String, Map<String, String>> parsePluginConfigValues(
@VisibleForTesting
Map<String, Map<String, ConfigValue>> parsePluginConfigValues(
List<String> pluginConfigValues) throws UnloggedFailure {
Map<String, Map<String, String>> m = new HashMap<>();
Map<String, Map<String, ConfigValue>> m = new HashMap<>();
for (String pluginConfigValue : pluginConfigValues) {
String[] s = pluginConfigValue.split("=");
String[] s2 = s[0].split("\\.");
if (s.length != 2 || s2.length != 2) {
throw new UnloggedFailure(1, "Invalid plugin config value '"
+ pluginConfigValue
+ "', expected format '<plugin-name>.<parameter-name>=<value>'");
+ "', expected format '<plugin-name>.<parameter-name>=<value>'"
+ " or '<plugin-name>.<parameter-name>=<value1,value2,...>'");
}
ConfigValue value = new ConfigValue();
String v = s[1];
if (v.contains(",")) {
value.values = Lists.newArrayList(Splitter.on(",").split(v));
} else {
value.value = v;
}
String value = s[1];
String pluginName = s2[0];
String paramName = s2[1];
Map<String, String> l = m.get(pluginName);
Map<String, ConfigValue> l = m.get(pluginName);
if (l == null) {
l = new HashMap<>();
m.put(pluginName, l);

View File

@ -0,0 +1,56 @@
// 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.sshd.commands;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNull;
import com.google.gerrit.server.project.PutConfig.ConfigValue;
import org.junit.Before;
import org.junit.Test;
import java.util.Collections;
import java.util.Map;
public class ProjectConfigParamParserTest {
private CreateProjectCommand cmd;
@Before
public void setUp() {
cmd = new CreateProjectCommand();
}
@Test
public void parseSingleValue() throws Exception {
String in = "a.b=c";
Map<String, Map<String, ConfigValue>> r =
cmd.parsePluginConfigValues(Collections.singletonList(in));
ConfigValue configValue = r.get("a").get("b");
assertEquals("c", configValue.value);
assertNull(configValue.values);
}
@Test
public void parseMultipleValue() throws Exception {
String in = "a.b=c,d,e";
Map<String, Map<String, ConfigValue>> r =
cmd.parsePluginConfigValues(Collections.singletonList(in));
ConfigValue configValue = r.get("a").get("b");
assertArrayEquals(new String[] {"c", "d", "e"}, configValue.values.toArray());
assertNull(configValue.value);
}
}