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