001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2013 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.config;
021
022import com.google.common.base.Preconditions;
023import com.google.common.base.Strings;
024import com.google.common.collect.ImmutableList;
025import com.google.common.collect.Lists;
026import org.apache.commons.lang.StringUtils;
027import org.apache.commons.lang.math.NumberUtils;
028import org.sonar.api.BatchExtension;
029import org.sonar.api.Property;
030import org.sonar.api.PropertyType;
031import org.sonar.api.ServerExtension;
032import org.sonar.api.resources.Qualifiers;
033
034import javax.annotation.Nullable;
035
036import java.util.Arrays;
037import java.util.List;
038
039import static com.google.common.collect.Lists.newArrayList;
040
041/**
042 * Declare a plugin property. Values are available at runtime through the component {@link Settings}.
043 * <p/>
044 * It's the programmatic alternative to the annotation {@link org.sonar.api.Property}. It is more
045 * testable and adds new features like sub-categories and ordering.
046 * <p/>
047 * Example:
048 * <pre>
049 *   public class MyPlugin extends SonarPlugin {
050 *     public List getExtensions() {
051 *       return Arrays.asList(
052 *         PropertyDefinition.builder("sonar.foo").name("Foo").build(),
053 *         PropertyDefinition.builder("sonar.bar").name("Bar").defaultValue("123").type(PropertyType.INTEGER).build()
054 *       );
055 *     }
056 *   }
057 * </pre>
058 * <p/>
059 * Keys in localization bundles are:
060 * <ul>
061 * <li>{@code property.<key>.name} is the label of the property</li>
062 * <li>{@code property.<key>.description} is the optional description of the property</li>
063 * <li>{@code property.category.<category>} is the category label</li>
064 * <li>{@code property.category.<category>.description} is the category description</li>
065 * <li>{@code property.category.<category>.<subcategory>} is the sub-category label</li>
066 * <li>{@code property.category.<category>.<subcategory>.description} is the sub-category description</li>
067 * </ul>
068 *
069 * @since 3.6
070 */
071public final class PropertyDefinition implements BatchExtension, ServerExtension {
072
073  private String key;
074  private String defaultValue;
075  private String name;
076  private PropertyType type;
077  private List<String> options;
078  private String description;
079  /**
080   * @see org.sonar.api.config.PropertyDefinition.Builder#category(String)
081   */
082  private String category;
083  private List<String> qualifiers;
084  private boolean global;
085  private boolean multiValues;
086  private String propertySetKey;
087  private String deprecatedKey;
088  private List<PropertyFieldDefinition> fields;
089  /**
090   * @see org.sonar.api.config.PropertyDefinition.Builder#subCategory(String)
091   */
092  private String subCategory;
093  private int index;
094
095  private PropertyDefinition(Builder builder) {
096    this.key = builder.key;
097    this.name = builder.name;
098    this.description = builder.description;
099    this.defaultValue = builder.defaultValue;
100    this.category = builder.category;
101    this.subCategory = builder.subCategory;
102    this.global = builder.global;
103    this.type = builder.type;
104    this.options = builder.options;
105    this.multiValues = builder.multiValues;
106    this.propertySetKey = builder.propertySetKey;
107    this.fields = builder.fields;
108    this.deprecatedKey = builder.deprecatedKey;
109    this.qualifiers = builder.onQualifiers;
110    this.qualifiers.addAll(builder.onlyOnQualifiers);
111    this.index = builder.index;
112  }
113
114  public static Builder builder(String key) {
115    return new Builder(key);
116  }
117
118  static PropertyDefinition create(Property annotation) {
119    Builder builder = PropertyDefinition.builder(annotation.key())
120      .name(annotation.name())
121      .defaultValue(annotation.defaultValue())
122      .description(annotation.description())
123      .category(annotation.category())
124      .type(annotation.type())
125      .options(Arrays.asList(annotation.options()))
126      .multiValues(annotation.multiValues())
127      .propertySetKey(annotation.propertySetKey())
128      .fields(PropertyFieldDefinition.create(annotation.fields()))
129      .deprecatedKey(annotation.deprecatedKey());
130    List<String> qualifiers = newArrayList();
131    if (annotation.project()) {
132      qualifiers.add(Qualifiers.PROJECT);
133    }
134    if (annotation.module()) {
135      qualifiers.add(Qualifiers.MODULE);
136    }
137    if (annotation.global()) {
138      builder.onQualifiers(qualifiers);
139    } else {
140      builder.onlyOnQualifiers(qualifiers);
141    }
142    return builder.build();
143  }
144
145  public static Result validate(PropertyType type, @Nullable String value, List<String> options) {
146    if (StringUtils.isNotBlank(value)) {
147      if (type == PropertyType.BOOLEAN) {
148        return validateBoolean(value);
149      } else if (type == PropertyType.INTEGER) {
150        return validateInteger(value);
151      } else if (type == PropertyType.FLOAT) {
152        return validateFloat(value);
153      } else if (type == PropertyType.SINGLE_SELECT_LIST && !options.contains(value)) {
154        return Result.newError("notInOptions");
155      }
156    }
157    return Result.SUCCESS;
158  }
159
160  private static Result validateBoolean(String value) {
161    if (!StringUtils.equalsIgnoreCase(value, "true") && !StringUtils.equalsIgnoreCase(value, "false")) {
162      return Result.newError("notBoolean");
163    }
164    return Result.SUCCESS;
165  }
166
167  private static Result validateInteger(String value) {
168    if (!NumberUtils.isDigits(value)) {
169      return Result.newError("notInteger");
170    }
171    return Result.SUCCESS;
172  }
173
174  private static Result validateFloat(String value) {
175    try {
176      Double.parseDouble(value);
177      return Result.SUCCESS;
178    } catch (NumberFormatException e) {
179      return Result.newError("notFloat");
180    }
181  }
182
183  public Result validate(@Nullable String value) {
184    return validate(type, value, options);
185  }
186
187  /**
188   * Unique key within all plugins. It's recommended to prefix the key by 'sonar.' and the plugin name. Examples :
189   * 'sonar.cobertura.reportPath' and 'sonar.cpd.minimumTokens'.
190   */
191  public String key() {
192    return key;
193  }
194
195  public String defaultValue() {
196    return defaultValue;
197  }
198
199  public String name() {
200    return name;
201  }
202
203  public PropertyType type() {
204    return type;
205  }
206
207  /**
208   * Options for *_LIST types
209   * <p/>
210   * Options for property of type PropertyType.SINGLE_SELECT_LIST</code>
211   * For example {"property_1", "property_3", "property_3"}).
212   * <p/>
213   * Options for property of type PropertyType.METRIC</code>.
214   * If no option is specified, any metric will match.
215   * If options are specified, all must match for the metric to be displayed.
216   * Three types of filter are supported <code>key:REGEXP</code>, <code>domain:REGEXP</code> and <code>type:comma_separated__list_of_types</code>.
217   * For example <code>key:new_.*</code> will match any metric which key starts by <code>new_</code>.
218   * For example <code>type:INT,FLOAT</code> will match any metric of type <code>INT</code> or <code>FLOAT</code>.
219   * For example <code>type:NUMERIC</code> will match any metric of numerictype.
220   */
221  public List<String> options() {
222    return options;
223  }
224
225  public String description() {
226    return description;
227  }
228
229  /**
230   * Category where the property appears in settings pages. By default equal to plugin name.
231   */
232  public String category() {
233    return category;
234  }
235
236  /**
237   * Sub-category where property appears in settings pages. By default sub-category is the category.
238   */
239  public String subCategory() {
240    return subCategory;
241  }
242
243  /**
244   * Qualifiers that can display this property
245   */
246  public List<String> qualifiers() {
247    return qualifiers;
248  }
249
250  /**
251   * Is the property displayed in global settings page ?
252   */
253  public boolean global() {
254    return global;
255  }
256
257  public boolean multiValues() {
258    return multiValues;
259  }
260
261  public String propertySetKey() {
262    return propertySetKey;
263  }
264
265  public List<PropertyFieldDefinition> fields() {
266    return fields;
267  }
268
269  public String deprecatedKey() {
270    return deprecatedKey;
271  }
272
273  /**
274   * Order to display properties in Sonar UI. When two properties have the same index then it is sorted by
275   * lexicographic order of property name.
276   */
277  public int index() {
278    return index;
279  }
280
281  @Override
282  public String toString() {
283    if (StringUtils.isEmpty(propertySetKey)) {
284      return key;
285    }
286    return new StringBuilder().append(propertySetKey).append('|').append(key).toString();
287  }
288
289  public static final class Result {
290    private static final Result SUCCESS = new Result(null);
291    private String errorKey = null;
292
293    @Nullable
294    private Result(@Nullable String errorKey) {
295      this.errorKey = errorKey;
296    }
297
298    private static Result newError(String key) {
299      return new Result(key);
300    }
301
302    public boolean isValid() {
303      return StringUtils.isBlank(errorKey);
304    }
305
306    @Nullable
307    public String getErrorKey() {
308      return errorKey;
309    }
310  }
311
312  public static class Builder {
313    private final String key;
314    private String name = "";
315    private String description = "";
316    private String defaultValue = "";
317    /**
318     * @see PropertyDefinition.Builder#category(String)
319     */
320    private String category = "";
321    /**
322     * @see PropertyDefinition.Builder#subCategory(String)
323     */
324    private String subCategory = "";
325    private List<String> onQualifiers = newArrayList();
326    private List<String> onlyOnQualifiers = newArrayList();
327    private boolean global = true;
328    private PropertyType type = PropertyType.STRING;
329    private List<String> options = newArrayList();
330    private boolean multiValues = false;
331    private String propertySetKey = "";
332    private List<PropertyFieldDefinition> fields = newArrayList();
333    private String deprecatedKey = "";
334    private boolean hidden = false;
335    private int index = 999;
336
337    private Builder(String key) {
338      this.key = key;
339    }
340
341    public Builder description(String description) {
342      this.description = description;
343      return this;
344    }
345
346    /**
347     * @see PropertyDefinition#name()
348     */
349    public Builder name(String name) {
350      this.name = name;
351      return this;
352    }
353
354    /**
355     * @see PropertyDefinition#defaultValue()
356     */
357    public Builder defaultValue(String defaultValue) {
358      this.defaultValue = defaultValue;
359      return this;
360    }
361
362
363    /**
364     * @see PropertyDefinition#category()
365     */
366    public Builder category(String category) {
367      this.category = category;
368      return this;
369    }
370
371    /**
372     * @see PropertyDefinition#subCategory()
373     */
374    public Builder subCategory(String subCategory) {
375      this.subCategory = subCategory;
376      return this;
377    }
378
379    /**
380     * The property will be available in General Settings AND in the components
381     * with the given qualifiers.
382     * <p/>
383     * For example @{code onQualifiers(Qualifiers.PROJECT)} allows to configure the
384     * property in General Settings and in Project Settings.
385     * <p/>
386     * See supported constant values in {@link Qualifiers}. By default property is available
387     * only in General Settings.
388     */
389    public Builder onQualifiers(String first, String... rest) {
390      this.onQualifiers.addAll(Lists.asList(first, rest));
391      this.global = true;
392      return this;
393    }
394
395    /**
396     * The property will be available in General Settings AND in the components
397     * with the given qualifiers.
398     * <p/>
399     * For example @{code onQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
400     * property in General Settings and in Project Settings.
401     * <p/>
402     * See supported constant values in {@link Qualifiers}. By default property is available
403     * only in General Settings.
404     */
405    public Builder onQualifiers(List<String> qualifiers) {
406      this.onQualifiers.addAll(ImmutableList.copyOf(qualifiers));
407      this.global = true;
408      return this;
409    }
410
411    /**
412     * The property will be available in the components
413     * with the given qualifiers, but NOT in General Settings.
414     * <p/>
415     * For example @{code onlyOnQualifiers(Qualifiers.PROJECT)} allows to configure the
416     * property in Project Settings only.
417     * <p/>
418     * See supported constant values in {@link Qualifiers}. By default property is available
419     * only in General Settings.
420     */
421    public Builder onlyOnQualifiers(String first, String... rest) {
422      this.onlyOnQualifiers.addAll(Lists.asList(first, rest));
423      this.global = false;
424      return this;
425    }
426
427    /**
428     * The property will be available in the components
429     * with the given qualifiers, but NOT in General Settings.
430     * <p/>
431     * For example @{code onlyOnQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
432     * property in Project Settings only.
433     * <p/>
434     * See supported constant values in {@link Qualifiers}. By default property is available
435     * only in General Settings.
436     */
437    public Builder onlyOnQualifiers(List<String> qualifiers) {
438      this.onlyOnQualifiers.addAll(ImmutableList.copyOf(qualifiers));
439      this.global = false;
440      return this;
441    }
442
443    /**
444     * @see org.sonar.api.config.PropertyDefinition#type()
445     */
446    public Builder type(PropertyType type) {
447      this.type = type;
448      return this;
449    }
450
451    public Builder options(String first, String... rest) {
452      this.options.addAll(Lists.asList(first, rest));
453      return this;
454    }
455
456    public Builder options(List<String> options) {
457      this.options.addAll(ImmutableList.copyOf(options));
458      return this;
459    }
460
461    public Builder multiValues(boolean multiValues) {
462      this.multiValues = multiValues;
463      return this;
464    }
465
466    public Builder propertySetKey(String propertySetKey) {
467      this.propertySetKey = propertySetKey;
468      return this;
469    }
470
471    public Builder fields(PropertyFieldDefinition first, PropertyFieldDefinition... rest) {
472      this.fields.addAll(Lists.asList(first, rest));
473      return this;
474    }
475
476    public Builder fields(List<PropertyFieldDefinition> fields) {
477      this.fields.addAll(ImmutableList.copyOf(fields));
478      return this;
479    }
480
481    public Builder deprecatedKey(String deprecatedKey) {
482      this.deprecatedKey = deprecatedKey;
483      return this;
484    }
485
486    /**
487     * Flag the property as hidden. Hidden properties are not displayed in Settings pages
488     * but allow plugins to benefit from type and default values when calling {@link Settings}.
489     */
490    public Builder hidden() {
491      this.hidden = true;
492      return this;
493    }
494
495    /**
496     * Set the order index in Settings pages. A property with a lower index is displayed
497     * before properties with higher index.
498     */
499    public Builder index(int index) {
500      this.index = index;
501      return this;
502    }
503
504    public PropertyDefinition build() {
505      Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Key must be set");
506      fixType(key, type);
507      Preconditions.checkArgument(onQualifiers.isEmpty() || onlyOnQualifiers.isEmpty(), "Cannot define both onQualifiers and onlyOnQualifiers");
508      Preconditions.checkArgument(!hidden || (onQualifiers.isEmpty() && onlyOnQualifiers.isEmpty()), "Cannot be hidden and defining qualifiers on which to display");
509      if (hidden) {
510        global = false;
511      }
512      return new PropertyDefinition(this);
513    }
514
515    private void fixType(String key, PropertyType type) {
516      // Auto-detect passwords and licenses for old versions of plugins that
517      // do not declare property types
518      if (type == PropertyType.STRING) {
519        if (StringUtils.endsWith(key, ".password.secured")) {
520          this.type = PropertyType.PASSWORD;
521        } else if (StringUtils.endsWith(key, ".license.secured")) {
522          this.type = PropertyType.LICENSE;
523        }
524      }
525    }
526  }
527}