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