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