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