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