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.Sets;
026import java.io.IOException;
027import java.net.URL;
028import java.nio.charset.StandardCharsets;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Set;
037import java.util.stream.Collectors;
038import javax.annotation.CheckForNull;
039import javax.annotation.Nullable;
040import javax.annotation.concurrent.Immutable;
041import org.apache.commons.io.FilenameUtils;
042import org.apache.commons.io.IOUtils;
043import org.apache.commons.lang.StringUtils;
044import org.sonar.api.ExtensionPoint;
045import org.sonar.api.server.ServerSide;
046import org.sonar.api.utils.log.Logger;
047import org.sonar.api.utils.log.Loggers;
048
049import static com.google.common.base.Preconditions.checkArgument;
050import static com.google.common.base.Preconditions.checkState;
051import static com.google.common.base.Strings.isNullOrEmpty;
052import static java.lang.String.format;
053import static java.util.Arrays.asList;
054import static java.util.Arrays.stream;
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 = new HashMap<>();
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 = new HashMap<>();
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 = new HashMap<>();
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 = 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     * Add predefined parameters related to pagination of results.
356     */
357    public NewAction addPagingParams(int defaultPageSize) {
358      createParam(Param.PAGE)
359        .setDescription("1-based page number")
360        .setExampleValue("42")
361        .setDeprecatedKey("pageIndex", "5.2")
362        .setDefaultValue("1");
363
364      createParam(Param.PAGE_SIZE)
365        .setDescription("Page size. Must be greater than 0.")
366        .setExampleValue("20")
367        .setDeprecatedKey("pageSize", "5.2")
368        .setDefaultValue(String.valueOf(defaultPageSize));
369      return this;
370    }
371
372    /**
373     * Add predefined parameters related to pagination of results with a maximum page size.
374     * Note the maximum is a documentation only feature. It does not check anything.
375     */
376    public NewAction addPagingParams(int defaultPageSize, int maxPageSize) {
377      createPageParam();
378      createPageSize(defaultPageSize, maxPageSize);
379      return this;
380    }
381
382    public NewParam createPageParam() {
383      return createParam(Param.PAGE)
384        .setDescription("1-based page number")
385        .setExampleValue("42")
386        .setDeprecatedKey("pageIndex", "5.2")
387        .setDefaultValue("1");
388    }
389
390    public NewParam createPageSize(int defaultPageSize, int maxPageSize) {
391      return createParam(Param.PAGE_SIZE)
392        .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize)
393        .setExampleValue("20")
394        .setDeprecatedKey("pageSize", "5.2")
395        .setDefaultValue(String.valueOf(defaultPageSize));
396    }
397
398    /**
399     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is
400     * used to restrict the number of fields returned in JSON response.
401     */
402    public NewAction addFieldsParam(Collection<?> possibleValues) {
403      createFieldsParam(possibleValues);
404      return this;
405    }
406
407    public NewParam createFieldsParam(Collection<?> possibleValues) {
408      return createParam(Param.FIELDS)
409        .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.")
410        .setPossibleValues(possibleValues);
411    }
412
413    /**
414     *
415     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is
416     * used to search for a subset of fields containing the supplied string.
417     * <p>
418     * The fields must be in the <strong>plural</strong> form (ex: "names", "keys").
419     * </p>
420     */
421    public NewAction addSearchQuery(String exampleValue, String... pluralFields) {
422      createSearchQuery(exampleValue, pluralFields);
423      return this;
424    }
425
426    /**
427     *
428     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is
429     * used to search for a subset of fields containing the supplied string.
430     * <p>
431     * The fields must be in the <strong>plural</strong> form (ex: "names", "keys").
432     * </p>
433     */
434    public NewParam createSearchQuery(String exampleValue, String... pluralFields) {
435      String actionDescription = format("Limit search to %s that contain the supplied string.", Joiner.on(" or ").join(pluralFields));
436
437      return createParam(Param.TEXT_QUERY)
438        .setDescription(actionDescription)
439        .setExampleValue(exampleValue);
440    }
441
442    /**
443     * Add predefined parameters related to sorting of results.
444     */
445    public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
446      createSortParams(possibleValues, defaultValue, defaultAscending);
447      return this;
448    }
449
450    /**
451     * Add predefined parameters related to sorting of results.
452     */
453    public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
454      createParam(Param.ASCENDING)
455        .setDescription("Ascending sort")
456        .setBooleanPossibleValues()
457        .setDefaultValue(defaultAscending);
458
459      return createParam(Param.SORT)
460        .setDescription("Sort field")
461        .setDeprecatedKey("sort", "5.4")
462        .setDefaultValue(defaultValue)
463        .setPossibleValues(possibleValues);
464    }
465
466    /**
467     * Add 'selected=(selected|deselected|all)' for select-list oriented WS.
468     */
469    public NewAction addSelectionModeParam() {
470      createParam(Param.SELECTED)
471        .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " +
472          "or all items with their selection status (selected=all).")
473        .setDefaultValue(SelectionMode.SELECTED.value())
474        .setPossibleValues(SelectionMode.possibleValues());
475      return this;
476    }
477  }
478
479  @Immutable
480  class Action {
481    private static final Logger LOGGER = Loggers.get(Action.class);
482
483    private final String key;
484    private final String deprecatedKey;
485    private final String path;
486    private final String description;
487    private final String since;
488    private final String deprecatedSince;
489    private final boolean post;
490    private final boolean isInternal;
491    private final RequestHandler handler;
492    private final Map<String, Param> params;
493    private final URL responseExample;
494    private final List<Change> changelog;
495
496    private Action(Controller controller, NewAction newAction) {
497      this.key = newAction.key;
498      this.deprecatedKey = newAction.deprecatedKey;
499      this.path = format("%s/%s", controller.path(), key);
500      this.description = newAction.description;
501      this.since = newAction.since;
502      this.deprecatedSince = newAction.deprecatedSince;
503      this.post = newAction.post;
504      this.isInternal = newAction.isInternal;
505      this.responseExample = newAction.responseExample;
506      this.handler = newAction.handler;
507      this.changelog = newAction.changelog;
508
509      checkState(this.handler != null, "RequestHandler is not set on action %s", path);
510      logWarningIf(isNullOrEmpty(this.description), "Description is not set on action " + path);
511      logWarningIf(isNullOrEmpty(this.since), "Since is not set on action " + path);
512      logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path);
513
514      ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder();
515      for (NewParam newParam : newAction.newParams.values()) {
516        paramsBuilder.put(newParam.key, new Param(this, newParam));
517      }
518      this.params = paramsBuilder.build();
519    }
520
521    private static void logWarningIf(boolean condition, String message) {
522      if (condition) {
523        LOGGER.warn(message);
524      }
525    }
526
527    public String key() {
528      return key;
529    }
530
531    public String deprecatedKey() {
532      return deprecatedKey;
533    }
534
535    public String path() {
536      return path;
537    }
538
539    @CheckForNull
540    public String description() {
541      return description;
542    }
543
544    /**
545     * Set if different than controller.
546     */
547    @CheckForNull
548    public String since() {
549      return since;
550    }
551
552    @CheckForNull
553    public String deprecatedSince() {
554      return deprecatedSince;
555    }
556
557    public boolean isPost() {
558      return post;
559    }
560
561    /**
562     * @see NewAction#setChangelog(Change...)
563     * @since 6.4
564     */
565    public List<Change> changelog() {
566      return changelog;
567    }
568
569    /**
570     * @see NewAction#setInternal(boolean)
571     */
572    public boolean isInternal() {
573      return isInternal;
574    }
575
576    public RequestHandler handler() {
577      return handler;
578    }
579
580    /**
581     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
582     */
583    @CheckForNull
584    public URL responseExample() {
585      return responseExample;
586    }
587
588    /**
589     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
590     */
591    @CheckForNull
592    public String responseExampleAsString() {
593      try {
594        if (responseExample != null) {
595          return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8));
596        }
597        return null;
598      } catch (IOException e) {
599        throw new IllegalStateException("Fail to load " + responseExample, e);
600      }
601    }
602
603    /**
604     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
605     */
606    @CheckForNull
607    public String responseExampleFormat() {
608      if (responseExample != null) {
609        return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile()));
610      }
611      return null;
612    }
613
614    @CheckForNull
615    public Param param(String key) {
616      return params.get(key);
617    }
618
619    public Collection<Param> params() {
620      return params.values();
621    }
622
623    @Override
624    public String toString() {
625      return path;
626    }
627  }
628
629  class NewParam {
630    private String key;
631    private String since;
632    private String deprecatedSince;
633    private String deprecatedKey;
634    private String deprecatedKeySince;
635    private String description;
636    private String exampleValue;
637    private String defaultValue;
638    private boolean required = false;
639    private boolean internal = false;
640    private Set<String> possibleValues = null;
641    private Integer maxValuesAllowed = null;
642
643    private NewParam(String key) {
644      this.key = key;
645    }
646
647    /**
648     * @since 5.3
649     */
650    public NewParam setSince(@Nullable String since) {
651      this.since = since;
652      return this;
653    }
654
655    /**
656     * @since 5.3
657     */
658    public NewParam setDeprecatedSince(@Nullable String deprecatedSince) {
659      this.deprecatedSince = deprecatedSince;
660      return this;
661    }
662
663    /**
664     * @since 5.0
665     * @deprecated since 6.4
666     * @see #setDeprecatedKey(String, String) 
667     */
668    @Deprecated
669    public NewParam setDeprecatedKey(@Nullable String s) {
670      this.deprecatedKey = s;
671      return this;
672    }
673
674    /**
675     *
676     * @param deprecatedSince Version when the old key was replaced/deprecated. Ex: 5.6
677     * @since 6.4
678     */
679    public NewParam setDeprecatedKey(@Nullable String key, @Nullable String deprecatedSince) {
680      this.deprecatedKey = key;
681      this.deprecatedKeySince = deprecatedSince;
682      return this;
683    }
684
685    public NewParam setDescription(@Nullable String description) {
686      this.description = description;
687      return this;
688    }
689
690    /**
691     * @since 5.6
692     */
693    public NewParam setDescription(@Nullable String description, Object... descriptionArgument) {
694      this.description = description == null ? null : String.format(description, descriptionArgument);
695      return this;
696    }
697
698    /**
699     * Is the parameter required or optional ? Default value is false (optional).
700     *
701     * @since 4.4
702     */
703    public NewParam setRequired(boolean b) {
704      this.required = b;
705      return this;
706    }
707
708    /**
709     * Internal parameters are not displayed by default in the web api documentation. They are
710     * displayed only when the check-box "Show Internal API" is selected. By default
711     * a parameter is not internal.
712     *
713     * @since 6.2
714     */
715    public NewParam setInternal(boolean b) {
716      this.internal = b;
717      return this;
718    }
719
720    /**
721     * @since 4.4
722     */
723    public NewParam setExampleValue(@Nullable Object s) {
724      this.exampleValue = (s != null) ? s.toString() : null;
725      return this;
726    }
727
728    /**
729     * Exhaustive list of possible values when it makes sense, for example
730     * list of severities.
731     *
732     * @since 4.4
733     */
734    public NewParam setPossibleValues(@Nullable Object... values) {
735      return setPossibleValues(values == null ? Collections.emptyList() : asList(values));
736    }
737
738    /**
739     * @since 4.4
740     */
741    public NewParam setBooleanPossibleValues() {
742      return setPossibleValues("true", "false", "yes", "no");
743    }
744
745    /**
746     * Exhaustive list of possible values when it makes sense, for example
747     * list of severities.
748     *
749     * @since 4.4
750     */
751    public NewParam setPossibleValues(@Nullable Collection<?> values) {
752      if (values == null || values.isEmpty()) {
753        this.possibleValues = null;
754      } else {
755        this.possibleValues = Sets.newLinkedHashSet();
756        for (Object value : values) {
757          this.possibleValues.add(value.toString());
758        }
759      }
760      return this;
761    }
762
763    /**
764     * @since 4.4
765     */
766    public NewParam setDefaultValue(@Nullable Object o) {
767      this.defaultValue = (o != null) ? o.toString() : null;
768      return this;
769    }
770
771    /**
772     * @since 6.4
773     */
774    public NewParam setMaxValuesAllowed(@Nullable Integer maxValuesAllowed) {
775      this.maxValuesAllowed = maxValuesAllowed;
776      return this;
777    }
778
779    @Override
780    public String toString() {
781      return key;
782    }
783  }
784
785  enum SelectionMode {
786    SELECTED("selected"), DESELECTED("deselected"), ALL("all");
787
788    private final String paramValue;
789
790    private static final Map<String, SelectionMode> BY_VALUE = stream(values())
791      .collect(Collectors.toMap(v -> v.paramValue, v -> v));
792
793    SelectionMode(String paramValue) {
794      this.paramValue = paramValue;
795    }
796
797    public String value() {
798      return paramValue;
799    }
800
801    public static SelectionMode fromParam(String paramValue) {
802      checkArgument(BY_VALUE.containsKey(paramValue));
803      return BY_VALUE.get(paramValue);
804    }
805
806    public static Collection<String> possibleValues() {
807      return BY_VALUE.keySet();
808    }
809  }
810
811  @Immutable
812  class Param {
813    public static final String TEXT_QUERY = "q";
814    public static final String PAGE = "p";
815    public static final String PAGE_SIZE = "ps";
816    public static final String FIELDS = "f";
817    public static final String SORT = "s";
818    public static final String ASCENDING = "asc";
819    public static final String FACETS = "facets";
820    public static final String SELECTED = "selected";
821
822    private final String key;
823    private final String since;
824    private final String deprecatedSince;
825    private final String deprecatedKey;
826    private final String deprecatedKeySince;
827    private final String description;
828    private final String exampleValue;
829    private final String defaultValue;
830    private final boolean required;
831    private final boolean internal;
832    private final Set<String> possibleValues;
833    private final Integer maxValuesAllowed;
834
835    protected Param(Action action, NewParam newParam) {
836      this.key = newParam.key;
837      this.since = newParam.since;
838      this.deprecatedSince = newParam.deprecatedSince;
839      this.deprecatedKey = newParam.deprecatedKey;
840      this.deprecatedKeySince = newParam.deprecatedKeySince;
841      this.description = newParam.description;
842      this.exampleValue = newParam.exampleValue;
843      this.defaultValue = newParam.defaultValue;
844      this.required = newParam.required;
845      this.internal = newParam.internal;
846      this.possibleValues = newParam.possibleValues;
847      this.maxValuesAllowed = newParam.maxValuesAllowed;
848      checkArgument(!required || defaultValue == null, "Default value must not be set on parameter '%s?%s' as it's marked as required", action, key);
849    }
850
851    public String key() {
852      return key;
853    }
854
855    /**
856     * @since 5.3
857     */
858    @CheckForNull
859    public String since() {
860      return since;
861    }
862
863    /**
864     * @since 5.3
865     */
866    @CheckForNull
867    public String deprecatedSince() {
868      return deprecatedSince;
869    }
870
871    /**
872     * @since 5.0
873     */
874    @CheckForNull
875    public String deprecatedKey() {
876      return deprecatedKey;
877    }
878
879    /**
880     * @since 6.4
881     */
882    @CheckForNull
883    public String deprecatedKeySince() {
884      return deprecatedKeySince;
885    }
886
887    @CheckForNull
888    public String description() {
889      return description;
890    }
891
892    /**
893     * @since 4.4
894     */
895    @CheckForNull
896    public String exampleValue() {
897      return exampleValue;
898    }
899
900    /**
901     * Is the parameter required or optional ?
902     *
903     * @since 4.4
904     */
905    public boolean isRequired() {
906      return required;
907    }
908
909    /**
910     * Is the parameter internal ?
911     *
912     * @since 6.2
913     * @see NewParam#setInternal(boolean)
914     */
915    public boolean isInternal() {
916      return internal;
917    }
918
919    /**
920     * @since 4.4
921     */
922    @CheckForNull
923    public Set<String> possibleValues() {
924      return possibleValues;
925    }
926
927    /**
928     * @since 4.4
929     */
930    @CheckForNull
931    public String defaultValue() {
932      return defaultValue;
933    }
934
935    /**
936     * Specify the maximum number of values allowed when using this a parameter
937     *
938     * @since 6.4
939     */
940    public Integer maxValuesAllowed() {
941      return maxValuesAllowed;
942    }
943
944    @Override
945    public String toString() {
946      return key;
947    }
948  }
949
950  /**
951   * Executed once at server startup.
952   */
953  @Override
954  void define(Context context);
955
956}