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