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