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