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