001/* 002 * SonarQube 003 * Copyright (C) 2009-2018 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 java.util.ArrayList; 023import java.util.EnumMap; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.function.Function; 028import java.util.regex.Pattern; 029import java.util.regex.PatternSyntaxException; 030import java.util.stream.Stream; 031import javax.annotation.Nullable; 032import org.apache.commons.lang.StringUtils; 033import org.apache.commons.lang.math.NumberUtils; 034import org.sonar.api.ExtensionPoint; 035import org.sonar.api.Property; 036import org.sonar.api.PropertyType; 037import org.sonar.api.batch.ScannerSide; 038import org.sonar.api.ce.ComputeEngineSide; 039import org.sonar.api.resources.Qualifiers; 040import org.sonar.api.server.ServerSide; 041import org.sonarsource.api.sonarlint.SonarLintSide; 042 043import static com.google.common.base.Preconditions.checkArgument; 044import static java.util.Arrays.asList; 045import static java.util.Arrays.stream; 046import static java.util.Collections.unmodifiableSet; 047import static java.util.Objects.requireNonNull; 048import static org.apache.commons.lang.StringUtils.isBlank; 049import static org.apache.commons.lang.StringUtils.isEmpty; 050import static org.sonar.api.PropertyType.BOOLEAN; 051import static org.sonar.api.PropertyType.FLOAT; 052import static org.sonar.api.PropertyType.INTEGER; 053import static org.sonar.api.PropertyType.LONG; 054import static org.sonar.api.PropertyType.PROPERTY_SET; 055import static org.sonar.api.PropertyType.REGULAR_EXPRESSION; 056import static org.sonar.api.PropertyType.SINGLE_SELECT_LIST; 057 058/** 059 * Declare a plugin property. Values are available at runtime through the component {@link Configuration}. 060 * <br> 061 * It's the programmatic alternative to the annotation {@link org.sonar.api.Property}. It is more 062 * testable and adds new features like sub-categories and ordering. 063 * <br> 064 * Example: 065 * <pre><code> 066 * public class MyPlugin extends SonarPlugin { 067 * public List getExtensions() { 068 * return Arrays.asList( 069 * PropertyDefinition.builder("sonar.foo").name("Foo").build(), 070 * PropertyDefinition.builder("sonar.bar").name("Bar").defaultValue("123").type(PropertyType.INTEGER).build() 071 * ); 072 * } 073 * } 074 * </code></pre> 075 * <br> 076 * Keys in localization bundles are: 077 * <ul> 078 * <li>{@code property.<key>.name} is the label of the property</li> 079 * <li>{@code property.<key>.description} is the optional description of the property</li> 080 * <li>{@code property.category.<category>} is the category label</li> 081 * <li>{@code property.category.<category>.description} is the category description</li> 082 * <li>{@code property.category.<category>.<subcategory>} is the sub-category label</li> 083 * <li>{@code property.category.<category>.<subcategory>.description} is the sub-category description</li> 084 * </ul> 085 * 086 * @since 3.6 087 */ 088@ScannerSide 089@ServerSide 090@ComputeEngineSide 091@SonarLintSide 092@ExtensionPoint 093public final class PropertyDefinition { 094 095 private static final Set<String> SUPPORTED_QUALIFIERS = unmodifiableSet(new LinkedHashSet<>( 096 asList(Qualifiers.PROJECT, Qualifiers.VIEW, Qualifiers.MODULE, Qualifiers.SUBVIEW, Qualifiers.APP))); 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 EnumMap<PropertyType, Function<String, Result>> map = new EnumMap<>(PropertyType.class); 181 map.put(BOOLEAN, validateBoolean()); 182 map.put(INTEGER, validateInteger()); 183 map.put(LONG, validateInteger()); 184 map.put(FLOAT, validateFloat()); 185 map.put(REGULAR_EXPRESSION, validateRegexp()); 186 map.put(SINGLE_SELECT_LIST, 187 aValue -> options.contains(aValue) ? Result.SUCCESS : Result.newError("notInOptions")); 188 return map; 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}, {@link Qualifiers#APP APP}, 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}, {@link Qualifiers#APP APP}, 460 * {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed. 461 * @throws IllegalArgumentException only qualifiers {@link Qualifiers#PROJECT PROJECT}, {@link Qualifiers#MODULE MODULE}, 462 * {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed. 463 */ 464 public Builder onQualifiers(List<String> qualifiers) { 465 addQualifiers(this.onQualifiers, qualifiers); 466 this.global = true; 467 return this; 468 } 469 470 /** 471 * The property will be available in the components 472 * with the given qualifiers, but NOT in General Settings. 473 * <br> 474 * For example @{code onlyOnQualifiers(Qualifiers.PROJECT)} allows to configure the 475 * property in Project Settings only. 476 * <br> 477 * See supported constant values in {@link Qualifiers}. By default property is available 478 * only in General Settings. 479 * 480 * @throws IllegalArgumentException only qualifiers {@link Qualifiers#PROJECT PROJECT}, {@link Qualifiers#MODULE MODULE}, {@link Qualifiers#APP APP}, 481 * {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed. 482 */ 483 public Builder onlyOnQualifiers(String first, String... rest) { 484 addQualifiers(this.onlyOnQualifiers, first, rest); 485 this.global = false; 486 return this; 487 } 488 489 /** 490 * The property will be available in the components 491 * with the given qualifiers, but NOT in General Settings. 492 * <br> 493 * For example @{code onlyOnQualifiers(Arrays.asList(Qualifiers.PROJECT))} allows to configure the 494 * property in Project Settings only. 495 * <br> 496 * See supported constant values in {@link Qualifiers}. By default property is available 497 * only in General Settings. 498 * 499 * @throws IllegalArgumentException only qualifiers {@link Qualifiers#PROJECT PROJECT}, {@link Qualifiers#MODULE MODULE}, {@link Qualifiers#APP APP}, 500 * {@link Qualifiers#VIEW VIEW} and {@link Qualifiers#SUBVIEW SVW} are allowed. 501 */ 502 public Builder onlyOnQualifiers(List<String> qualifiers) { 503 addQualifiers(this.onlyOnQualifiers, qualifiers); 504 this.global = false; 505 return this; 506 } 507 508 private static void addQualifiers(List<String> target, String first, String... rest) { 509 Stream.concat(Stream.of(first), stream(rest)).peek(PropertyDefinition.Builder::validateQualifier).forEach(target::add); 510 } 511 512 private static void addQualifiers(List<String> target, List<String> qualifiers) { 513 qualifiers.stream().peek(PropertyDefinition.Builder::validateQualifier).forEach(target::add); 514 } 515 516 private static void validateQualifier(@Nullable String qualifier) { 517 requireNonNull(qualifier, "Qualifier cannot be null"); 518 checkArgument(SUPPORTED_QUALIFIERS.contains(qualifier), "Qualifier must be one of %s", SUPPORTED_QUALIFIERS); 519 } 520 521 /** 522 * @see org.sonar.api.config.PropertyDefinition#type() 523 */ 524 public Builder type(PropertyType type) { 525 this.type = type; 526 return this; 527 } 528 529 public Builder options(String first, String... rest) { 530 this.options.add(first); 531 stream(rest).forEach(o -> options.add(o)); 532 return this; 533 } 534 535 public Builder options(List<String> options) { 536 this.options.addAll(options); 537 return this; 538 } 539 540 public Builder multiValues(boolean multiValues) { 541 this.multiValues = multiValues; 542 return this; 543 } 544 545 /** 546 * @deprecated since 6.1, as it was not used and too complex to maintain. 547 */ 548 @Deprecated 549 public Builder propertySetKey(String propertySetKey) { 550 this.propertySetKey = propertySetKey; 551 return this; 552 } 553 554 public Builder fields(PropertyFieldDefinition first, PropertyFieldDefinition... rest) { 555 this.fields.add(first); 556 this.fields.addAll(asList(rest)); 557 return this; 558 } 559 560 public Builder fields(List<PropertyFieldDefinition> fields) { 561 this.fields.addAll(fields); 562 return this; 563 } 564 565 public Builder deprecatedKey(String deprecatedKey) { 566 this.deprecatedKey = deprecatedKey; 567 return this; 568 } 569 570 /** 571 * Flag the property as hidden. Hidden properties are not displayed in Settings pages 572 * but allow plugins to benefit from type and default values when calling {@link Settings}. 573 */ 574 public Builder hidden() { 575 this.hidden = true; 576 return this; 577 } 578 579 /** 580 * Set the order index in Settings pages. A property with a lower index is displayed 581 * before properties with higher index. 582 */ 583 public Builder index(int index) { 584 this.index = index; 585 return this; 586 } 587 588 public PropertyDefinition build() { 589 checkArgument(!isEmpty(key), "Key must be set"); 590 fixType(key, type); 591 checkArgument(onQualifiers.isEmpty() || onlyOnQualifiers.isEmpty(), "Cannot define both onQualifiers and onlyOnQualifiers"); 592 checkArgument(!hidden || (onQualifiers.isEmpty() && onlyOnQualifiers.isEmpty()), "Cannot be hidden and defining qualifiers on which to display"); 593 if (hidden) { 594 global = false; 595 } 596 if (!fields.isEmpty()) { 597 type = PROPERTY_SET; 598 } 599 return new PropertyDefinition(this); 600 } 601 602 private void fixType(String key, PropertyType type) { 603 // Auto-detect passwords and licenses for old versions of plugins that 604 // do not declare property types 605 if (type == PropertyType.STRING) { 606 if (StringUtils.endsWith(key, ".password.secured")) { 607 this.type = PropertyType.PASSWORD; 608 } else if (StringUtils.endsWith(key, ".license.secured")) { 609 this.type = PropertyType.LICENSE; 610 } 611 } 612 } 613 } 614}