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