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.collect.ImmutableMap;
023import java.util.ArrayList;
024import java.util.EnumMap;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Set;
028import java.util.function.Function;
029import java.util.regex.Pattern;
030import java.util.regex.PatternSyntaxException;
031import java.util.stream.Stream;
032import javax.annotation.Nullable;
033import org.apache.commons.lang.StringUtils;
034import org.apache.commons.lang.math.NumberUtils;
035import org.sonar.api.ExtensionPoint;
036import org.sonar.api.Property;
037import org.sonar.api.PropertyType;
038import org.sonar.api.batch.ScannerSide;
039import org.sonar.api.ce.ComputeEngineSide;
040import org.sonar.api.resources.Qualifiers;
041import org.sonar.api.server.ServerSide;
042import org.sonarsource.api.sonarlint.SonarLintSide;
043
044import static com.google.common.base.Preconditions.checkArgument;
045import static java.util.Arrays.asList;
046import static java.util.Arrays.stream;
047import static java.util.Collections.unmodifiableSet;
048import static java.util.Objects.requireNonNull;
049import static org.apache.commons.lang.StringUtils.isBlank;
050import static org.apache.commons.lang.StringUtils.isEmpty;
051import static org.sonar.api.PropertyType.BOOLEAN;
052import static org.sonar.api.PropertyType.FLOAT;
053import static org.sonar.api.PropertyType.INTEGER;
054import static org.sonar.api.PropertyType.LONG;
055import static org.sonar.api.PropertyType.PROPERTY_SET;
056import static org.sonar.api.PropertyType.REGULAR_EXPRESSION;
057import static org.sonar.api.PropertyType.SINGLE_SELECT_LIST;
058
059/**
060 * Declare a plugin property. Values are available at runtime through the component {@link Configuration}.
061 * <br>
062 * It's the programmatic alternative to the annotation {@link org.sonar.api.Property}. It is more
063 * testable and adds new features like sub-categories and ordering.
064 * <br>
065 * Example:
066 * <pre><code>
067 *   public class MyPlugin extends SonarPlugin {
068 *     public List getExtensions() {
069 *       return Arrays.asList(
070 *         PropertyDefinition.builder("sonar.foo").name("Foo").build(),
071 *         PropertyDefinition.builder("sonar.bar").name("Bar").defaultValue("123").type(PropertyType.INTEGER).build()
072 *       );
073 *     }
074 *   }
075 * </code></pre>
076 * <br>
077 * Keys in localization bundles are:
078 * <ul>
079 * <li>{@code property.<key>.name} is the label of the property</li>
080 * <li>{@code property.<key>.description} is the optional description of the property</li>
081 * <li>{@code property.category.<category>} is the category label</li>
082 * <li>{@code property.category.<category>.description} is the category description</li>
083 * <li>{@code property.category.<category>.<subcategory>} is the sub-category label</li>
084 * <li>{@code property.category.<category>.<subcategory>.description} is the sub-category description</li>
085 * </ul>
086 *
087 * @since 3.6
088 */
089@ScannerSide
090@ServerSide
091@ComputeEngineSide
092@SonarLintSide
093@ExtensionPoint
094public final class PropertyDefinition {
095
096  private static final Set<String> SUPPORTED_QUALIFIERS = unmodifiableSet(new LinkedHashSet<>(asList(Qualifiers.PROJECT, Qualifiers.VIEW, Qualifiers.MODULE, Qualifiers.SUBVIEW)));
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    return new EnumMap<>(ImmutableMap.<PropertyType, Function<String, Result>>builder()
181      .put(BOOLEAN, validateBoolean())
182      .put(INTEGER, validateInteger())
183      .put(LONG, validateInteger())
184      .put(FLOAT, validateFloat())
185      .put(REGULAR_EXPRESSION, validateRegexp())
186      .put(SINGLE_SELECT_LIST,
187        aValue -> options.contains(aValue) ? Result.SUCCESS : Result.newError("notInOptions"))
188      .build());
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},
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},
460     *         {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed.
461     */
462    public Builder onQualifiers(List<String> qualifiers) {
463      addQualifiers(this.onQualifiers, qualifiers);
464      this.global = true;
465      return this;
466    }
467
468    /**
469     * The property will be available in the components
470     * with the given qualifiers, but NOT in General Settings.
471     * <br>
472     * For example @{code onlyOnQualifiers(Qualifiers.PROJECT)} allows to configure the
473     * property in Project Settings only.
474     * <br>
475     * See supported constant values in {@link Qualifiers}. By default property is available
476     * only in General Settings.
477     *
478     * @throws IllegalArgumentException only qualifiers {@link Qualifiers#PROJECT PROJECT}, {@link Qualifiers#MODULE MODULE},
479     *         {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed.
480     */
481    public Builder onlyOnQualifiers(String first, String... rest) {
482      addQualifiers(this.onlyOnQualifiers, first, rest);
483      this.global = false;
484      return this;
485    }
486
487    /**
488     * The property will be available in the components
489     * with the given qualifiers, but NOT in General Settings.
490     * <br>
491     * For example @{code onlyOnQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
492     * property in Project Settings only.
493     * <br>
494     * See supported constant values in {@link Qualifiers}. By default property is available
495     * only in General Settings.
496     *
497     * @throws IllegalArgumentException only qualifiers {@link Qualifiers#PROJECT PROJECT}, {@link Qualifiers#MODULE MODULE},
498     *         {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed.
499     */
500    public Builder onlyOnQualifiers(List<String> qualifiers) {
501      addQualifiers(this.onlyOnQualifiers, qualifiers);
502      this.global = false;
503      return this;
504    }
505
506    private static void addQualifiers(List<String> target, String first, String... rest) {
507      Stream.concat(Stream.of(first), stream(rest)).peek(PropertyDefinition.Builder::validateQualifier).forEach(target::add);
508    }
509
510    private static void addQualifiers(List<String> target, List<String> qualifiers) {
511      qualifiers.stream().peek(PropertyDefinition.Builder::validateQualifier).forEach(target::add);
512    }
513
514    private static void validateQualifier(@Nullable String qualifier) {
515      requireNonNull(qualifier, "Qualifier cannot be null");
516      checkArgument(SUPPORTED_QUALIFIERS.contains(qualifier), "Qualifier must be one of %s", SUPPORTED_QUALIFIERS);
517    }
518
519    /**
520     * @see org.sonar.api.config.PropertyDefinition#type()
521     */
522    public Builder type(PropertyType type) {
523      this.type = type;
524      return this;
525    }
526
527    public Builder options(String first, String... rest) {
528      this.options.add(first);
529      stream(rest).forEach(o -> options.add(o));
530      return this;
531    }
532
533    public Builder options(List<String> options) {
534      this.options.addAll(options);
535      return this;
536    }
537
538    public Builder multiValues(boolean multiValues) {
539      this.multiValues = multiValues;
540      return this;
541    }
542
543    /**
544     * @deprecated since 6.1, as it was not used and too complex to maintain.
545     */
546    @Deprecated
547    public Builder propertySetKey(String propertySetKey) {
548      this.propertySetKey = propertySetKey;
549      return this;
550    }
551
552    public Builder fields(PropertyFieldDefinition first, PropertyFieldDefinition... rest) {
553      this.fields.add(first);
554      this.fields.addAll(asList(rest));
555      return this;
556    }
557
558    public Builder fields(List<PropertyFieldDefinition> fields) {
559      this.fields.addAll(fields);
560      return this;
561    }
562
563    public Builder deprecatedKey(String deprecatedKey) {
564      this.deprecatedKey = deprecatedKey;
565      return this;
566    }
567
568    /**
569     * Flag the property as hidden. Hidden properties are not displayed in Settings pages
570     * but allow plugins to benefit from type and default values when calling {@link Settings}.
571     */
572    public Builder hidden() {
573      this.hidden = true;
574      return this;
575    }
576
577    /**
578     * Set the order index in Settings pages. A property with a lower index is displayed
579     * before properties with higher index.
580     */
581    public Builder index(int index) {
582      this.index = index;
583      return this;
584    }
585
586    public PropertyDefinition build() {
587      checkArgument(!isEmpty(key), "Key must be set");
588      fixType(key, type);
589      checkArgument(onQualifiers.isEmpty() || onlyOnQualifiers.isEmpty(), "Cannot define both onQualifiers and onlyOnQualifiers");
590      checkArgument(!hidden || (onQualifiers.isEmpty() && onlyOnQualifiers.isEmpty()), "Cannot be hidden and defining qualifiers on which to display");
591      if (hidden) {
592        global = false;
593      }
594      if (!fields.isEmpty()) {
595        type = PROPERTY_SET;
596      }
597      return new PropertyDefinition(this);
598    }
599
600    private void fixType(String key, PropertyType type) {
601      // Auto-detect passwords and licenses for old versions of plugins that
602      // do not declare property types
603      if (type == PropertyType.STRING) {
604        if (StringUtils.endsWith(key, ".password.secured")) {
605          this.type = PropertyType.PASSWORD;
606        } else if (StringUtils.endsWith(key, ".license.secured")) {
607          this.type = PropertyType.LICENSE;
608        }
609      }
610    }
611  }
612}