001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2013 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      public static final class Result {
282        private static final Result SUCCESS = new Result(null);
283        private String errorKey = null;
284    
285        @Nullable
286        private Result(@Nullable String errorKey) {
287          this.errorKey = errorKey;
288        }
289    
290        private static Result newError(String key) {
291          return new Result(key);
292        }
293    
294        public boolean isValid() {
295          return StringUtils.isBlank(errorKey);
296        }
297    
298        @Nullable
299        public String getErrorKey() {
300          return errorKey;
301        }
302      }
303    
304      public static class Builder {
305        private final String key;
306        private String name = "";
307        private String description = "";
308        private String defaultValue = "";
309        /**
310         * @see PropertyDefinition.Builder#category(String)
311         */
312        private String category = "";
313        /**
314         * @see PropertyDefinition.Builder#subCategory(String)
315         */
316        private String subCategory = "";
317        private List<String> onQualifiers = newArrayList();
318        private List<String> onlyOnQualifiers = newArrayList();
319        private boolean global = true;
320        private PropertyType type = PropertyType.STRING;
321        private List<String> options = newArrayList();
322        private boolean multiValues = false;
323        private String propertySetKey = "";
324        private List<PropertyFieldDefinition> fields = newArrayList();
325        private String deprecatedKey = "";
326        private boolean hidden = false;
327        private int index = 999;
328    
329        private Builder(String key) {
330          this.key = key;
331        }
332    
333        public Builder description(String description) {
334          this.description = description;
335          return this;
336        }
337    
338        /**
339         * @see PropertyDefinition#name()
340         */
341        public Builder name(String name) {
342          this.name = name;
343          return this;
344        }
345    
346        /**
347         * @see PropertyDefinition#defaultValue()
348         */
349        public Builder defaultValue(String defaultValue) {
350          this.defaultValue = defaultValue;
351          return this;
352        }
353    
354    
355        /**
356         * @see PropertyDefinition#category()
357         */
358        public Builder category(String category) {
359          this.category = category;
360          return this;
361        }
362    
363        /**
364         * @see PropertyDefinition#subCategory()
365         */
366        public Builder subCategory(String subCategory) {
367          this.subCategory = subCategory;
368          return this;
369        }
370    
371        /**
372         * The property will be available in General Settings AND in the components
373         * with the given qualifiers.
374         * <p/>
375         * For example @{code onQualifiers(Qualifiers.PROJECT)} allows to configure the
376         * property in General Settings and in Project Settings.
377         * <p/>
378         * See supported constant values in {@link Qualifiers}. By default property is available
379         * only in General Settings.
380         */
381        public Builder onQualifiers(String first, String... rest) {
382          this.onQualifiers.addAll(Lists.asList(first, rest));
383          this.global = true;
384          return this;
385        }
386    
387        /**
388         * The property will be available in General Settings AND in the components
389         * with the given qualifiers.
390         * <p/>
391         * For example @{code onQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
392         * property in General Settings and in Project Settings.
393         * <p/>
394         * See supported constant values in {@link Qualifiers}. By default property is available
395         * only in General Settings.
396         */
397        public Builder onQualifiers(List<String> qualifiers) {
398          this.onQualifiers.addAll(ImmutableList.copyOf(qualifiers));
399          this.global = true;
400          return this;
401        }
402    
403        /**
404         * The property will be available in the components
405         * with the given qualifiers, but NOT in General Settings.
406         * <p/>
407         * For example @{code onlyOnQualifiers(Qualifiers.PROJECT)} allows to configure the
408         * property in Project Settings only.
409         * <p/>
410         * See supported constant values in {@link Qualifiers}. By default property is available
411         * only in General Settings.
412         */
413        public Builder onlyOnQualifiers(String first, String... rest) {
414          this.onlyOnQualifiers.addAll(Lists.asList(first, rest));
415          this.global = false;
416          return this;
417        }
418    
419        /**
420         * The property will be available in the components
421         * with the given qualifiers, but NOT in General Settings.
422         * <p/>
423         * For example @{code onlyOnQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the
424         * property in Project Settings only.
425         * <p/>
426         * See supported constant values in {@link Qualifiers}. By default property is available
427         * only in General Settings.
428         */
429        public Builder onlyOnQualifiers(List<String> qualifiers) {
430          this.onlyOnQualifiers.addAll(ImmutableList.copyOf(qualifiers));
431          this.global = false;
432          return this;
433        }
434    
435        /**
436         * @see org.sonar.api.config.PropertyDefinition#type()
437         */
438        public Builder type(PropertyType type) {
439          this.type = type;
440          return this;
441        }
442    
443        public Builder options(String first, String... rest) {
444          this.options.addAll(Lists.asList(first, rest));
445          return this;
446        }
447    
448        public Builder options(List<String> options) {
449          this.options.addAll(ImmutableList.copyOf(options));
450          return this;
451        }
452    
453        public Builder multiValues(boolean multiValues) {
454          this.multiValues = multiValues;
455          return this;
456        }
457    
458        public Builder propertySetKey(String propertySetKey) {
459          this.propertySetKey = propertySetKey;
460          return this;
461        }
462    
463        public Builder fields(PropertyFieldDefinition first, PropertyFieldDefinition... rest) {
464          this.fields.addAll(Lists.asList(first, rest));
465          return this;
466        }
467    
468        public Builder fields(List<PropertyFieldDefinition> fields) {
469          this.fields.addAll(ImmutableList.copyOf(fields));
470          return this;
471        }
472    
473        public Builder deprecatedKey(String deprecatedKey) {
474          this.deprecatedKey = deprecatedKey;
475          return this;
476        }
477    
478        /**
479         * Flag the property as hidden. Hidden properties are not displayed in Settings pages
480         * but allow plugins to benefit from type and default values when calling {@link Settings}.
481         */
482        public Builder hidden() {
483          this.hidden = true;
484          return this;
485        }
486    
487        /**
488         * Set the order index in Settings pages. A property with a lower index is displayed
489         * before properties with higher index.
490         */
491        public Builder index(int index) {
492          this.index = index;
493          return this;
494        }
495    
496        public PropertyDefinition build() {
497          Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Key must be set");
498          fixType(key, type);
499          Preconditions.checkArgument(onQualifiers.isEmpty() || onlyOnQualifiers.isEmpty(), "Cannot define both onQualifiers and onlyOnQualifiers");
500          Preconditions.checkArgument((!hidden || (onQualifiers.isEmpty()) && onlyOnQualifiers.isEmpty()), "Cannot be hidden and defining qualifiers on which to display");
501          if (hidden) {
502            global = false;
503          }
504          return new PropertyDefinition(this);
505        }
506    
507        private void fixType(String key, PropertyType type) {
508          // Auto-detect passwords and licenses for old versions of plugins that
509          // do not declare property types
510          if (type == PropertyType.STRING) {
511            if (StringUtils.endsWith(key, ".password.secured")) {
512              this.type = PropertyType.PASSWORD;
513            } else if (StringUtils.endsWith(key, ".license.secured")) {
514              this.type = PropertyType.LICENSE;
515            }
516          }
517        }
518      }
519    
520    }