aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-plugin-api/src/main/java/org/sonar/api/config
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@gmail.com>2013-03-26 18:12:39 +0100
committerJulien Lancelot <julien.lancelot@gmail.com>2013-03-26 18:13:15 +0100
commit4689f87a0ad833bb9522fbde1896b3ab5fcb52c8 (patch)
treefc8be79d3fa5d36b2c1ee0d2fed688bcaddc0607 /sonar-plugin-api/src/main/java/org/sonar/api/config
parent996c779bd2b0336bbb3d207d0fb3c5bcaab18fca (diff)
downloadsonarqube-4689f87a0ad833bb9522fbde1896b3ab5fcb52c8.tar.gz
sonarqube-4689f87a0ad833bb9522fbde1896b3ab5fcb52c8.zip
SONAR-3891 Update PropertyDefinition in order to be used directly to define properties (with builder pattern)
Diffstat (limited to 'sonar-plugin-api/src/main/java/org/sonar/api/config')
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java406
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java31
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyFieldDefinition.java109
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java4
4 files changed, 402 insertions, 148 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java
index 083b598d4da..be4ee0527e2 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java
@@ -19,121 +19,95 @@
*/
package org.sonar.api.config;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
+import org.sonar.api.BatchComponent;
import org.sonar.api.Property;
import org.sonar.api.PropertyType;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.resources.Qualifiers;
import javax.annotation.Nullable;
import java.util.List;
+import static com.google.common.collect.Lists.newArrayList;
+
/**
+ * Property value can be set in different ways :
+ * <ul>
+ * <li>System property</li>
+ * <li>Maven command-line (-Dfoo=bar)</li>
+ * <li>Maven pom.xml (element <properties>)</li>
+ * <li>Maven settings.xml</li>
+ * <li>Sonar web interface</li>
+ * </ul>
+ * <p/>
+ * Value is accessible in batch extensions via the Configuration object of class <code>org.sonar.api.resources.Project</code>
+ * (see method <code>getConfiguration()</code>).
+ * <p/>
+ * <p><strong>Must be used in <code>org.sonar.api.Plugin</code> classes only.</strong></p>
+ *
* @since 3.0
*/
-public final class PropertyDefinition {
-
- public static final class Result {
- private static final Result SUCCESS = new Result(null);
-
- private String errorKey = null;
-
- private static Result newError(String key) {
- return new Result(key);
- }
-
- @Nullable
- private Result(@Nullable String errorKey) {
- this.errorKey = errorKey;
- }
-
- public boolean isValid() {
- return StringUtils.isBlank(errorKey);
- }
-
- @Nullable
- public String getErrorKey() {
- return errorKey;
- }
- }
-
- private final String key;
- private final String defaultValue;
- private final String name;
- private final PropertyType type;
- private final String[] options;
- private final String description;
- private final String category;
- private final boolean onProject;
- private final boolean onModule;
- private final boolean isGlobal;
- private final boolean multiValues;
- private final String propertySetKey;
- private final String deprecatedKey;
- private final List<PropertyFieldDefinition> fields;
-
- private PropertyDefinition(Property annotation) {
- this.key = annotation.key();
- this.name = annotation.name();
- this.defaultValue = annotation.defaultValue();
- this.description = annotation.description();
- this.isGlobal = annotation.global();
- this.onProject = annotation.project();
- this.onModule = annotation.module();
- this.category = annotation.category();
- this.type = fixType(annotation.key(), annotation.type());
- this.options = annotation.options();
- this.multiValues = annotation.multiValues();
- this.propertySetKey = annotation.propertySetKey();
- this.fields = ImmutableList.copyOf(PropertyFieldDefinition.create(annotation.fields()));
- this.deprecatedKey = annotation.deprecatedKey();
- }
-
- private PropertyDefinition(String key, PropertyType type, String[] options) {
- this.key = key;
- this.name = null;
- this.defaultValue = null;
- this.description = null;
- this.isGlobal = true;
- this.onProject = false;
- this.onModule = false;
- this.category = null;
- this.type = type;
- this.options = options;
- this.multiValues = false;
- this.propertySetKey = null;
- this.fields = null;
- this.deprecatedKey = null;
- }
-
- private static PropertyType fixType(String key, PropertyType type) {
- // Auto-detect passwords and licenses for old versions of plugins that
- // do not declare property types
- if (type == PropertyType.STRING) {
- if (StringUtils.endsWith(key, ".password.secured")) {
- return PropertyType.PASSWORD;
- } else if (StringUtils.endsWith(key, ".license.secured")) {
- return PropertyType.LICENSE;
- }
- }
- return type;
+public final class PropertyDefinition implements BatchComponent, ServerComponent {
+
+ private String key;
+ private String defaultValue;
+ private String name;
+ private PropertyType type;
+ private List<String> options;
+ private String description;
+ private String category;
+ private List<String> qualifiers;
+ private boolean global;
+ private boolean multiValues;
+ private String propertySetKey;
+ private String deprecatedKey;
+ private List<PropertyFieldDefinition> fields;
+
+ private PropertyDefinition(Builder builder) {
+ this.key = builder.key;
+ this.name = builder.name;
+ this.description = builder.description;
+ this.defaultValue = builder.defaultValue;
+ this.category = builder.category;
+ this.global = builder.global;
+ this.type = builder.type;
+ this.options = builder.options;
+ this.multiValues = builder.multiValues;
+ this.propertySetKey = builder.propertySetKey;
+ this.fields = builder.fields;
+ this.deprecatedKey = builder.deprecatedKey;
+ this.qualifiers = builder.qualifiers;
}
- public static PropertyDefinition create(Property annotation) {
- return new PropertyDefinition(annotation);
+ public static Builder build(String key) {
+ return new Builder(key);
}
- public static PropertyDefinition create(String key, PropertyType type, String[] options) {
- return new PropertyDefinition(key, type, options);
+ static PropertyDefinition create(Property annotation) {
+ return PropertyDefinition.build(annotation.key())
+ .name(annotation.name())
+ .defaultValue(annotation.defaultValue())
+ .description(annotation.description())
+ .global(annotation.global())
+ .project(annotation.project())
+ .module(annotation.module())
+ .category(annotation.category())
+ .type(annotation.type())
+ .options(annotation.options())
+ .multiValues(annotation.multiValues())
+ .propertySetKey(annotation.propertySetKey())
+ .fields(PropertyFieldDefinition.create(annotation.fields()))
+ .deprecatedKey(annotation.deprecatedKey())
+ .build();
}
- public Result validate(@Nullable String value) {
- return validate(type, value, options);
- }
-
- static Result validate(PropertyType type, @Nullable String value, String[] options) {
+ public static Result validate(PropertyType type, @Nullable String value, List<String> options) {
if (StringUtils.isNotBlank(value)) {
if (type == PropertyType.BOOLEAN) {
if (!StringUtils.equalsIgnoreCase(value, "true") && !StringUtils.equalsIgnoreCase(value, "false")) {
@@ -150,7 +124,7 @@ public final class PropertyDefinition {
return Result.newError("notFloat");
}
} else if (type == PropertyType.SINGLE_SELECT_LIST) {
- if (!ArrayUtils.contains(options, value)) {
+ if (!options.contains(value)) {
return Result.newError("notInOptions");
}
}
@@ -158,71 +132,273 @@ public final class PropertyDefinition {
return Result.SUCCESS;
}
- public String getKey() {
+ public Result validate(@Nullable String value) {
+ return validate(type, value, options);
+ }
+
+ /**
+ * Unique key within all plugins. It's recommended to prefix the key by 'sonar.' and the plugin name. Examples :
+ * 'sonar.cobertura.reportPath' and 'sonar.cpd.minimumTokens'.
+ */
+ public String key() {
return key;
}
- public String getDefaultValue() {
+ public String defaultValue() {
return defaultValue;
}
- public String getName() {
+ public String name() {
return name;
}
- public PropertyType getType() {
+ public PropertyType type() {
return type;
}
- public String[] getOptions() {
- return options.clone();
+ /**
+ * Options for *_LIST types
+ * <p/>
+ * Options for property of type PropertyType.SINGLE_SELECT_LIST</code>
+ * For example {"property_1", "property_3", "property_3"}).
+ * <p/>
+ * Options for property of type PropertyType.METRIC</code>.
+ * If no option is specified, any metric will match.
+ * If options are specified, all must match for the metric to be displayed.
+ * Three types of filter are supported <code>key:REGEXP</code>, <code>domain:REGEXP</code> and <code>type:comma_separated__list_of_types</code>.
+ * For example <code>key:new_.*</code> will match any metric which key starts by <code>new_</code>.
+ * For example <code>type:INT,FLOAT</code> will match any metric of type <code>INT</code> or <code>FLOAT</code>.
+ * For example <code>type:NUMERIC</code> will match any metric of numerictype.
+ */
+ public List<String> options() {
+ return options;
}
- public String getDescription() {
+ public String description() {
return description;
}
- public String getCategory() {
+ public String category() {
return category;
}
- public boolean isOnProject() {
- return onProject;
+ /**
+ * Is the property displayed in project settings page ?
+ */
+ public boolean project() {
+ return qualifiers.contains(Qualifiers.PROJECT);
+ }
+
+ /**
+ * Is the property displayed in module settings page ? A module is a maven sub-project.
+ */
+ public boolean module() {
+ return qualifiers.contains(Qualifiers.MODULE);
}
- public boolean isOnModule() {
- return onModule;
+ /**
+ * Qualifiers that can display this property
+ *
+ * @since 3.6
+ */
+ public List<String> qualifiers() {
+ return qualifiers;
}
- public boolean isGlobal() {
- return isGlobal;
+ /**
+ * Is the property displayed in global settings page ?
+ */
+ public boolean global() {
+ return global;
}
/**
* @since 3.3
*/
- public boolean isMultiValues() {
+ public boolean multiValues() {
return multiValues;
}
/**
* @since 3.3
*/
- public String getPropertySetKey() {
+ public String propertySetKey() {
return propertySetKey;
}
/**
* @since 3.3
*/
- public List<PropertyFieldDefinition> getFields() {
+ public List<PropertyFieldDefinition> fields() {
return fields;
}
/**
* @since 3.4
*/
- public String getDeprecatedKey() {
+ public String deprecatedKey() {
return deprecatedKey;
}
+
+ public static final class Result {
+ private static final Result SUCCESS = new Result(null);
+ private String errorKey = null;
+
+ @Nullable
+ private Result(@Nullable String errorKey) {
+ this.errorKey = errorKey;
+ }
+
+ private static Result newError(String key) {
+ return new Result(key);
+ }
+
+ public boolean isValid() {
+ return StringUtils.isBlank(errorKey);
+ }
+
+ @Nullable
+ public String getErrorKey() {
+ return errorKey;
+ }
+ }
+
+ public static class Builder {
+ private String key;
+ private String name;
+ private String description;
+ private String defaultValue;
+ private String category;
+ private List<String> qualifiers;
+ private boolean global;
+ private PropertyType type;
+ private List<String> options;
+ private boolean multiValues;
+ private String propertySetKey;
+ private List<PropertyFieldDefinition> fields;
+ private String deprecatedKey;
+
+ private Builder(String key) {
+ this.key = key;
+ this.name = "";
+ this.description = "";
+ this.defaultValue = "";
+ this.category = "";
+ this.propertySetKey = "";
+ this.deprecatedKey = "";
+ this.global = true;
+ this.type = PropertyType.STRING;
+ this.qualifiers = newArrayList();
+ this.options = newArrayList();
+ this.fields = newArrayList();
+ }
+
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder defaultValue(String defaultValue) {
+ this.defaultValue = defaultValue;
+ return this;
+ }
+
+ public Builder category(String category) {
+ this.category = category;
+ return this;
+ }
+
+ public Builder qualifiers(String... qualifiers) {
+ this.qualifiers.addAll(newArrayList(qualifiers));
+ return this;
+ }
+
+ public Builder project(boolean displayOnProject) {
+ if (displayOnProject) {
+ this.qualifiers.add(Qualifiers.PROJECT);
+ } else {
+ this.qualifiers.remove(Qualifiers.PROJECT);
+ }
+ return this;
+ }
+
+ public Builder module(boolean displayOnModule) {
+ if (displayOnModule) {
+ this.qualifiers.add(Qualifiers.MODULE);
+ } else {
+ this.qualifiers.remove(Qualifiers.MODULE);
+ }
+ return this;
+ }
+
+ public Builder global(boolean global) {
+ this.global = global;
+ return this;
+ }
+
+ public Builder type(PropertyType type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder options(String... options) {
+ this.options.addAll(ImmutableList.copyOf(options));
+ return this;
+ }
+
+ public Builder options(List<String> options) {
+ this.options.addAll(ImmutableList.copyOf(options));
+ return this;
+ }
+
+ public Builder multiValues(boolean multiValues) {
+ this.multiValues = multiValues;
+ return this;
+ }
+
+ public Builder propertySetKey(String propertySetKey) {
+ this.propertySetKey = propertySetKey;
+ return this;
+ }
+
+ public Builder fields(PropertyFieldDefinition... fields) {
+ this.fields.addAll(ImmutableList.copyOf(fields));
+ return this;
+ }
+
+ public Builder fields(List<PropertyFieldDefinition> fields) {
+ this.fields.addAll(ImmutableList.copyOf(fields));
+ return this;
+ }
+
+ public Builder deprecatedKey(String deprecatedKey) {
+ this.deprecatedKey = deprecatedKey;
+ return this;
+ }
+
+ public PropertyDefinition build() {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Key must be set");
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Name must be set");
+ fixType(key, type);
+ return new PropertyDefinition(this);
+ }
+
+ private void fixType(String key, PropertyType type) {
+ // Auto-detect passwords and licenses for old versions of plugins that
+ // do not declare property types
+ if (type == PropertyType.STRING) {
+ if (StringUtils.endsWith(key, ".password.secured")) {
+ this.type = PropertyType.PASSWORD;
+ } else if (StringUtils.endsWith(key, ".license.secured")) {
+ this.type = PropertyType.LICENSE;
+ }
+ }
+ }
+ }
+
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java
index 58820d07115..e7e2fb96c64 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java
@@ -69,6 +69,15 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
}
public PropertyDefinitions addComponent(Object component, String defaultCategory) {
+ addComponentFromAnnotationPropety(component, defaultCategory);
+ if (component instanceof PropertyDefinition) {
+ PropertyDefinition propertyDefinition = (PropertyDefinition) component;
+ add(propertyDefinition, defaultCategory);
+ }
+ return this;
+ }
+
+ private PropertyDefinitions addComponentFromAnnotationPropety(Object component, String defaultCategory){
Properties annotations = AnnotationUtils.getAnnotation(component, Properties.class);
if (annotations != null) {
for (Property property : annotations.value()) {
@@ -88,11 +97,11 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
}
private PropertyDefinitions add(PropertyDefinition definition, String defaultCategory) {
- if (!definitions.containsKey(definition.getKey())) {
- definitions.put(definition.getKey(), definition);
- categories.put(definition.getKey(), StringUtils.defaultIfBlank(definition.getCategory(), defaultCategory));
- if (!Strings.isNullOrEmpty(definition.getDeprecatedKey()) && !definition.getDeprecatedKey().equals(definition.getKey())) {
- deprecatedKeys.put(definition.getDeprecatedKey(), definition.getKey());
+ if (!definitions.containsKey(definition.key())) {
+ definitions.put(definition.key(), definition);
+ categories.put(definition.key(), StringUtils.defaultIfBlank(definition.category(), defaultCategory));
+ if (!Strings.isNullOrEmpty(definition.deprecatedKey()) && !definition.deprecatedKey().equals(definition.key())) {
+ deprecatedKeys.put(definition.deprecatedKey(), definition.key());
}
}
return this;
@@ -114,19 +123,19 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
GLOBAL {
@Override
boolean accept(PropertyDefinition propertyDefinition) {
- return propertyDefinition.isGlobal();
+ return propertyDefinition.global();
}
},
PROJECT {
@Override
boolean accept(PropertyDefinition propertyDefinition) {
- return propertyDefinition.isOnProject();
+ return propertyDefinition.project();
}
},
MODULE {
@Override
boolean accept(PropertyDefinition propertyDefinition) {
- return propertyDefinition.isOnModule();
+ return propertyDefinition.module();
}
};
@@ -138,7 +147,7 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
for (PropertyDefinition definition : getAll()) {
if (filter.accept(definition)) {
- byCategory.put(getCategory(definition.getKey()), definition);
+ byCategory.put(getCategory(definition.key()), definition);
}
}
@@ -171,7 +180,7 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
if (def == null) {
return null;
}
- return StringUtils.defaultIfEmpty(def.getDefaultValue(), null);
+ return StringUtils.defaultIfEmpty(def.defaultValue(), null);
}
public String getCategory(String key) {
@@ -191,6 +200,6 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
if (def == null) {
return null;
}
- return StringUtils.defaultIfEmpty(def.getDeprecatedKey(), null);
+ return StringUtils.defaultIfEmpty(def.deprecatedKey(), null);
}
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyFieldDefinition.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyFieldDefinition.java
index 05fbea9cf7e..b71ff860ec9 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyFieldDefinition.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyFieldDefinition.java
@@ -19,7 +19,9 @@
*/
package org.sonar.api.config;
-import com.google.common.collect.Lists;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import org.sonar.api.PropertyField;
import org.sonar.api.PropertyType;
@@ -27,6 +29,8 @@ import javax.annotation.Nullable;
import java.util.List;
+import static com.google.common.collect.Lists.newArrayList;
+
/**
* @since 3.3
*/
@@ -36,50 +40,115 @@ public final class PropertyFieldDefinition {
private final String description;
private final int indicativeSize;
private final PropertyType type;
- private final String[] options;
-
- private PropertyFieldDefinition(PropertyField annotation) {
- this.key = annotation.key();
- this.name = annotation.name();
- this.description = annotation.description();
- this.indicativeSize = annotation.indicativeSize();
- this.type = annotation.type();
- this.options = annotation.options();
+ private final List<String> options;
+
+ private PropertyFieldDefinition(Builder builder) {
+ this.key = builder.key;
+ this.name = builder.name;
+ this.description = builder.description;
+ this.indicativeSize = builder.indicativeSize;
+ this.type = builder.type;
+ this.options = builder.options;
}
- public static List<PropertyFieldDefinition> create(PropertyField[] fields) {
- List<PropertyFieldDefinition> definitions = Lists.newArrayList();
+ static List<PropertyFieldDefinition> create(PropertyField[] fields) {
+ List<PropertyFieldDefinition> definitions = newArrayList();
for (PropertyField field : fields) {
- definitions.add(new PropertyFieldDefinition(field));
+ definitions.add(PropertyFieldDefinition.build(field.key())
+ .name(field.name())
+ .description(field.description())
+ .indicativeSize(field.indicativeSize())
+ .type(field.type())
+ .options(field.options())
+ .build()
+ );
}
return definitions;
}
- public String getKey() {
+ public static Builder build(String key) {
+ return new Builder(key);
+ }
+
+ public String key() {
return key;
}
- public String getName() {
+ public String name() {
return name;
}
- public String getDescription() {
+ public String description() {
return description;
}
- public int getIndicativeSize() {
+ public int indicativeSize() {
return indicativeSize;
}
- public PropertyType getType() {
+ public PropertyType type() {
return type;
}
- public String[] getOptions() {
- return options.clone();
+ public List<String> options() {
+ return options;
}
public PropertyDefinition.Result validate(@Nullable String value) {
return PropertyDefinition.validate(type, value, options);
}
+
+ public static class Builder {
+ private String key;
+ private String name;
+ private String description;
+ private int indicativeSize;
+ private PropertyType type;
+ private List<String> options;
+
+ private Builder(String key) {
+ this.key = key;
+ this.name = "";
+ this.description = "";
+ this.indicativeSize = 20;
+ this.type = PropertyType.STRING;
+ this.options = newArrayList();
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder indicativeSize(int indicativeSize) {
+ this.indicativeSize = indicativeSize;
+ return this;
+ }
+
+ public Builder type(PropertyType type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder options(String... options) {
+ this.options.addAll(ImmutableList.copyOf(options));
+ return this;
+ }
+
+ public Builder options(List<String> options) {
+ this.options.addAll(ImmutableList.copyOf(options));
+ return this;
+ }
+
+ public PropertyFieldDefinition build() {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Key must be set");
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Name must be set");
+ return new PropertyFieldDefinition(this);
+ }
+ }
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java
index 1a9c2ffc975..d51c5efcfac 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java
@@ -191,7 +191,7 @@ public class Settings implements BatchComponent, ServerComponent {
*/
public String[] getStringArray(String key) {
PropertyDefinition property = getDefinitions().get(key);
- if ((null != property) && (property.isMultiValues())) {
+ if ((null != property) && (property.multiValues())) {
String value = getString(key);
if (value == null) {
return ArrayUtils.EMPTY_STRING_ARRAY;
@@ -259,7 +259,7 @@ public class Settings implements BatchComponent, ServerComponent {
public Settings setProperty(String key, @Nullable String[] values) {
PropertyDefinition property = getDefinitions().get(key);
- if ((null == property) || (!property.isMultiValues())) {
+ if ((null == property) || (!property.multiValues())) {
throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
}