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://&lt;server&gt;/&lt;controller path&gt;/&lt;action path&gt;?&lt;parameters&gt;</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}