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.server.ws; 021 022 import com.google.common.base.Charsets; 023 import com.google.common.collect.ImmutableList; 024 import com.google.common.collect.ImmutableMap; 025 import com.google.common.collect.Maps; 026 import com.google.common.collect.Sets; 027 import org.apache.commons.io.FilenameUtils; 028 import org.apache.commons.io.IOUtils; 029 import org.apache.commons.lang.StringUtils; 030 import org.sonar.api.ServerExtension; 031 032 import javax.annotation.CheckForNull; 033 import javax.annotation.Nullable; 034 import javax.annotation.concurrent.Immutable; 035 036 import java.io.IOException; 037 import java.net.URL; 038 import java.util.*; 039 040 /** 041 * Defines a web service. Note that contrary to the deprecated {@link org.sonar.api.web.Webservice} 042 * the ws is fully implemented in Java and does not require any Ruby on Rails code. 043 * <p/> 044 * <p/> 045 * The classes implementing this extension point must be declared in {@link org.sonar.api.SonarPlugin#getExtensions()}. 046 * <p/> 047 * <h3>How to use</h3> 048 * <pre> 049 * public class HelloWs implements WebService { 050 * {@literal @}Override 051 * public void define(Context context) { 052 * NewController controller = context.createController("api/hello"); 053 * controller.setDescription("Web service example"); 054 * 055 * // create the URL /api/hello/show 056 * controller.createAction("show") 057 * .setDescription("Entry point") 058 * .setHandler(new RequestHandler() { 059 * {@literal @}Override 060 * public void handle(Request request, Response response) { 061 * // read request parameters and generates response output 062 * response.newJsonWriter() 063 * .prop("hello", request.mandatoryParam("key")) 064 * .close(); 065 * } 066 * }) 067 * .createParam("key").setDescription("Example key").setRequired(true); 068 * 069 * // important to apply changes 070 * controller.done(); 071 * } 072 * } 073 * </pre> 074 * <h3>How to test</h3> 075 * <pre> 076 * public class HelloWsTest { 077 * WebService ws = new HelloWs(); 078 * 079 * {@literal @}Test 080 * public void should_define_ws() throws Exception { 081 * // WsTester is available in the Maven artifact org.codehaus.sonar:sonar-testing-harness 082 * WsTester tester = new WsTester(ws); 083 * WebService.Controller controller = tester.controller("api/hello"); 084 * assertThat(controller).isNotNull(); 085 * assertThat(controller.path()).isEqualTo("api/hello"); 086 * assertThat(controller.description()).isNotEmpty(); 087 * assertThat(controller.actions()).hasSize(1); 088 * 089 * WebService.Action show = controller.action("show"); 090 * assertThat(show).isNotNull(); 091 * assertThat(show.key()).isEqualTo("show"); 092 * assertThat(index.handler()).isNotNull(); 093 * } 094 * } 095 * </pre> 096 * 097 * @since 4.2 098 */ 099 public interface WebService extends ServerExtension { 100 101 class Context { 102 private final Map<String, Controller> controllers = Maps.newHashMap(); 103 104 /** 105 * Create a new controller. 106 * <p/> 107 * Structure of request URL is <code>http://<server>/<>controller path>/<action path>?<parameters></code>. 108 * 109 * @param path the controller path must not start or end with "/". It is recommended to start with "api/" 110 * and to use lower-case format with underscores, for example "api/coding_rules". Usual actions 111 * are "search", "list", "show", "create" and "delete" 112 */ 113 public NewController createController(String path) { 114 return new NewController(this, path); 115 } 116 117 private void register(NewController newController) { 118 if (controllers.containsKey(newController.path)) { 119 throw new IllegalStateException( 120 String.format("The web service '%s' is defined multiple times", newController.path) 121 ); 122 } 123 controllers.put(newController.path, new Controller(newController)); 124 } 125 126 @CheckForNull 127 public Controller controller(String key) { 128 return controllers.get(key); 129 } 130 131 public List<Controller> controllers() { 132 return ImmutableList.copyOf(controllers.values()); 133 } 134 } 135 136 class NewController { 137 private final Context context; 138 private final String path; 139 private String description, since; 140 private final Map<String, NewAction> actions = Maps.newHashMap(); 141 142 private NewController(Context context, String path) { 143 if (StringUtils.isBlank(path)) { 144 throw new IllegalArgumentException("WS controller path must not be empty"); 145 } 146 if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) { 147 throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path); 148 } 149 this.context = context; 150 this.path = path; 151 } 152 153 /** 154 * Important - this method must be called in order to apply changes and make the 155 * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()} 156 */ 157 public void done() { 158 context.register(this); 159 } 160 161 /** 162 * Optional description (accept HTML) 163 */ 164 public NewController setDescription(@Nullable String s) { 165 this.description = s; 166 return this; 167 } 168 169 /** 170 * Optional version when the controller was created 171 */ 172 public NewController setSince(@Nullable String s) { 173 this.since = s; 174 return this; 175 } 176 177 public NewAction createAction(String actionKey) { 178 if (actions.containsKey(actionKey)) { 179 throw new IllegalStateException( 180 String.format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path) 181 ); 182 } 183 NewAction action = new NewAction(actionKey); 184 actions.put(actionKey, action); 185 return action; 186 } 187 } 188 189 @Immutable 190 class Controller { 191 private final String path, description, since; 192 private final Map<String, Action> actions; 193 194 private Controller(NewController newController) { 195 if (newController.actions.isEmpty()) { 196 throw new IllegalStateException( 197 String.format("At least one action must be declared in the web service '%s'", newController.path) 198 ); 199 } 200 this.path = newController.path; 201 this.description = newController.description; 202 this.since = newController.since; 203 ImmutableMap.Builder<String, Action> mapBuilder = ImmutableMap.builder(); 204 for (NewAction newAction : newController.actions.values()) { 205 mapBuilder.put(newAction.key, new Action(this, newAction)); 206 } 207 this.actions = mapBuilder.build(); 208 } 209 210 public String path() { 211 return path; 212 } 213 214 @CheckForNull 215 public String description() { 216 return description; 217 } 218 219 @CheckForNull 220 public String since() { 221 return since; 222 } 223 224 @CheckForNull 225 public Action action(String actionKey) { 226 return actions.get(actionKey); 227 } 228 229 public Collection<Action> actions() { 230 return actions.values(); 231 } 232 233 /** 234 * Returns true if all the actions are for internal use 235 * 236 * @see org.sonar.api.server.ws.WebService.Action#isInternal() 237 * @since 4.3 238 */ 239 public boolean isInternal() { 240 for (Action action : actions()) { 241 if (!action.isInternal()) { 242 return false; 243 } 244 } 245 return true; 246 } 247 } 248 249 class NewAction { 250 private final String key; 251 private String deprecatedKey, description, since; 252 private boolean post = false, isInternal = false; 253 private RequestHandler handler; 254 private Map<String, NewParam> newParams = Maps.newHashMap(); 255 private URL responseExample = null; 256 257 private NewAction(String key) { 258 this.key = key; 259 } 260 261 public NewAction setDeprecatedKey(@Nullable String s) { 262 this.deprecatedKey = s; 263 return this; 264 } 265 266 public NewAction setDescription(@Nullable String s) { 267 this.description = s; 268 return this; 269 } 270 271 public NewAction setSince(@Nullable String s) { 272 this.since = s; 273 return this; 274 } 275 276 public NewAction setPost(boolean b) { 277 this.post = b; 278 return this; 279 } 280 281 public NewAction setInternal(boolean b) { 282 this.isInternal = b; 283 return this; 284 } 285 286 public NewAction setHandler(RequestHandler h) { 287 this.handler = h; 288 return this; 289 } 290 291 /** 292 * Link to the document containing an example of response. Content must be UTF-8 encoded. 293 * <p/> 294 * Example: 295 * <pre> 296 * newAction.setResponseExample(getClass().getResource("/org/sonar/my-ws-response-example.json")); 297 * </pre> 298 * 299 * @since 4.4 300 */ 301 public NewAction setResponseExample(@Nullable URL url) { 302 this.responseExample = url; 303 return this; 304 } 305 306 public NewParam createParam(String paramKey) { 307 if (newParams.containsKey(paramKey)) { 308 throw new IllegalStateException( 309 String.format("The parameter '%s' is defined multiple times in the action '%s'", paramKey, key) 310 ); 311 } 312 NewParam newParam = new NewParam(paramKey); 313 newParams.put(paramKey, newParam); 314 return newParam; 315 } 316 317 /** 318 * @deprecated since 4.4. Use {@link #createParam(String paramKey)} instead. 319 */ 320 @Deprecated 321 public NewAction createParam(String paramKey, @Nullable String description) { 322 createParam(paramKey).setDescription(description); 323 return this; 324 } 325 } 326 327 @Immutable 328 class Action { 329 private final String key, deprecatedKey, path, description, since; 330 private final boolean post, isInternal; 331 private final RequestHandler handler; 332 private final Map<String, Param> params; 333 private final URL responseExample; 334 335 private Action(Controller controller, NewAction newAction) { 336 this.key = newAction.key; 337 this.deprecatedKey = newAction.deprecatedKey; 338 this.path = String.format("%s/%s", controller.path(), key); 339 this.description = newAction.description; 340 this.since = StringUtils.defaultIfBlank(newAction.since, controller.since); 341 this.post = newAction.post; 342 this.isInternal = newAction.isInternal; 343 this.responseExample = newAction.responseExample; 344 345 if (newAction.handler == null) { 346 throw new IllegalArgumentException("RequestHandler is not set on action " + path); 347 } 348 this.handler = newAction.handler; 349 350 ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder(); 351 for (NewParam newParam : newAction.newParams.values()) { 352 paramsBuilder.put(newParam.key, new Param(newParam)); 353 } 354 this.params = paramsBuilder.build(); 355 } 356 357 public String key() { 358 return key; 359 } 360 361 public String deprecatedKey() { 362 return deprecatedKey; 363 } 364 365 public String path() { 366 return path; 367 } 368 369 @CheckForNull 370 public String description() { 371 return description; 372 } 373 374 /** 375 * Set if different than controller. 376 */ 377 @CheckForNull 378 public String since() { 379 return since; 380 } 381 382 public boolean isPost() { 383 return post; 384 } 385 386 public boolean isInternal() { 387 return isInternal; 388 } 389 390 public RequestHandler handler() { 391 return handler; 392 } 393 394 /** 395 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 396 */ 397 @CheckForNull 398 public URL responseExample() { 399 return responseExample; 400 } 401 402 /** 403 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 404 */ 405 @CheckForNull 406 public String responseExampleAsString() { 407 try { 408 if (responseExample != null) { 409 return StringUtils.trim(IOUtils.toString(responseExample, Charsets.UTF_8)); 410 } 411 return null; 412 } catch (IOException e) { 413 throw new IllegalStateException("Fail to load " + responseExample, e); 414 } 415 } 416 417 /** 418 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 419 */ 420 @CheckForNull 421 public String responseExampleFormat() { 422 if (responseExample != null) { 423 return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile())); 424 } 425 return null; 426 } 427 428 @CheckForNull 429 public Param param(String key) { 430 return params.get(key); 431 } 432 433 public Collection<Param> params() { 434 return params.values(); 435 } 436 437 @Override 438 public String toString() { 439 return path; 440 } 441 } 442 443 class NewParam { 444 private String key, deprecatedKey, description, exampleValue, defaultValue; 445 private boolean required = false; 446 private Set<String> possibleValues = null; 447 448 private NewParam(String key) { 449 this.key = key; 450 } 451 452 /** 453 * @since 5.0 454 */ 455 public NewParam setDeprecatedKey(@Nullable String s) { 456 this.deprecatedKey = s; 457 return this; 458 } 459 460 public NewParam setDescription(@Nullable String s) { 461 this.description = s; 462 return this; 463 } 464 465 /** 466 * Is the parameter required or optional ? Default value is false (optional). 467 * 468 * @since 4.4 469 */ 470 public NewParam setRequired(boolean b) { 471 this.required = b; 472 return this; 473 } 474 475 /** 476 * @since 4.4 477 */ 478 public NewParam setExampleValue(@Nullable Object s) { 479 this.exampleValue = (s != null ? s.toString() : null); 480 return this; 481 } 482 483 /** 484 * Exhaustive list of possible values when it makes sense, for example 485 * list of severities. 486 * 487 * @since 4.4 488 */ 489 public NewParam setPossibleValues(@Nullable Object... values) { 490 return setPossibleValues(values == null ? (Collection) null : Arrays.asList(values)); 491 } 492 493 /** 494 * @since 4.4 495 */ 496 public NewParam setBooleanPossibleValues() { 497 return setPossibleValues("true", "false", "yes", "no"); 498 } 499 500 /** 501 * Exhaustive list of possible values when it makes sense, for example 502 * list of severities. 503 * 504 * @since 4.4 505 */ 506 public NewParam setPossibleValues(@Nullable Collection values) { 507 if (values == null) { 508 this.possibleValues = null; 509 } else { 510 this.possibleValues = Sets.newLinkedHashSet(); 511 for (Object value : values) { 512 this.possibleValues.add(value.toString()); 513 } 514 } 515 return this; 516 } 517 518 /** 519 * @since 4.4 520 */ 521 public NewParam setDefaultValue(@Nullable Object o) { 522 this.defaultValue = (o != null ? o.toString() : null); 523 return this; 524 } 525 526 @Override 527 public String toString() { 528 return key; 529 } 530 } 531 532 @Immutable 533 class Param { 534 private final String key, deprecatedKey, description, exampleValue, defaultValue; 535 private final boolean required; 536 private final Set<String> possibleValues; 537 538 public Param(NewParam newParam) { 539 this.key = newParam.key; 540 this.deprecatedKey = newParam.deprecatedKey; 541 this.description = newParam.description; 542 this.exampleValue = newParam.exampleValue; 543 this.defaultValue = newParam.defaultValue; 544 this.required = newParam.required; 545 this.possibleValues = newParam.possibleValues; 546 } 547 548 public String key() { 549 return key; 550 } 551 552 /** 553 * @since 5.0 554 */ 555 @CheckForNull 556 public String deprecatedKey() { 557 return deprecatedKey; 558 } 559 560 @CheckForNull 561 public String description() { 562 return description; 563 } 564 565 /** 566 * @since 4.4 567 */ 568 @CheckForNull 569 public String exampleValue() { 570 return exampleValue; 571 } 572 573 /** 574 * Is the parameter required or optional ? 575 * 576 * @since 4.4 577 */ 578 public boolean isRequired() { 579 return required; 580 } 581 582 /** 583 * @since 4.4 584 */ 585 @CheckForNull 586 public Set<String> possibleValues() { 587 return possibleValues; 588 } 589 590 /** 591 * @since 4.4 592 */ 593 @CheckForNull 594 public String defaultValue() { 595 return defaultValue; 596 } 597 598 @Override 599 public String toString() { 600 return key; 601 } 602 } 603 604 /** 605 * Executed once at server startup. 606 */ 607 void define(Context context); 608 609 }