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