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