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