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