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