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 }