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