001/*
002 * SonarQube
003 * Copyright (C) 2009-2018 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        .setDeprecatedKey("pageSize", "5.2")
390        .setDefaultValue(String.valueOf(defaultPageSize))
391        .setMaximumValue(maxPageSize)
392        .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize)
393        .setExampleValue("20");
394    }
395
396    /**
397     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is
398     * used to restrict the number of fields returned in JSON response.
399     */
400    public NewAction addFieldsParam(Collection<?> possibleValues) {
401      createFieldsParam(possibleValues);
402      return this;
403    }
404
405    public NewParam createFieldsParam(Collection<?> possibleValues) {
406      return createParam(Param.FIELDS)
407        .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.")
408        .setPossibleValues(possibleValues);
409    }
410
411    /**
412     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is
413     * used to search for a subset of fields containing the supplied string.
414     * <p>
415     * The fields must be in the <strong>plural</strong> form (ex: "names", "keys").
416     * </p>
417     */
418    public NewAction addSearchQuery(String exampleValue, String... pluralFields) {
419      createSearchQuery(exampleValue, pluralFields);
420      return this;
421    }
422
423    /**
424     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is
425     * used to search for a subset of fields containing the supplied string.
426     * <p>
427     * The fields must be in the <strong>plural</strong> form (ex: "names", "keys").
428     * </p>
429     */
430    public NewParam createSearchQuery(String exampleValue, String... pluralFields) {
431      String actionDescription = format("Limit search to %s that contain the supplied string.", String.join(" or ", pluralFields));
432
433      return createParam(Param.TEXT_QUERY)
434        .setDescription(actionDescription)
435        .setExampleValue(exampleValue);
436    }
437
438    /**
439     * Add predefined parameters related to sorting of results.
440     */
441    public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
442      createSortParams(possibleValues, defaultValue, defaultAscending);
443      return this;
444    }
445
446    /**
447     * Add predefined parameters related to sorting of results.
448     */
449    public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
450      createParam(Param.ASCENDING)
451        .setDescription("Ascending sort")
452        .setBooleanPossibleValues()
453        .setDefaultValue(defaultAscending);
454
455      return createParam(Param.SORT)
456        .setDescription("Sort field")
457        .setDeprecatedKey("sort", "5.4")
458        .setDefaultValue(defaultValue)
459        .setPossibleValues(possibleValues);
460    }
461
462    /**
463     * Add 'selected=(selected|deselected|all)' for select-list oriented WS.
464     */
465    public NewAction addSelectionModeParam() {
466      createParam(Param.SELECTED)
467        .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " +
468          "or all items with their selection status (selected=all).")
469        .setDefaultValue(SelectionMode.SELECTED.value())
470        .setPossibleValues(SelectionMode.possibleValues());
471      return this;
472    }
473  }
474
475  @Immutable
476  class Action {
477    private static final Logger LOGGER = Loggers.get(Action.class);
478
479    private final String key;
480    private final String deprecatedKey;
481    private final String path;
482    private final String description;
483    private final String since;
484    private final String deprecatedSince;
485    private final boolean post;
486    private final boolean isInternal;
487    private final RequestHandler handler;
488    private final Map<String, Param> params;
489    private final URL responseExample;
490    private final List<Change> changelog;
491
492    private Action(Controller controller, NewAction newAction) {
493      this.key = newAction.key;
494      this.deprecatedKey = newAction.deprecatedKey;
495      this.path = format("%s/%s", controller.path(), key);
496      this.description = newAction.description;
497      this.since = newAction.since;
498      this.deprecatedSince = newAction.deprecatedSince;
499      this.post = newAction.post;
500      this.isInternal = newAction.isInternal;
501      this.responseExample = newAction.responseExample;
502      this.handler = newAction.handler;
503      this.changelog = newAction.changelog;
504
505      checkState(this.handler != null, "RequestHandler is not set on action %s", path);
506      logWarningIf(isNullOrEmpty(this.description), "Description is not set on action " + path);
507      logWarningIf(isNullOrEmpty(this.since), "Since is not set on action " + path);
508      logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path);
509
510      Map<String, Param> paramsBuilder = new HashMap<>();
511      for (NewParam newParam : newAction.newParams.values()) {
512        paramsBuilder.put(newParam.key, new Param(this, newParam));
513      }
514      this.params = Collections.unmodifiableMap(paramsBuilder);
515    }
516
517    private static void logWarningIf(boolean condition, String message) {
518      if (condition) {
519        LOGGER.warn(message);
520      }
521    }
522
523    public String key() {
524      return key;
525    }
526
527    public String deprecatedKey() {
528      return deprecatedKey;
529    }
530
531    public String path() {
532      return path;
533    }
534
535    @CheckForNull
536    public String description() {
537      return description;
538    }
539
540    /**
541     * Set if different than controller.
542     */
543    @CheckForNull
544    public String since() {
545      return since;
546    }
547
548    @CheckForNull
549    public String deprecatedSince() {
550      return deprecatedSince;
551    }
552
553    public boolean isPost() {
554      return post;
555    }
556
557    /**
558     * @see NewAction#setChangelog(Change...)
559     * @since 6.4
560     */
561    public List<Change> changelog() {
562      return changelog;
563    }
564
565    /**
566     * @see NewAction#setInternal(boolean)
567     */
568    public boolean isInternal() {
569      return isInternal;
570    }
571
572    public RequestHandler handler() {
573      return handler;
574    }
575
576    /**
577     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
578     */
579    @CheckForNull
580    public URL responseExample() {
581      return responseExample;
582    }
583
584    /**
585     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
586     */
587    @CheckForNull
588    public String responseExampleAsString() {
589      try {
590        if (responseExample != null) {
591          return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8));
592        }
593        return null;
594      } catch (IOException e) {
595        throw new IllegalStateException("Fail to load " + responseExample, e);
596      }
597    }
598
599    /**
600     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
601     */
602    @CheckForNull
603    public String responseExampleFormat() {
604      if (responseExample != null) {
605        return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile()));
606      }
607      return null;
608    }
609
610    @CheckForNull
611    public Param param(String key) {
612      return params.get(key);
613    }
614
615    public Collection<Param> params() {
616      return params.values();
617    }
618
619    @Override
620    public String toString() {
621      return path;
622    }
623  }
624
625  class NewParam {
626    private String key;
627    private String since;
628    private String deprecatedSince;
629    private String deprecatedKey;
630    private String deprecatedKeySince;
631    private String description;
632    private String exampleValue;
633    private String defaultValue;
634    private boolean required = false;
635    private boolean internal = false;
636    private Set<String> possibleValues = null;
637    private Integer maxValuesAllowed;
638    private Integer maximumLength;
639    private Integer minimumLength;
640    private Integer maximumValue;
641
642    private NewParam(String key) {
643      this.key = key;
644    }
645
646    /**
647     * @since 5.3
648     * @see Param#since()
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     * @see #setDeprecatedKey(String, String)
665     * @since 5.0
666     * @deprecated since 6.4
667     */
668    @Deprecated
669    public NewParam setDeprecatedKey(@Nullable String s) {
670      this.deprecatedKey = s;
671      return this;
672    }
673
674    /**
675     * @param deprecatedSince Version when the old key was replaced/deprecated. Ex: 5.6
676     * @since 6.4
677     * @see Param#deprecatedKey()
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     * @see Param#description()
693     */
694    public NewParam setDescription(@Nullable String description, Object... descriptionArgument) {
695      this.description = description == null ? null : String.format(description, descriptionArgument);
696      return this;
697    }
698
699    /**
700     * Is the parameter required or optional ? Default value is false (optional).
701     *
702     * @since 4.4
703     * @see Param#isRequired()
704     */
705    public NewParam setRequired(boolean b) {
706      this.required = b;
707      return this;
708    }
709
710    /**
711     * Internal parameters are not displayed by default in the web api documentation. They are
712     * displayed only when the check-box "Show Internal API" is selected. By default
713     * a parameter is not internal.
714     *
715     * @since 6.2
716     * @see Param#isInternal()
717     */
718    public NewParam setInternal(boolean b) {
719      this.internal = b;
720      return this;
721    }
722
723    /**
724     * @since 4.4
725     * @see Param#exampleValue()
726     */
727    public NewParam setExampleValue(@Nullable Object s) {
728      this.exampleValue = (s != null) ? s.toString() : null;
729      return this;
730    }
731
732    /**
733     * Exhaustive list of possible values when it makes sense, for example
734     * list of severities.
735     *
736     * @since 4.4
737     * @see Param#possibleValues()
738     */
739    public NewParam setPossibleValues(@Nullable Object... values) {
740      return setPossibleValues(values == null ? Collections.emptyList() : asList(values));
741    }
742
743    /**
744     * Shortcut for {@code setPossibleValues("true", "false", "yes", "no")}
745     * @since 4.4
746     */
747    public NewParam setBooleanPossibleValues() {
748      return setPossibleValues("true", "false", "yes", "no");
749    }
750
751    /**
752     * Exhaustive list of possible values when it makes sense, for example
753     * list of severities.
754     *
755     * @since 4.4
756     * @see Param#possibleValues()
757     */
758    public NewParam setPossibleValues(@Nullable Collection<?> values) {
759      if (values == null || values.isEmpty()) {
760        this.possibleValues = null;
761      } else {
762        this.possibleValues = new LinkedHashSet<>();
763        for (Object value : values) {
764          this.possibleValues.add(value.toString());
765        }
766      }
767      return this;
768    }
769
770    /**
771     * @since 4.4
772     * @see Param#defaultValue()
773     */
774    public NewParam setDefaultValue(@Nullable Object o) {
775      this.defaultValue = (o != null) ? o.toString() : null;
776      return this;
777    }
778
779    /**
780     * @since 6.4
781     * @see Param#maxValuesAllowed()
782     */
783    public NewParam setMaxValuesAllowed(@Nullable Integer maxValuesAllowed) {
784      this.maxValuesAllowed = maxValuesAllowed;
785      return this;
786    }
787
788    /**
789     * @since 7.0
790     * @see Param#maximumLength()
791     */
792    public NewParam setMaximumLength(@Nullable Integer maximumLength) {
793      this.maximumLength = maximumLength;
794      return this;
795    }
796
797    /**
798     * @since 7.0
799     * @see Param#minimumLength()
800     */
801    public NewParam setMinimumLength(@Nullable Integer minimumLength) {
802      this.minimumLength = minimumLength;
803      return this;
804    }
805
806    /**
807     * @since 7.0
808     * @see Param#maximumValue()
809     */
810    public NewParam setMaximumValue(@Nullable Integer maximumValue) {
811      this.maximumValue = maximumValue;
812      return this;
813    }
814
815    @Override
816    public String toString() {
817      return key;
818    }
819  }
820
821  enum SelectionMode {
822    SELECTED("selected"), DESELECTED("deselected"), ALL("all");
823
824    private final String paramValue;
825
826    private static final Map<String, SelectionMode> BY_VALUE = stream(values())
827      .collect(Collectors.toMap(v -> v.paramValue, v -> v));
828
829    SelectionMode(String paramValue) {
830      this.paramValue = paramValue;
831    }
832
833    public String value() {
834      return paramValue;
835    }
836
837    public static SelectionMode fromParam(String paramValue) {
838      checkArgument(BY_VALUE.containsKey(paramValue));
839      return BY_VALUE.get(paramValue);
840    }
841
842    public static Collection<String> possibleValues() {
843      return BY_VALUE.keySet();
844    }
845  }
846
847  @Immutable
848  class Param {
849    public static final String TEXT_QUERY = "q";
850    public static final String PAGE = "p";
851    public static final String PAGE_SIZE = "ps";
852    public static final String FIELDS = "f";
853    public static final String SORT = "s";
854    public static final String ASCENDING = "asc";
855    public static final String FACETS = "facets";
856    public static final String SELECTED = "selected";
857
858    private final String key;
859    private final String since;
860    private final String deprecatedSince;
861    private final String deprecatedKey;
862    private final String deprecatedKeySince;
863    private final String description;
864    private final String exampleValue;
865    private final String defaultValue;
866    private final boolean required;
867    private final boolean internal;
868    private final Set<String> possibleValues;
869    private final Integer maximumLength;
870    private final Integer minimumLength;
871    private final Integer maximumValue;
872    private final Integer maxValuesAllowed;
873
874    protected Param(Action action, NewParam newParam) {
875      this.key = newParam.key;
876      this.since = newParam.since;
877      this.deprecatedSince = newParam.deprecatedSince;
878      this.deprecatedKey = newParam.deprecatedKey;
879      this.deprecatedKeySince = newParam.deprecatedKeySince;
880      this.description = newParam.description;
881      this.exampleValue = newParam.exampleValue;
882      this.defaultValue = newParam.defaultValue;
883      this.required = newParam.required;
884      this.internal = newParam.internal;
885      this.possibleValues = newParam.possibleValues;
886      this.maxValuesAllowed = newParam.maxValuesAllowed;
887      this.maximumLength = newParam.maximumLength;
888      this.minimumLength = newParam.minimumLength;
889      this.maximumValue = newParam.maximumValue;
890      checkArgument(!required || defaultValue == null, "Default value must not be set on parameter '%s?%s' as it's marked as required", action, key);
891    }
892
893    public String key() {
894      return key;
895    }
896
897    /**
898     * @since 5.3
899     */
900    @CheckForNull
901    public String since() {
902      return since;
903    }
904
905    /**
906     * @since 5.3
907     */
908    @CheckForNull
909    public String deprecatedSince() {
910      return deprecatedSince;
911    }
912
913    /**
914     * @since 5.0
915     */
916    @CheckForNull
917    public String deprecatedKey() {
918      return deprecatedKey;
919    }
920
921    /**
922     * @since 6.4
923     */
924    @CheckForNull
925    public String deprecatedKeySince() {
926      return deprecatedKeySince;
927    }
928
929    @CheckForNull
930    public String description() {
931      return description;
932    }
933
934    /**
935     * @since 4.4
936     */
937    @CheckForNull
938    public String exampleValue() {
939      return exampleValue;
940    }
941
942    /**
943     * Is the parameter required or optional ?
944     *
945     * @since 4.4
946     */
947    public boolean isRequired() {
948      return required;
949    }
950
951    /**
952     * Is the parameter internal ?
953     *
954     * @see NewParam#setInternal(boolean)
955     * @since 6.2
956     */
957    public boolean isInternal() {
958      return internal;
959    }
960
961    /**
962     * @since 4.4
963     */
964    @CheckForNull
965    public Set<String> possibleValues() {
966      return possibleValues;
967    }
968
969    /**
970     * @since 4.4
971     */
972    @CheckForNull
973    public String defaultValue() {
974      return defaultValue;
975    }
976
977    /**
978     * Specify the maximum number of values allowed when using {@link Request#multiParam(String)}
979     *
980     * @since 6.4
981     */
982    public Integer maxValuesAllowed() {
983      return maxValuesAllowed;
984    }
985
986    /**
987     * Specify the maximum length of the value used in this parameter
988     *
989     * @since 7.0
990     */
991    @CheckForNull
992    public Integer maximumLength() {
993      return maximumLength;
994    }
995
996    /**
997     * Specify the minimum length of the value used in this parameter
998     *
999     * @since 7.0
1000     */
1001    @CheckForNull
1002    public Integer minimumLength() {
1003      return minimumLength;
1004    }
1005
1006    /**
1007     * Specify the maximum value of the numeric variable used in this parameter
1008     *
1009     * @since 7.0
1010     */
1011    @CheckForNull
1012    public Integer maximumValue() {
1013      return maximumValue;
1014    }
1015
1016    @Override
1017    public String toString() {
1018      return key;
1019    }
1020  }
1021
1022  /**
1023   * Executed once at server startup.
1024   */
1025  @Override
1026  void define(Context context);
1027
1028}