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