001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.server.ws;
021
022import com.google.common.base.Function;
023import com.google.common.base.Joiner;
024import com.google.common.collect.ImmutableList;
025import com.google.common.collect.ImmutableMap;
026import com.google.common.collect.Maps;
027import com.google.common.collect.Sets;
028import java.io.IOException;
029import java.net.URL;
030import java.nio.charset.StandardCharsets;
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.Collections;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037import javax.annotation.CheckForNull;
038import javax.annotation.Nonnull;
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;
053
054/**
055 * Defines a web service. Note that contrary to the deprecated {@link org.sonar.api.web.Webservice}
056 * the ws is fully implemented in Java and does not require any Ruby on Rails code.
057 * <br>
058 * <br>
059 * The classes implementing this extension point must be declared by {@link org.sonar.api.Plugin}.
060 * <br>
061 * <h3>How to use</h3>
062 * <pre>
063 * public class HelloWs implements WebService {
064 *   {@literal @}Override
065 *   public void define(Context context) {
066 *     NewController controller = context.createController("api/hello");
067 *     controller.setDescription("Web service example");
068 *
069 *     // create the URL /api/hello/show
070 *     controller.createAction("show")
071 *       .setDescription("Entry point")
072 *       .setHandler(new RequestHandler() {
073 *         {@literal @}Override
074 *         public void handle(Request request, Response response) {
075 *           // read request parameters and generates response output
076 *           response.newJsonWriter()
077 *             .beginObject()
078 *             .prop("hello", request.mandatoryParam("key"))
079 *             .endObject()
080 *             .close();
081 *         }
082 *      })
083 *      .createParam("key").setDescription("Example key").setRequired(true);
084 *
085 *    // important to apply changes
086 *    controller.done();
087 *   }
088 * }
089 * </pre>
090 *
091 * Since version 5.5, a web service can call another web service to get some data. See {@link Request#localConnector()}
092 * provided by {@link RequestHandler#handle(Request, Response)}.
093 *
094 * @since 4.2
095 */
096@ServerSide
097@ExtensionPoint
098public interface WebService extends Definable<WebService.Context> {
099
100  class Context {
101    private final Map<String, Controller> controllers = Maps.newHashMap();
102
103    /**
104     * Create a new controller.
105     * <br>
106     * Structure of request URL is <code>http://&lt;server&gt;/&lt;controller path&gt;/&lt;action path&gt;?&lt;parameters&gt;</code>.
107     *
108     * @param path the controller path must not start or end with "/". It is recommended to start with "api/"
109     *             and to use lower-case format with underscores, for example "api/coding_rules". Usual actions
110     *             are "search", "list", "show", "create" and "delete".
111     *             the plural form is recommended - ex: api/projects
112     */
113    public NewController createController(String path) {
114      return new NewController(this, path);
115    }
116
117    private void register(NewController newController) {
118      if (controllers.containsKey(newController.path)) {
119        throw new IllegalStateException(
120          format("The web service '%s' is defined multiple times", newController.path));
121      }
122      controllers.put(newController.path, new Controller(newController));
123    }
124
125    @CheckForNull
126    public Controller controller(String key) {
127      return controllers.get(key);
128    }
129
130    public List<Controller> controllers() {
131      return ImmutableList.copyOf(controllers.values());
132    }
133  }
134
135  class NewController {
136    private final Context context;
137    private final String path;
138    private String description;
139    private String since;
140    private final Map<String, NewAction> actions = Maps.newHashMap();
141
142    private NewController(Context context, String path) {
143      if (StringUtils.isBlank(path)) {
144        throw new IllegalArgumentException("WS controller path must not be empty");
145      }
146      if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) {
147        throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path);
148      }
149      this.context = context;
150      this.path = path;
151    }
152
153    /**
154     * Important - this method must be called in order to apply changes and make the
155     * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()}
156     */
157    public void done() {
158      context.register(this);
159    }
160
161    /**
162     * Optional description (accept HTML)
163     */
164    public NewController setDescription(@Nullable String s) {
165      this.description = s;
166      return this;
167    }
168
169    /**
170     * Optional version when the controller was created
171     */
172    public NewController setSince(@Nullable String s) {
173      this.since = s;
174      return this;
175    }
176
177    public NewAction createAction(String actionKey) {
178      if (actions.containsKey(actionKey)) {
179        throw new IllegalStateException(
180          format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path));
181      }
182      NewAction action = new NewAction(actionKey);
183      actions.put(actionKey, action);
184      return action;
185    }
186  }
187
188  @Immutable
189  class Controller {
190    private final String path;
191    private final String description;
192    private final String since;
193    private final Map<String, Action> actions;
194
195    private Controller(NewController newController) {
196      checkState(!newController.actions.isEmpty(), format("At least one action must be declared in the web service '%s'", newController.path));
197      this.path = newController.path;
198      this.description = newController.description;
199      this.since = newController.since;
200      ImmutableMap.Builder<String, Action> mapBuilder = ImmutableMap.builder();
201      for (NewAction newAction : newController.actions.values()) {
202        mapBuilder.put(newAction.key, new Action(this, newAction));
203      }
204      this.actions = mapBuilder.build();
205    }
206
207    public String path() {
208      return path;
209    }
210
211    @CheckForNull
212    public String description() {
213      return description;
214    }
215
216    @CheckForNull
217    public String since() {
218      return since;
219    }
220
221    @CheckForNull
222    public Action action(String actionKey) {
223      return actions.get(actionKey);
224    }
225
226    public Collection<Action> actions() {
227      return actions.values();
228    }
229
230    /**
231     * Returns true if all the actions are for internal use
232     *
233     * @see org.sonar.api.server.ws.WebService.Action#isInternal()
234     * @since 4.3
235     */
236    public boolean isInternal() {
237      for (Action action : actions()) {
238        if (!action.isInternal()) {
239          return false;
240        }
241      }
242      return true;
243    }
244  }
245
246  class NewAction {
247    private final String key;
248    private String deprecatedKey;
249    private String description;
250    private String since;
251    private String deprecatedSince;
252    private boolean post = false;
253    private boolean isInternal = false;
254    private RequestHandler handler;
255    private Map<String, NewParam> newParams = Maps.newHashMap();
256    private URL responseExample = null;
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    public NewAction setDescription(@Nullable String s) {
268      this.description = s;
269      return this;
270    }
271
272    public NewAction setSince(@Nullable String s) {
273      this.since = s;
274      return this;
275    }
276
277    public NewAction setDeprecatedSince(@Nullable String deprecatedSince) {
278      this.deprecatedSince = deprecatedSince;
279      return this;
280    }
281
282    public NewAction setPost(boolean b) {
283      this.post = b;
284      return this;
285    }
286
287    public NewAction setInternal(boolean b) {
288      this.isInternal = b;
289      return this;
290    }
291
292    public NewAction setHandler(RequestHandler h) {
293      this.handler = h;
294      return this;
295    }
296
297    /**
298     * Link to the document containing an example of response. Content must be UTF-8 encoded.
299     * <br>
300     * Example:
301     * <pre>
302     *   newAction.setResponseExample(getClass().getResource("/org/sonar/my-ws-response-example.json"));
303     * </pre>
304     *
305     * @since 4.4
306     */
307    public NewAction setResponseExample(@Nullable URL url) {
308      this.responseExample = url;
309      return this;
310    }
311
312    public NewParam createParam(String paramKey) {
313      checkState(!newParams.containsKey(paramKey),
314        format("The parameter '%s' is defined multiple times in the action '%s'", paramKey, key));
315      NewParam newParam = new NewParam(paramKey);
316      newParams.put(paramKey, newParam);
317      return newParam;
318    }
319
320    /**
321     * @deprecated since 4.4. Use {@link #createParam(String paramKey)} instead.
322     */
323    @Deprecated
324    public NewAction createParam(String paramKey, @Nullable String description) {
325      createParam(paramKey).setDescription(description);
326      return this;
327    }
328
329    /**
330     * Add predefined parameters related to pagination of results.
331     */
332    public NewAction addPagingParams(int defaultPageSize) {
333      createParam(Param.PAGE)
334        .setDescription("1-based page number")
335        .setExampleValue("42")
336        .setDeprecatedKey("pageIndex")
337        .setDefaultValue("1");
338
339      createParam(Param.PAGE_SIZE)
340        .setDescription("Page size. Must be greater than 0.")
341        .setExampleValue("20")
342        .setDeprecatedKey("pageSize")
343        .setDefaultValue(String.valueOf(defaultPageSize));
344      return this;
345    }
346
347    /**
348     * Add predefined parameters related to pagination of results with a maximum page size.
349     * Note the maximum is a documentation only feature. It does not check anything.
350     */
351    public NewAction addPagingParams(int defaultPageSize, int maxPageSize) {
352      addPageParam();
353      addPageSize(defaultPageSize, maxPageSize);
354      return this;
355    }
356
357    public NewAction addPageParam() {
358      createParam(Param.PAGE)
359        .setDescription("1-based page number")
360        .setExampleValue("42")
361        .setDeprecatedKey("pageIndex")
362        .setDefaultValue("1");
363      return this;
364    }
365
366    public NewAction addPageSize(int defaultPageSize, int maxPageSize) {
367      createParam(Param.PAGE_SIZE)
368        .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize)
369        .setExampleValue("20")
370        .setDeprecatedKey("pageSize")
371        .setDefaultValue(String.valueOf(defaultPageSize));
372      return this;
373    }
374
375    /**
376     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is
377     * used to restrict the number of fields returned in JSON response.
378     */
379    public NewAction addFieldsParam(Collection<?> possibleValues) {
380      createFieldsParam(possibleValues);
381      return this;
382    }
383
384    public NewParam createFieldsParam(Collection<?> possibleValues) {
385      return createParam(Param.FIELDS)
386        .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.")
387        .setPossibleValues(possibleValues);
388    }
389
390    /**$
391     *
392     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is
393     * used to search for a subset of fields containing the supplied string.
394     * <p>
395     * The fields must be in the <strong>plural</strong> form (ex: "names", "keys").
396     * </p>
397     */
398    public NewAction addSearchQuery(String exampleValue, String... pluralFields) {
399      String actionDescription = format("Limit search to %s that contain the supplied string.", Joiner.on(" or ").join(pluralFields));
400      createParam(Param.TEXT_QUERY)
401        .setDescription(actionDescription)
402        .setExampleValue(exampleValue);
403      return this;
404    }
405
406    /**
407     * Add predefined parameters related to sorting of results.
408     */
409    public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
410      createSortParams(possibleValues, defaultValue, defaultAscending);
411      return this;
412    }
413
414    /**
415     * Add predefined parameters related to sorting of results.
416     */
417    public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
418      createParam(Param.ASCENDING)
419        .setDescription("Ascending sort")
420        .setBooleanPossibleValues()
421        .setDefaultValue(defaultAscending);
422
423      return createParam(Param.SORT)
424        .setDescription("Sort field")
425        .setDeprecatedKey("sort")
426        .setDefaultValue(defaultValue)
427        .setPossibleValues(possibleValues);
428    }
429
430    /**
431     * Add 'selected=(selected|deselected|all)' for select-list oriented WS.
432     */
433    public NewAction addSelectionModeParam() {
434      createParam(Param.SELECTED)
435        .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " +
436          "or all items with their selection status (selected=all).")
437        .setDefaultValue(SelectionMode.SELECTED.value())
438        .setPossibleValues(SelectionMode.possibleValues());
439      return this;
440    }
441  }
442
443  @Immutable
444  class Action {
445    private static final Logger LOGGER = Loggers.get(Action.class);
446
447    private final String key;
448    private final String deprecatedKey;
449    private final String path;
450    private final String description;
451    private final String since;
452    private final String deprecatedSince;
453    private final boolean post;
454    private final boolean isInternal;
455    private final RequestHandler handler;
456    private final Map<String, Param> params;
457    private final URL responseExample;
458
459    private Action(Controller controller, NewAction newAction) {
460      this.key = newAction.key;
461      this.deprecatedKey = newAction.deprecatedKey;
462      this.path = format("%s/%s", controller.path(), key);
463      this.description = newAction.description;
464      this.since = newAction.since;
465      this.deprecatedSince = newAction.deprecatedSince;
466      this.post = newAction.post;
467      this.isInternal = newAction.isInternal;
468      this.responseExample = newAction.responseExample;
469      this.handler = newAction.handler;
470
471      checkState(this.handler != null, "RequestHandler is not set on action " + path);
472      logWarningIf(isNullOrEmpty(this.description), "Description is not set on action " + path);
473      logWarningIf(isNullOrEmpty(this.since), "Since is not set on action " + path);
474      logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path);
475
476      ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder();
477      for (NewParam newParam : newAction.newParams.values()) {
478        paramsBuilder.put(newParam.key, new Param(this, newParam));
479      }
480      this.params = paramsBuilder.build();
481    }
482
483    private static void logWarningIf(boolean condition, String message) {
484      if (condition) {
485        LOGGER.warn(message);
486      }
487    }
488
489    public String key() {
490      return key;
491    }
492
493    public String deprecatedKey() {
494      return deprecatedKey;
495    }
496
497    public String path() {
498      return path;
499    }
500
501    @CheckForNull
502    public String description() {
503      return description;
504    }
505
506    /**
507     * Set if different than controller.
508     */
509    @CheckForNull
510    public String since() {
511      return since;
512    }
513
514    @CheckForNull
515    public String deprecatedSince() {
516      return deprecatedSince;
517    }
518
519    public boolean isPost() {
520      return post;
521    }
522
523    public boolean isInternal() {
524      return isInternal;
525    }
526
527    public RequestHandler handler() {
528      return handler;
529    }
530
531    /**
532     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
533     */
534    @CheckForNull
535    public URL responseExample() {
536      return responseExample;
537    }
538
539    /**
540     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
541     */
542    @CheckForNull
543    public String responseExampleAsString() {
544      try {
545        if (responseExample != null) {
546          return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8));
547        }
548        return null;
549      } catch (IOException e) {
550        throw new IllegalStateException("Fail to load " + responseExample, e);
551      }
552    }
553
554    /**
555     * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL)
556     */
557    @CheckForNull
558    public String responseExampleFormat() {
559      if (responseExample != null) {
560        return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile()));
561      }
562      return null;
563    }
564
565    @CheckForNull
566    public Param param(String key) {
567      return params.get(key);
568    }
569
570    public Collection<Param> params() {
571      return params.values();
572    }
573
574    @Override
575    public String toString() {
576      return path;
577    }
578  }
579
580  class NewParam {
581    private String key;
582    private String since;
583    private String deprecatedSince;
584    private String deprecatedKey;
585    private String description;
586    private String exampleValue;
587    private String defaultValue;
588    private boolean required = false;
589    private Set<String> possibleValues = null;
590
591    private NewParam(String key) {
592      this.key = key;
593    }
594
595    public NewParam setSince(@Nullable String since) {
596      this.since = since;
597      return this;
598    }
599
600    public NewParam setDeprecatedSince(@Nullable String deprecatedSince) {
601      this.deprecatedSince = deprecatedSince;
602      return this;
603    }
604
605    /**
606     * @since 5.0
607     */
608    public NewParam setDeprecatedKey(@Nullable String s) {
609      this.deprecatedKey = s;
610      return this;
611    }
612
613    public NewParam setDescription(@Nullable String s) {
614      this.description = s;
615      return this;
616    }
617
618    /**
619     * Is the parameter required or optional ? Default value is false (optional).
620     *
621     * @since 4.4
622     */
623    public NewParam setRequired(boolean b) {
624      this.required = b;
625      return this;
626    }
627
628    /**
629     * @since 4.4
630     */
631    public NewParam setExampleValue(@Nullable Object s) {
632      this.exampleValue = (s != null) ? s.toString() : null;
633      return this;
634    }
635
636    /**
637     * Exhaustive list of possible values when it makes sense, for example
638     * list of severities.
639     *
640     * @since 4.4
641     */
642    public NewParam setPossibleValues(@Nullable Object... values) {
643      return setPossibleValues(values == null ? Collections.emptyList() : Arrays.asList(values));
644    }
645
646    /**
647     * @since 4.4
648     */
649    public NewParam setBooleanPossibleValues() {
650      return setPossibleValues("true", "false", "yes", "no");
651    }
652
653    /**
654     * Exhaustive list of possible values when it makes sense, for example
655     * list of severities.
656     *
657     * @since 4.4
658     */
659    public NewParam setPossibleValues(@Nullable Collection<?> values) {
660      if (values == null || values.isEmpty()) {
661        this.possibleValues = null;
662      } else {
663        this.possibleValues = Sets.newLinkedHashSet();
664        for (Object value : values) {
665          this.possibleValues.add(value.toString());
666        }
667      }
668      return this;
669    }
670
671    /**
672     * @since 4.4
673     */
674    public NewParam setDefaultValue(@Nullable Object o) {
675      this.defaultValue = (o != null) ? o.toString() : null;
676      return this;
677    }
678
679    @Override
680    public String toString() {
681      return key;
682    }
683  }
684
685  enum SelectionMode {
686    SELECTED("selected"), DESELECTED("deselected"), ALL("all");
687
688    private final String paramValue;
689
690    private static final Map<String, SelectionMode> BY_VALUE = Maps.uniqueIndex(Arrays.asList(values()), new Function<SelectionMode, String>() {
691      @Override
692      public String apply(@Nonnull SelectionMode input) {
693        return input.paramValue;
694      }
695    });
696
697    private SelectionMode(String paramValue) {
698      this.paramValue = paramValue;
699    }
700
701    public String value() {
702      return paramValue;
703    }
704
705    public static SelectionMode fromParam(String paramValue) {
706      checkArgument(BY_VALUE.containsKey(paramValue));
707      return BY_VALUE.get(paramValue);
708    }
709
710    public static Collection<String> possibleValues() {
711      return BY_VALUE.keySet();
712    }
713  }
714
715  @Immutable
716  class Param {
717    public static final String TEXT_QUERY = "q";
718    public static final String PAGE = "p";
719    public static final String PAGE_SIZE = "ps";
720    public static final String FIELDS = "f";
721    public static final String SORT = "s";
722    public static final String ASCENDING = "asc";
723    public static final String FACETS = "facets";
724    public static final String SELECTED = "selected";
725
726    private final String key;
727    private final String since;
728    private final String deprecatedSince;
729    private final String deprecatedKey;
730    private final String description;
731    private final String exampleValue;
732    private final String defaultValue;
733    private final boolean required;
734    private final Set<String> possibleValues;
735
736    protected Param(Action action, NewParam newParam) {
737      this.key = newParam.key;
738      this.since = newParam.since;
739      this.deprecatedSince = newParam.deprecatedSince;
740      this.deprecatedKey = newParam.deprecatedKey;
741      this.description = newParam.description;
742      this.exampleValue = newParam.exampleValue;
743      this.defaultValue = newParam.defaultValue;
744      this.required = newParam.required;
745      this.possibleValues = newParam.possibleValues;
746      if (required && defaultValue != null) {
747        throw new IllegalArgumentException(format("Default value must not be set on parameter '%s?%s' as it's marked as required", action, key));
748      }
749    }
750
751    public String key() {
752      return key;
753    }
754
755    /**
756     * @since 5.3
757     */
758    @CheckForNull
759    public String since() {
760      return since;
761    }
762
763    /**
764     * @since 5.3
765     */
766    @CheckForNull
767    public String deprecatedSince() {
768      return deprecatedSince;
769    }
770
771    /**
772     * @since 5.0
773     */
774    @CheckForNull
775    public String deprecatedKey() {
776      return deprecatedKey;
777    }
778
779    @CheckForNull
780    public String description() {
781      return description;
782    }
783
784    /**
785     * @since 4.4
786     */
787    @CheckForNull
788    public String exampleValue() {
789      return exampleValue;
790    }
791
792    /**
793     * Is the parameter required or optional ?
794     *
795     * @since 4.4
796     */
797    public boolean isRequired() {
798      return required;
799    }
800
801    /**
802     * @since 4.4
803     */
804    @CheckForNull
805    public Set<String> possibleValues() {
806      return possibleValues;
807    }
808
809    /**
810     * @since 4.4
811     */
812    @CheckForNull
813    public String defaultValue() {
814      return defaultValue;
815    }
816
817    @Override
818    public String toString() {
819      return key;
820    }
821  }
822
823  /**
824   * Executed once at server startup.
825   */
826  @Override
827  void define(Context context);
828
829}