001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info 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.server.ws; 021 022import com.google.common.base.Joiner; 023import com.google.common.collect.ImmutableList; 024import com.google.common.collect.ImmutableMap; 025import com.google.common.collect.Maps; 026import com.google.common.collect.Sets; 027import java.io.IOException; 028import java.net.URL; 029import java.nio.charset.StandardCharsets; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035import javax.annotation.CheckForNull; 036import javax.annotation.Nullable; 037import javax.annotation.concurrent.Immutable; 038import org.apache.commons.io.FilenameUtils; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang.StringUtils; 041import org.sonar.api.ExtensionPoint; 042import org.sonar.api.server.ServerSide; 043import org.sonar.api.utils.log.Logger; 044import org.sonar.api.utils.log.Loggers; 045 046import static com.google.common.base.Preconditions.checkArgument; 047import static com.google.common.base.Preconditions.checkState; 048import static com.google.common.base.Strings.isNullOrEmpty; 049import static java.lang.String.format; 050import static java.util.Arrays.asList; 051 052/** 053 * Defines a web service. 054 * <br> 055 * <br> 056 * The classes implementing this extension point must be declared by {@link org.sonar.api.Plugin}. 057 * <br> 058 * <h3>How to use</h3> 059 * <pre> 060 * public class HelloWs implements WebService { 061 * {@literal @}Override 062 * public void define(Context context) { 063 * NewController controller = context.createController("api/hello"); 064 * controller.setDescription("Web service example"); 065 * 066 * // create the URL /api/hello/show 067 * controller.createAction("show") 068 * .setDescription("Entry point") 069 * .setHandler(new RequestHandler() { 070 * {@literal @}Override 071 * public void handle(Request request, Response response) { 072 * // read request parameters and generates response output 073 * response.newJsonWriter() 074 * .beginObject() 075 * .prop("hello", request.mandatoryParam("key")) 076 * .endObject() 077 * .close(); 078 * } 079 * }) 080 * .createParam("key").setDescription("Example key").setRequired(true); 081 * 082 * // important to apply changes 083 * controller.done(); 084 * } 085 * } 086 * </pre> 087 * 088 * Since version 5.5, a web service can call another web service to get some data. See {@link Request#localConnector()} 089 * provided by {@link RequestHandler#handle(Request, Response)}. 090 * 091 * @since 4.2 092 */ 093@ServerSide 094@ExtensionPoint 095public interface WebService extends Definable<WebService.Context> { 096 097 class Context { 098 private final Map<String, Controller> controllers = Maps.newHashMap(); 099 100 /** 101 * Create a new controller. 102 * <br> 103 * Structure of request URL is <code>http://<server>/<controller path>/<action path>?<parameters></code>. 104 * 105 * @param path the controller path must not start or end with "/". It is recommended to start with "api/" 106 * and to use lower-case format with underscores, for example "api/coding_rules". Usual actions 107 * are "search", "list", "show", "create" and "delete". 108 * the plural form is recommended - ex: api/projects 109 */ 110 public NewController createController(String path) { 111 return new NewController(this, path); 112 } 113 114 private void register(NewController newController) { 115 if (controllers.containsKey(newController.path)) { 116 throw new IllegalStateException( 117 format("The web service '%s' is defined multiple times", newController.path)); 118 } 119 controllers.put(newController.path, new Controller(newController)); 120 } 121 122 @CheckForNull 123 public Controller controller(String key) { 124 return controllers.get(key); 125 } 126 127 public List<Controller> controllers() { 128 return ImmutableList.copyOf(controllers.values()); 129 } 130 } 131 132 class NewController { 133 private final Context context; 134 private final String path; 135 private String description; 136 private String since; 137 private final Map<String, NewAction> actions = Maps.newHashMap(); 138 139 private NewController(Context context, String path) { 140 if (StringUtils.isBlank(path)) { 141 throw new IllegalArgumentException("WS controller path must not be empty"); 142 } 143 if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) { 144 throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path); 145 } 146 this.context = context; 147 this.path = path; 148 } 149 150 /** 151 * Important - this method must be called in order to apply changes and make the 152 * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()} 153 */ 154 public void done() { 155 context.register(this); 156 } 157 158 /** 159 * Optional description (accept HTML) 160 */ 161 public NewController setDescription(@Nullable String s) { 162 this.description = s; 163 return this; 164 } 165 166 /** 167 * Optional version when the controller was created 168 */ 169 public NewController setSince(@Nullable String s) { 170 this.since = s; 171 return this; 172 } 173 174 public NewAction createAction(String actionKey) { 175 if (actions.containsKey(actionKey)) { 176 throw new IllegalStateException( 177 format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path)); 178 } 179 NewAction action = new NewAction(actionKey); 180 actions.put(actionKey, action); 181 return action; 182 } 183 } 184 185 @Immutable 186 class Controller { 187 private final String path; 188 private final String description; 189 private final String since; 190 private final Map<String, Action> actions; 191 192 private Controller(NewController newController) { 193 checkState(!newController.actions.isEmpty(), "At least one action must be declared in the web service '%s'", newController.path); 194 this.path = newController.path; 195 this.description = newController.description; 196 this.since = newController.since; 197 ImmutableMap.Builder<String, Action> mapBuilder = ImmutableMap.builder(); 198 for (NewAction newAction : newController.actions.values()) { 199 mapBuilder.put(newAction.key, new Action(this, newAction)); 200 } 201 this.actions = mapBuilder.build(); 202 } 203 204 public String path() { 205 return path; 206 } 207 208 @CheckForNull 209 public String description() { 210 return description; 211 } 212 213 @CheckForNull 214 public String since() { 215 return since; 216 } 217 218 @CheckForNull 219 public Action action(String actionKey) { 220 return actions.get(actionKey); 221 } 222 223 public Collection<Action> actions() { 224 return actions.values(); 225 } 226 227 /** 228 * Returns true if all the actions are for internal use 229 * 230 * @see org.sonar.api.server.ws.WebService.Action#isInternal() 231 * @since 4.3 232 */ 233 public boolean isInternal() { 234 for (Action action : actions()) { 235 if (!action.isInternal()) { 236 return false; 237 } 238 } 239 return true; 240 } 241 } 242 243 class NewAction { 244 private final String key; 245 private String deprecatedKey; 246 private String description; 247 private String since; 248 private String deprecatedSince; 249 private boolean post = false; 250 private boolean isInternal = false; 251 private RequestHandler handler; 252 private Map<String, NewParam> newParams = Maps.newHashMap(); 253 private URL responseExample = null; 254 255 private NewAction(String key) { 256 this.key = key; 257 } 258 259 public NewAction setDeprecatedKey(@Nullable String s) { 260 this.deprecatedKey = s; 261 return this; 262 } 263 264 /** 265 * Used in Orchestrator 266 */ 267 public NewAction setDescription(@Nullable String description) { 268 this.description = description; 269 return this; 270 } 271 272 /** 273 * @since 5.6 274 */ 275 public NewAction setDescription(@Nullable String description, Object... descriptionArgument) { 276 this.description = description == null ? null : String.format(description, descriptionArgument); 277 return this; 278 } 279 280 public NewAction setSince(@Nullable String s) { 281 this.since = s; 282 return this; 283 } 284 285 /** 286 * @since 5.3 287 */ 288 public NewAction setDeprecatedSince(@Nullable String deprecatedSince) { 289 this.deprecatedSince = deprecatedSince; 290 return this; 291 } 292 293 public NewAction setPost(boolean b) { 294 this.post = b; 295 return this; 296 } 297 298 /** 299 * Internal actions are not displayed by default in the web api documentation. They are 300 * displayed only when the check-box "Show Internal API" is selected. By default 301 * an action is not internal. 302 */ 303 public NewAction setInternal(boolean b) { 304 this.isInternal = b; 305 return this; 306 } 307 308 public NewAction setHandler(RequestHandler h) { 309 this.handler = h; 310 return this; 311 } 312 313 /** 314 * Link to the document containing an example of response. Content must be UTF-8 encoded. 315 * <br> 316 * Example: 317 * <pre> 318 * newAction.setResponseExample(getClass().getResource("/org/sonar/my-ws-response-example.json")); 319 * </pre> 320 * 321 * @since 4.4 322 */ 323 public NewAction setResponseExample(@Nullable URL url) { 324 this.responseExample = url; 325 return this; 326 } 327 328 public NewParam createParam(String paramKey) { 329 checkState(!newParams.containsKey(paramKey), "The parameter '%s' is defined multiple times in the action '%s'", paramKey, key); 330 NewParam newParam = new NewParam(paramKey); 331 newParams.put(paramKey, newParam); 332 return newParam; 333 } 334 335 /** 336 * @deprecated since 4.4. Use {@link #createParam(String paramKey)} instead. 337 */ 338 @Deprecated 339 public NewAction createParam(String paramKey, @Nullable String description) { 340 createParam(paramKey).setDescription(description); 341 return this; 342 } 343 344 /** 345 * Add predefined parameters related to pagination of results. 346 */ 347 public NewAction addPagingParams(int defaultPageSize) { 348 createParam(Param.PAGE) 349 .setDescription("1-based page number") 350 .setExampleValue("42") 351 .setDeprecatedKey("pageIndex") 352 .setDefaultValue("1"); 353 354 createParam(Param.PAGE_SIZE) 355 .setDescription("Page size. Must be greater than 0.") 356 .setExampleValue("20") 357 .setDeprecatedKey("pageSize") 358 .setDefaultValue(String.valueOf(defaultPageSize)); 359 return this; 360 } 361 362 /** 363 * Add predefined parameters related to pagination of results with a maximum page size. 364 * Note the maximum is a documentation only feature. It does not check anything. 365 */ 366 public NewAction addPagingParams(int defaultPageSize, int maxPageSize) { 367 addPageParam(); 368 addPageSize(defaultPageSize, maxPageSize); 369 return this; 370 } 371 372 public NewAction addPageParam() { 373 createParam(Param.PAGE) 374 .setDescription("1-based page number") 375 .setExampleValue("42") 376 .setDeprecatedKey("pageIndex") 377 .setDefaultValue("1"); 378 return this; 379 } 380 381 public NewAction addPageSize(int defaultPageSize, int maxPageSize) { 382 createParam(Param.PAGE_SIZE) 383 .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize) 384 .setExampleValue("20") 385 .setDeprecatedKey("pageSize") 386 .setDefaultValue(String.valueOf(defaultPageSize)); 387 return this; 388 } 389 390 /** 391 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is 392 * used to restrict the number of fields returned in JSON response. 393 */ 394 public NewAction addFieldsParam(Collection<?> possibleValues) { 395 createFieldsParam(possibleValues); 396 return this; 397 } 398 399 public NewParam createFieldsParam(Collection<?> possibleValues) { 400 return createParam(Param.FIELDS) 401 .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.") 402 .setPossibleValues(possibleValues); 403 } 404 405 /** 406 * 407 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is 408 * used to search for a subset of fields containing the supplied string. 409 * <p> 410 * The fields must be in the <strong>plural</strong> form (ex: "names", "keys"). 411 * </p> 412 */ 413 public NewAction addSearchQuery(String exampleValue, String... pluralFields) { 414 String actionDescription = format("Limit search to %s that contain the supplied string.", Joiner.on(" or ").join(pluralFields)); 415 createParam(Param.TEXT_QUERY) 416 .setDescription(actionDescription) 417 .setExampleValue(exampleValue); 418 return this; 419 } 420 421 /** 422 * Add predefined parameters related to sorting of results. 423 */ 424 public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { 425 createSortParams(possibleValues, defaultValue, defaultAscending); 426 return this; 427 } 428 429 /** 430 * Add predefined parameters related to sorting of results. 431 */ 432 public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { 433 createParam(Param.ASCENDING) 434 .setDescription("Ascending sort") 435 .setBooleanPossibleValues() 436 .setDefaultValue(defaultAscending); 437 438 return createParam(Param.SORT) 439 .setDescription("Sort field") 440 .setDeprecatedKey("sort") 441 .setDefaultValue(defaultValue) 442 .setPossibleValues(possibleValues); 443 } 444 445 /** 446 * Add 'selected=(selected|deselected|all)' for select-list oriented WS. 447 */ 448 public NewAction addSelectionModeParam() { 449 createParam(Param.SELECTED) 450 .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " + 451 "or all items with their selection status (selected=all).") 452 .setDefaultValue(SelectionMode.SELECTED.value()) 453 .setPossibleValues(SelectionMode.possibleValues()); 454 return this; 455 } 456 } 457 458 @Immutable 459 class Action { 460 private static final Logger LOGGER = Loggers.get(Action.class); 461 462 private final String key; 463 private final String deprecatedKey; 464 private final String path; 465 private final String description; 466 private final String since; 467 private final String deprecatedSince; 468 private final boolean post; 469 private final boolean isInternal; 470 private final RequestHandler handler; 471 private final Map<String, Param> params; 472 private final URL responseExample; 473 474 private Action(Controller controller, NewAction newAction) { 475 this.key = newAction.key; 476 this.deprecatedKey = newAction.deprecatedKey; 477 this.path = format("%s/%s", controller.path(), key); 478 this.description = newAction.description; 479 this.since = newAction.since; 480 this.deprecatedSince = newAction.deprecatedSince; 481 this.post = newAction.post; 482 this.isInternal = newAction.isInternal; 483 this.responseExample = newAction.responseExample; 484 this.handler = newAction.handler; 485 486 checkState(this.handler != null, "RequestHandler is not set on action %s", path); 487 logWarningIf(isNullOrEmpty(this.description), "Description is not set on action " + path); 488 logWarningIf(isNullOrEmpty(this.since), "Since is not set on action " + path); 489 logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path); 490 491 ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder(); 492 for (NewParam newParam : newAction.newParams.values()) { 493 paramsBuilder.put(newParam.key, new Param(this, newParam)); 494 } 495 this.params = paramsBuilder.build(); 496 } 497 498 private static void logWarningIf(boolean condition, String message) { 499 if (condition) { 500 LOGGER.warn(message); 501 } 502 } 503 504 public String key() { 505 return key; 506 } 507 508 public String deprecatedKey() { 509 return deprecatedKey; 510 } 511 512 public String path() { 513 return path; 514 } 515 516 @CheckForNull 517 public String description() { 518 return description; 519 } 520 521 /** 522 * Set if different than controller. 523 */ 524 @CheckForNull 525 public String since() { 526 return since; 527 } 528 529 @CheckForNull 530 public String deprecatedSince() { 531 return deprecatedSince; 532 } 533 534 public boolean isPost() { 535 return post; 536 } 537 538 /** 539 * @see NewAction#setInternal(boolean) 540 */ 541 public boolean isInternal() { 542 return isInternal; 543 } 544 545 public RequestHandler handler() { 546 return handler; 547 } 548 549 /** 550 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 551 */ 552 @CheckForNull 553 public URL responseExample() { 554 return responseExample; 555 } 556 557 /** 558 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 559 */ 560 @CheckForNull 561 public String responseExampleAsString() { 562 try { 563 if (responseExample != null) { 564 return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8)); 565 } 566 return null; 567 } catch (IOException e) { 568 throw new IllegalStateException("Fail to load " + responseExample, e); 569 } 570 } 571 572 /** 573 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 574 */ 575 @CheckForNull 576 public String responseExampleFormat() { 577 if (responseExample != null) { 578 return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile())); 579 } 580 return null; 581 } 582 583 @CheckForNull 584 public Param param(String key) { 585 return params.get(key); 586 } 587 588 public Collection<Param> params() { 589 return params.values(); 590 } 591 592 @Override 593 public String toString() { 594 return path; 595 } 596 } 597 598 class NewParam { 599 private String key; 600 private String since; 601 private String deprecatedSince; 602 private String deprecatedKey; 603 private String description; 604 private String exampleValue; 605 private String defaultValue; 606 private boolean required = false; 607 private boolean internal = false; 608 private Set<String> possibleValues = null; 609 610 private NewParam(String key) { 611 this.key = key; 612 } 613 614 /** 615 * @since 5.3 616 */ 617 public NewParam setSince(@Nullable String since) { 618 this.since = since; 619 return this; 620 } 621 622 /** 623 * @since 5.3 624 */ 625 public NewParam setDeprecatedSince(@Nullable String deprecatedSince) { 626 this.deprecatedSince = deprecatedSince; 627 return this; 628 } 629 630 /** 631 * @since 5.0 632 */ 633 public NewParam setDeprecatedKey(@Nullable String s) { 634 this.deprecatedKey = s; 635 return this; 636 } 637 638 public NewParam setDescription(@Nullable String description) { 639 this.description = description; 640 return this; 641 } 642 643 /** 644 * @since 5.6 645 */ 646 public NewParam setDescription(@Nullable String description, Object... descriptionArgument) { 647 this.description = description == null ? null : String.format(description, descriptionArgument); 648 return this; 649 } 650 651 /** 652 * Is the parameter required or optional ? Default value is false (optional). 653 * 654 * @since 4.4 655 */ 656 public NewParam setRequired(boolean b) { 657 this.required = b; 658 return this; 659 } 660 661 /** 662 * Internal parameters are not displayed by default in the web api documentation. They are 663 * displayed only when the check-box "Show Internal API" is selected. By default 664 * a parameter is not internal. 665 * 666 * @since 6.2 667 */ 668 public NewParam setInternal(boolean b) { 669 this.internal = b; 670 return this; 671 } 672 673 /** 674 * @since 4.4 675 */ 676 public NewParam setExampleValue(@Nullable Object s) { 677 this.exampleValue = (s != null) ? s.toString() : null; 678 return this; 679 } 680 681 /** 682 * Exhaustive list of possible values when it makes sense, for example 683 * list of severities. 684 * 685 * @since 4.4 686 */ 687 public NewParam setPossibleValues(@Nullable Object... values) { 688 return setPossibleValues(values == null ? Collections.emptyList() : asList(values)); 689 } 690 691 /** 692 * @since 4.4 693 */ 694 public NewParam setBooleanPossibleValues() { 695 return setPossibleValues("true", "false", "yes", "no"); 696 } 697 698 /** 699 * Exhaustive list of possible values when it makes sense, for example 700 * list of severities. 701 * 702 * @since 4.4 703 */ 704 public NewParam setPossibleValues(@Nullable Collection<?> values) { 705 if (values == null || values.isEmpty()) { 706 this.possibleValues = null; 707 } else { 708 this.possibleValues = Sets.newLinkedHashSet(); 709 for (Object value : values) { 710 this.possibleValues.add(value.toString()); 711 } 712 } 713 return this; 714 } 715 716 /** 717 * @since 4.4 718 */ 719 public NewParam setDefaultValue(@Nullable Object o) { 720 this.defaultValue = (o != null) ? o.toString() : null; 721 return this; 722 } 723 724 @Override 725 public String toString() { 726 return key; 727 } 728 } 729 730 enum SelectionMode { 731 SELECTED("selected"), DESELECTED("deselected"), ALL("all"); 732 733 private final String paramValue; 734 735 private static final Map<String, SelectionMode> BY_VALUE = Maps.uniqueIndex(asList(values()), input -> input.paramValue); 736 737 SelectionMode(String paramValue) { 738 this.paramValue = paramValue; 739 } 740 741 public String value() { 742 return paramValue; 743 } 744 745 public static SelectionMode fromParam(String paramValue) { 746 checkArgument(BY_VALUE.containsKey(paramValue)); 747 return BY_VALUE.get(paramValue); 748 } 749 750 public static Collection<String> possibleValues() { 751 return BY_VALUE.keySet(); 752 } 753 } 754 755 @Immutable 756 class Param { 757 public static final String TEXT_QUERY = "q"; 758 public static final String PAGE = "p"; 759 public static final String PAGE_SIZE = "ps"; 760 public static final String FIELDS = "f"; 761 public static final String SORT = "s"; 762 public static final String ASCENDING = "asc"; 763 public static final String FACETS = "facets"; 764 public static final String SELECTED = "selected"; 765 766 private final String key; 767 private final String since; 768 private final String deprecatedSince; 769 private final String deprecatedKey; 770 private final String description; 771 private final String exampleValue; 772 private final String defaultValue; 773 private final boolean required; 774 private final boolean internal; 775 private final Set<String> possibleValues; 776 777 protected Param(Action action, NewParam newParam) { 778 this.key = newParam.key; 779 this.since = newParam.since; 780 this.deprecatedSince = newParam.deprecatedSince; 781 this.deprecatedKey = newParam.deprecatedKey; 782 this.description = newParam.description; 783 this.exampleValue = newParam.exampleValue; 784 this.defaultValue = newParam.defaultValue; 785 this.required = newParam.required; 786 this.internal = newParam.internal; 787 this.possibleValues = newParam.possibleValues; 788 if (required && defaultValue != null) { 789 throw new IllegalArgumentException(format("Default value must not be set on parameter '%s?%s' as it's marked as required", action, key)); 790 } 791 } 792 793 public String key() { 794 return key; 795 } 796 797 /** 798 * @since 5.3 799 */ 800 @CheckForNull 801 public String since() { 802 return since; 803 } 804 805 /** 806 * @since 5.3 807 */ 808 @CheckForNull 809 public String deprecatedSince() { 810 return deprecatedSince; 811 } 812 813 /** 814 * @since 5.0 815 */ 816 @CheckForNull 817 public String deprecatedKey() { 818 return deprecatedKey; 819 } 820 821 @CheckForNull 822 public String description() { 823 return description; 824 } 825 826 /** 827 * @since 4.4 828 */ 829 @CheckForNull 830 public String exampleValue() { 831 return exampleValue; 832 } 833 834 /** 835 * Is the parameter required or optional ? 836 * 837 * @since 4.4 838 */ 839 public boolean isRequired() { 840 return required; 841 } 842 843 /** 844 * Is the parameter internal ? 845 * 846 * @since 6.2 847 * @see NewParam#setInternal(boolean) 848 */ 849 public boolean isInternal() { 850 return internal; 851 } 852 853 /** 854 * @since 4.4 855 */ 856 @CheckForNull 857 public Set<String> possibleValues() { 858 return possibleValues; 859 } 860 861 /** 862 * @since 4.4 863 */ 864 @CheckForNull 865 public String defaultValue() { 866 return defaultValue; 867 } 868 869 @Override 870 public String toString() { 871 return key; 872 } 873 } 874 875 /** 876 * Executed once at server startup. 877 */ 878 @Override 879 void define(Context context); 880 881}