/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.api.config;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.sonar.api.batch.BatchSide;
import org.sonar.api.ExtensionPoint;
import org.sonar.api.Property;
import org.sonar.api.PropertyType;
import org.sonar.api.server.ServerSide;
import org.sonar.api.resources.Qualifiers;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Declare a plugin property. Values are available at runtime through the component {@link Settings}.
*
* It's the programmatic alternative to the annotation {@link org.sonar.api.Property}. It is more
* testable and adds new features like sub-categories and ordering.
*
* Example:
*
* public class MyPlugin extends SonarPlugin {
* public List getExtensions() {
* return Arrays.asList(
* PropertyDefinition.builder("sonar.foo").name("Foo").build(),
* PropertyDefinition.builder("sonar.bar").name("Bar").defaultValue("123").type(PropertyType.INTEGER).build()
* );
* }
* }
*
*
* Keys in localization bundles are:
*
* - {@code property..name} is the label of the property
* - {@code property..description} is the optional description of the property
* - {@code property.category.} is the category label
* - {@code property.category..description} is the category description
* - {@code property.category..} is the sub-category label
* - {@code property.category...description} is the sub-category description
*
*
* @since 3.6
*/
@BatchSide
@ServerSide
@ExtensionPoint
public final class PropertyDefinition {
private String key;
private String defaultValue;
private String name;
private PropertyType type;
private List options;
private String description;
/**
* @see org.sonar.api.config.PropertyDefinition.Builder#category(String)
*/
private String category;
private List qualifiers;
private boolean global;
private boolean multiValues;
private String propertySetKey;
private String deprecatedKey;
private List fields;
/**
* @see org.sonar.api.config.PropertyDefinition.Builder#subCategory(String)
*/
private String subCategory;
private int index;
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.subCategory = builder.subCategory;
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.onQualifiers;
this.qualifiers.addAll(builder.onlyOnQualifiers);
this.index = builder.index;
}
public static Builder builder(String key) {
return new Builder(key);
}
static PropertyDefinition create(Property annotation) {
Builder builder = PropertyDefinition.builder(annotation.key())
.name(annotation.name())
.defaultValue(annotation.defaultValue())
.description(annotation.description())
.category(annotation.category())
.type(annotation.type())
.options(Arrays.asList(annotation.options()))
.multiValues(annotation.multiValues())
.propertySetKey(annotation.propertySetKey())
.fields(PropertyFieldDefinition.create(annotation.fields()))
.deprecatedKey(annotation.deprecatedKey());
List qualifiers = new ArrayList<>();
if (annotation.project()) {
qualifiers.add(Qualifiers.PROJECT);
}
if (annotation.module()) {
qualifiers.add(Qualifiers.MODULE);
}
if (annotation.global()) {
builder.onQualifiers(qualifiers);
} else {
builder.onlyOnQualifiers(qualifiers);
}
return builder.build();
}
public static Result validate(PropertyType type, @Nullable String value, List options) {
if (StringUtils.isNotBlank(value)) {
if (type == PropertyType.BOOLEAN) {
return validateBoolean(value);
} else if (type == PropertyType.INTEGER) {
return validateInteger(value);
} else if (type == PropertyType.FLOAT) {
return validateFloat(value);
} else if (type == PropertyType.SINGLE_SELECT_LIST && !options.contains(value)) {
return Result.newError("notInOptions");
}
}
return Result.SUCCESS;
}
private static Result validateBoolean(String value) {
if (!StringUtils.equalsIgnoreCase(value, "true") && !StringUtils.equalsIgnoreCase(value, "false")) {
return Result.newError("notBoolean");
}
return Result.SUCCESS;
}
private static Result validateInteger(String value) {
if (!NumberUtils.isDigits(value)) {
return Result.newError("notInteger");
}
return Result.SUCCESS;
}
private static Result validateFloat(String value) {
try {
Double.parseDouble(value);
return Result.SUCCESS;
} catch (NumberFormatException e) {
return Result.newError("notFloat");
}
}
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 defaultValue() {
return defaultValue;
}
public String name() {
return name;
}
public PropertyType type() {
return type;
}
/**
* Options for *_LIST types
*
* Options for property of type PropertyType.SINGLE_SELECT_LIST
* For example {"property_1", "property_3", "property_3"}).
*
* Options for property of type PropertyType.METRIC.
* 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 key:REGEXP
, domain:REGEXP
and type:comma_separated__list_of_types
.
* For example key:new_.*
will match any metric which key starts by new_
.
* For example type:INT,FLOAT
will match any metric of type INT
or FLOAT
.
* For example type:NUMERIC
will match any metric of numerictype.
*/
public List options() {
return options;
}
public String description() {
return description;
}
/**
* Category where the property appears in settings pages. By default equal to plugin name.
*/
public String category() {
return category;
}
/**
* Sub-category where property appears in settings pages. By default sub-category is the category.
*/
public String subCategory() {
return subCategory;
}
/**
* Qualifiers that can display this property
*/
public List qualifiers() {
return qualifiers;
}
/**
* Is the property displayed in global settings page ?
*/
public boolean global() {
return global;
}
public boolean multiValues() {
return multiValues;
}
public String propertySetKey() {
return propertySetKey;
}
public List fields() {
return fields;
}
public String deprecatedKey() {
return deprecatedKey;
}
/**
* Order to display properties in Sonar UI. When two properties have the same index then it is sorted by
* lexicographic order of property name.
*/
public int index() {
return index;
}
@Override
public String toString() {
if (StringUtils.isEmpty(propertySetKey)) {
return key;
}
return new StringBuilder().append(propertySetKey).append('|').append(key).toString();
}
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 final String key;
private String name = "";
private String description = "";
private String defaultValue = "";
/**
* @see PropertyDefinition.Builder#category(String)
*/
private String category = "";
/**
* @see PropertyDefinition.Builder#subCategory(String)
*/
private String subCategory = "";
private List onQualifiers = new ArrayList<>();
private List onlyOnQualifiers = new ArrayList<>();
private boolean global = true;
private PropertyType type = PropertyType.STRING;
private List options = new ArrayList<>();
private boolean multiValues = false;
private String propertySetKey = "";
private List fields = new ArrayList<>();
private String deprecatedKey = "";
private boolean hidden = false;
private int index = 999;
private Builder(String key) {
this.key = key;
}
public Builder description(String description) {
this.description = description;
return this;
}
/**
* @see PropertyDefinition#name()
*/
public Builder name(String name) {
this.name = name;
return this;
}
/**
* @see PropertyDefinition#defaultValue()
*/
public Builder defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
/**
* @see PropertyDefinition#category()
*/
public Builder category(String category) {
this.category = category;
return this;
}
/**
* @see PropertyDefinition#subCategory()
*/
public Builder subCategory(String subCategory) {
this.subCategory = subCategory;
return this;
}
/**
* The property will be available in General Settings AND in the components
* with the given qualifiers.
*
* For example @{code onQualifiers(Qualifiers.PROJECT)} allows to configure the
* property in General Settings and in Project Settings.
*
* See supported constant values in {@link Qualifiers}. By default property is available
* only in General Settings.
*/
public Builder onQualifiers(String first, String... rest) {
this.onQualifiers.addAll(Lists.asList(first, rest));
this.global = true;
return this;
}
/**
* The property will be available in General Settings AND in the components
* with the given qualifiers.
*
* For example @{code onQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
* property in General Settings and in Project Settings.
*
* See supported constant values in {@link Qualifiers}. By default property is available
* only in General Settings.
*/
public Builder onQualifiers(List qualifiers) {
this.onQualifiers.addAll(ImmutableList.copyOf(qualifiers));
this.global = true;
return this;
}
/**
* The property will be available in the components
* with the given qualifiers, but NOT in General Settings.
*
* For example @{code onlyOnQualifiers(Qualifiers.PROJECT)} allows to configure the
* property in Project Settings only.
*
* See supported constant values in {@link Qualifiers}. By default property is available
* only in General Settings.
*/
public Builder onlyOnQualifiers(String first, String... rest) {
this.onlyOnQualifiers.addAll(Lists.asList(first, rest));
this.global = false;
return this;
}
/**
* The property will be available in the components
* with the given qualifiers, but NOT in General Settings.
*
* For example @{code onlyOnQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
* property in Project Settings only.
*
* See supported constant values in {@link Qualifiers}. By default property is available
* only in General Settings.
*/
public Builder onlyOnQualifiers(List qualifiers) {
this.onlyOnQualifiers.addAll(ImmutableList.copyOf(qualifiers));
this.global = false;
return this;
}
/**
* @see org.sonar.api.config.PropertyDefinition#type()
*/
public Builder type(PropertyType type) {
this.type = type;
return this;
}
public Builder options(String first, String... rest) {
this.options.addAll(Lists.asList(first, rest));
return this;
}
public Builder options(List 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 first, PropertyFieldDefinition... rest) {
this.fields.addAll(Lists.asList(first, rest));
return this;
}
public Builder fields(List fields) {
this.fields.addAll(ImmutableList.copyOf(fields));
return this;
}
public Builder deprecatedKey(String deprecatedKey) {
this.deprecatedKey = deprecatedKey;
return this;
}
/**
* Flag the property as hidden. Hidden properties are not displayed in Settings pages
* but allow plugins to benefit from type and default values when calling {@link Settings}.
*/
public Builder hidden() {
this.hidden = true;
return this;
}
/**
* Set the order index in Settings pages. A property with a lower index is displayed
* before properties with higher index.
*/
public Builder index(int index) {
this.index = index;
return this;
}
public PropertyDefinition build() {
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Key must be set");
fixType(key, type);
Preconditions.checkArgument(onQualifiers.isEmpty() || onlyOnQualifiers.isEmpty(), "Cannot define both onQualifiers and onlyOnQualifiers");
Preconditions.checkArgument(!hidden || (onQualifiers.isEmpty() && onlyOnQualifiers.isEmpty()), "Cannot be hidden and defining qualifiers on which to display");
if (hidden) {
global = false;
}
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;
}
}
}
}
}