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