All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.api.server.ws.WebService Maven / Gradle / Ivy

There is a newer version: 10.14.0.2599
Show newest version
/*
 * Sonar Plugin API
 * Copyright (C) 2009-2023 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.api.server.ws;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.ExtensionPoint;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Objects.requireNonNull;
import static org.sonar.api.utils.Preconditions.checkArgument;
import static org.sonar.api.utils.Preconditions.checkState;

/**
 * Defines a web service.
 * 
*
* The classes implementing this extension point must be declared by {@link org.sonar.api.Plugin}. *
*

How to use

*
 * public class HelloWs implements WebService {
 *   {@literal @}Override
 *   public void define(Context context) {
 *     NewController controller = context.createController("api/hello");
 *     controller.setDescription("Web service example");
 *     // create the URL /api/hello/show
 *     controller.createAction("show")
 *       .setDescription("Entry point")
 *       .setHandler(new RequestHandler() {
 *         {@literal @}Override
 *         public void handle(Request request, Response response) {
 *           // read request parameters and generate response output
 *           response.newJsonWriter()
 *             .beginObject()
 *             .prop("hello", request.mandatoryParam("key"))
 *             .endObject()
 *             .close();
 *         }
 *      })
 *      .createParam("key").setDescription("Example key").setRequired(true);
 *    // important to apply changes
 *    controller.done();
 *   }
 * }
 * 
*

* Since version 5.5, a web service can call another web service to get some data. See {@link Request#localConnector()} * provided by {@link RequestHandler#handle(Request, Response)}. * * @since 4.2 */ @ServerSide @ExtensionPoint public interface WebService extends Definable { class Context { private final Map controllers = new HashMap<>(); /** * Create a new controller. *
* Structure of request URL is http://<server>/<controller path>/<action path>?<parameters>. * * @param path the controller path must not start or end with "/". It is recommended to start with "api/" * and to use lower-case format with underscores, for example "api/coding_rules". Usual actions * are "search", "list", "show", "create" and "delete". * the plural form is recommended - ex: api/projects */ public NewController createController(String path) { return new NewController(this, path); } private void register(NewController newController) { if (controllers.containsKey(newController.path)) { throw new IllegalStateException( format("The web service '%s' is defined multiple times", newController.path)); } controllers.put(newController.path, new Controller(newController)); } @CheckForNull public Controller controller(String key) { return controllers.get(key); } public List controllers() { return Collections.unmodifiableList(new ArrayList<>(controllers.values())); } } class NewController { private final Context context; private final String path; private String description; private String since; private final Map actions = new HashMap<>(); private NewController(Context context, String path) { if (StringUtils.isBlank(path)) { throw new IllegalArgumentException("WS controller path must not be empty"); } if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) { throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path); } this.context = context; this.path = path; } /** * Important - this method must be called in order to apply changes and make the * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()} */ public void done() { context.register(this); } /** * Optional description (accept HTML) */ public NewController setDescription(@Nullable String s) { this.description = s; return this; } /** * Optional version when the controller was created */ public NewController setSince(@Nullable String s) { this.since = s; return this; } public NewAction createAction(String actionKey) { if (actions.containsKey(actionKey)) { throw new IllegalStateException( format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path)); } NewAction action = new NewAction(actionKey); actions.put(actionKey, action); return action; } } @Immutable class Controller { private final String path; private final String description; private final String since; private final Map actions; private Controller(NewController newController) { checkState(!newController.actions.isEmpty(), "At least one action must be declared in the web service '%s'", newController.path); this.path = newController.path; this.description = newController.description; this.since = newController.since; Map mapBuilder = new HashMap<>(); for (NewAction newAction : newController.actions.values()) { mapBuilder.put(newAction.key, new Action(this, newAction)); } this.actions = Collections.unmodifiableMap(mapBuilder); } public String path() { return path; } @CheckForNull public String description() { return description; } @CheckForNull public String since() { return since; } @CheckForNull public Action action(String actionKey) { return actions.get(actionKey); } public Collection actions() { return actions.values(); } /** * Returns true if all the actions are for internal use * * @see org.sonar.api.server.ws.WebService.Action#isInternal() * @since 4.3 */ public boolean isInternal() { for (Action action : actions()) { if (!action.isInternal()) { return false; } } return true; } } class NewAction { private final String key; private static final String PAGE_PARAM_DESCRIPTION = "1-based page number"; private String deprecatedKey; private String description; private String since; private String deprecatedSince; private boolean post = false; private boolean isInternal = false; private RequestHandler handler; private Map newParams = new HashMap<>(); private URL responseExample = null; private List changelog = new ArrayList<>(); private NewAction(String key) { this.key = key; } public NewAction setDeprecatedKey(@Nullable String s) { this.deprecatedKey = s; return this; } /** * Used in Orchestrator */ public NewAction setDescription(@Nullable String description) { this.description = description; return this; } /** * @since 5.6 */ public NewAction setDescription(@Nullable String description, Object... descriptionArgument) { this.description = description == null ? null : String.format(description, descriptionArgument); return this; } public NewAction setSince(@Nullable String s) { this.since = s; return this; } /** * @since 5.3 */ public NewAction setDeprecatedSince(@Nullable String deprecatedSince) { this.deprecatedSince = deprecatedSince; return this; } public NewAction setPost(boolean b) { this.post = b; return this; } /** * Internal actions are not displayed by default in the web api documentation. They are * displayed only when the check-box "Show Internal API" is selected. By default * an action is not internal. */ public NewAction setInternal(boolean b) { this.isInternal = b; return this; } public NewAction setHandler(RequestHandler h) { this.handler = h; return this; } /** * Link to the document containing an example of response. Content must be UTF-8 encoded. *
* Example: *

     *   newAction.setResponseExample(getClass().getResource("/org/sonar/my-ws-response-example.json"));
     * 
* * @since 4.4 */ public NewAction setResponseExample(@Nullable URL url) { this.responseExample = url; return this; } /** * List of changes made to the contract or valuable insight. Example: changes to the response format. * Calling this method multiple times will not override previously set changes - all changes will be added. * * @since 6.4 */ public NewAction setChangelog(Change... changes) { requireNonNull(changes); Arrays.stream(changes) .filter(Objects::nonNull) .forEach(this.changelog::add); return this; } public NewParam createParam(String paramKey) { checkState(!newParams.containsKey(paramKey), "The parameter '%s' is defined multiple times in the action '%s'", paramKey, key); NewParam newParam = new NewParam(paramKey); newParams.put(paramKey, newParam); return newParam; } /** * Add predefined parameters related to pagination of results. */ public NewAction addPagingParams(int defaultPageSize) { createParam(Param.PAGE) .setDescription(PAGE_PARAM_DESCRIPTION) .setExampleValue("42") .setDefaultValue("1"); createParam(Param.PAGE_SIZE) .setDescription("Page size. Must be greater than 0.") .setExampleValue("20") .setDefaultValue(String.valueOf(defaultPageSize)); return this; } /** * Add predefined parameters related to pagination of results with a maximum page size. * Note the maximum is a documentation only feature. It does not check anything. */ public NewAction addPagingParams(int defaultPageSize, int maxPageSize) { createPageParam(); createPageSize(defaultPageSize, maxPageSize); return this; } public NewParam createPageParam() { return createParam(Param.PAGE) .setDescription(PAGE_PARAM_DESCRIPTION) .setExampleValue("42") .setDefaultValue("1"); } public NewParam createPageSize(int defaultPageSize, int maxPageSize) { return createParam(Param.PAGE_SIZE) .setDefaultValue(String.valueOf(defaultPageSize)) .setMaximumValue(maxPageSize) .setDescription("Page size. Must be greater than 0 and less or equal than " + maxPageSize) .setExampleValue("20"); } /** * Add predefined parameters related to pagination of results with a maximum page size. * * @since 7.1 */ public NewAction addPagingParamsSince(int defaultPageSize, int maxPageSize, String version) { createParam(Param.PAGE) .setDescription(PAGE_PARAM_DESCRIPTION) .setExampleValue("42") .setDefaultValue("1") .setSince(version); createParam(Param.PAGE_SIZE) .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize) .setDefaultValue(String.valueOf(defaultPageSize)) .setMaximumValue(maxPageSize) .setExampleValue("20") .setSince(version); return this; } /** * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is * used to restrict the number of fields returned in JSON response. */ public NewAction addFieldsParam(Collection possibleValues) { createFieldsParam(possibleValues); return this; } public NewParam createFieldsParam(Collection possibleValues) { return createParam(Param.FIELDS) .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.") .setPossibleValues(possibleValues); } /** * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is * used to search for a subset of fields containing the supplied string. *

* The fields must be in the plural form (ex: "names", "keys"). *

*/ public NewAction addSearchQuery(String exampleValue, String... pluralFields) { createSearchQuery(exampleValue, pluralFields); return this; } /** * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is * used to search for a subset of fields containing the supplied string. *

* The fields must be in the plural form (ex: "names", "keys"). *

*/ public NewParam createSearchQuery(String exampleValue, String... pluralFields) { String actionDescription = format("Limit search to %s that contain the supplied string.", String.join(" or ", pluralFields)); return createParam(Param.TEXT_QUERY) .setDescription(actionDescription) .setExampleValue(exampleValue); } /** * Add predefined parameters related to sorting of results. */ public NewAction addSortParams(Collection possibleValues, @Nullable V defaultValue, boolean defaultAscending) { createSortParams(possibleValues, defaultValue, defaultAscending); return this; } /** * Add predefined parameters related to sorting of results. */ public NewParam createSortParams(Collection possibleValues, @Nullable V defaultValue, boolean defaultAscending) { createParam(Param.ASCENDING) .setDescription("Ascending sort") .setBooleanPossibleValues() .setDefaultValue(defaultAscending); return createParam(Param.SORT) .setDescription("Sort field") .setDefaultValue(defaultValue) .setPossibleValues(possibleValues); } /** * Add 'selected=(selected|deselected|all)' for select-list oriented WS. */ public NewAction addSelectionModeParam() { createParam(Param.SELECTED) .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " + "or all items with their selection status (selected=all).") .setDefaultValue(SelectionMode.SELECTED.value()) .setPossibleValues(SelectionMode.possibleValues()); return this; } } @Immutable class Action { private static final Logger LOGGER = Loggers.get(Action.class); private final String key; private final String deprecatedKey; private final String path; private final String description; private final String since; private final String deprecatedSince; private final boolean post; private final boolean isInternal; private final RequestHandler handler; private final Map params; private final URL responseExample; private final List changelog; private Action(Controller controller, NewAction newAction) { this.key = newAction.key; this.deprecatedKey = newAction.deprecatedKey; this.path = format("%s/%s", controller.path(), key); this.description = newAction.description; this.since = newAction.since; this.deprecatedSince = newAction.deprecatedSince; this.post = newAction.post; this.isInternal = newAction.isInternal; this.responseExample = newAction.responseExample; this.handler = newAction.handler; this.changelog = newAction.changelog; checkState(this.handler != null, "RequestHandler is not set on action %s", path); logWarningIf(this.description == null || this.description.isEmpty(), "Description is not set on action " + path); logWarningIf(this.since == null || this.since.isEmpty(), "Since is not set on action " + path); logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path); Map paramsBuilder = new HashMap<>(); for (NewParam newParam : newAction.newParams.values()) { paramsBuilder.put(newParam.key, new Param(this, newParam)); } this.params = Collections.unmodifiableMap(paramsBuilder); } private static void logWarningIf(boolean condition, String message) { if (condition) { LOGGER.warn(message); } } public String key() { return key; } public String deprecatedKey() { return deprecatedKey; } public String path() { return path; } @CheckForNull public String description() { return description; } /** * Set if different than controller. */ @CheckForNull public String since() { return since; } @CheckForNull public String deprecatedSince() { return deprecatedSince; } public boolean isPost() { return post; } /** * @see NewAction#setChangelog(Change...) * @since 6.4 */ public List changelog() { return changelog; } /** * @see NewAction#setInternal(boolean) */ public boolean isInternal() { return isInternal; } public RequestHandler handler() { return handler; } /** * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) */ @CheckForNull public URL responseExample() { return responseExample; } /** * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) */ @CheckForNull public String responseExampleAsString() { try { if (responseExample != null) { return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8)); } return null; } catch (IOException e) { throw new IllegalStateException("Fail to load " + responseExample, e); } } /** * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) */ @CheckForNull public String responseExampleFormat() { if (responseExample != null) { return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile())); } return null; } @CheckForNull public Param param(String key) { return params.get(key); } public Collection params() { return params.values(); } @Override public String toString() { return path; } } class NewParam { private String key; private String since; private String deprecatedSince; private String deprecatedKey; private String deprecatedKeySince; private String description; private String exampleValue; private String defaultValue; private boolean required = false; private boolean internal = false; private Set possibleValues = null; private Integer maxValuesAllowed; private Integer maximumLength; private Integer minimumLength; private Integer maximumValue; private NewParam(String key) { this.key = key; } /** * @see Param#since() * @since 5.3 */ public NewParam setSince(@Nullable String since) { this.since = since; return this; } /** * @since 5.3 */ public NewParam setDeprecatedSince(@Nullable String deprecatedSince) { this.deprecatedSince = deprecatedSince; return this; } /** * @param deprecatedSince Version when the old key was replaced/deprecated. Ex: 5.6 * @see Param#deprecatedKey() * @since 6.4 */ public NewParam setDeprecatedKey(@Nullable String key, @Nullable String deprecatedSince) { this.deprecatedKey = key; this.deprecatedKeySince = deprecatedSince; return this; } public NewParam setDescription(@Nullable String description) { this.description = description; return this; } /** * @see Param#description() * @since 5.6 */ public NewParam setDescription(@Nullable String description, Object... descriptionArgument) { this.description = description == null ? null : String.format(description, descriptionArgument); return this; } /** * Is the parameter required or optional ? Default value is false (optional). * * @see Param#isRequired() * @since 4.4 */ public NewParam setRequired(boolean b) { this.required = b; return this; } /** * Internal parameters are not displayed by default in the web api documentation. They are * displayed only when the check-box "Show Internal API" is selected. By default * a parameter is not internal. * * @see Param#isInternal() * @since 6.2 */ public NewParam setInternal(boolean b) { this.internal = b; return this; } /** * @see Param#exampleValue() * @since 4.4 */ public NewParam setExampleValue(@Nullable Object s) { this.exampleValue = (s != null) ? s.toString() : null; return this; } /** * Exhaustive list of possible values when it makes sense, for example * list of severities. * * @see Param#possibleValues() * @since 4.4 */ public NewParam setPossibleValues(@Nullable T... values) { return setPossibleValues(values == null ? Collections.emptyList() : asList(values)); } /** * Shortcut for {@code setPossibleValues("true", "false", "yes", "no")} * * @since 4.4 */ public NewParam setBooleanPossibleValues() { return setPossibleValues("true", "false", "yes", "no"); } /** * Exhaustive list of possible values when it makes sense, for example * list of severities. * * @see Param#possibleValues() * @since 4.4 */ public NewParam setPossibleValues(@Nullable Collection values) { if (values == null || values.isEmpty()) { this.possibleValues = null; } else { this.possibleValues = new LinkedHashSet<>(); for (Object value : values) { this.possibleValues.add(value.toString()); } } return this; } /** * @see Param#defaultValue() * @since 4.4 */ public NewParam setDefaultValue(@Nullable Object o) { this.defaultValue = (o != null) ? o.toString() : null; return this; } /** * @see Param#maxValuesAllowed() * @since 6.4 */ public NewParam setMaxValuesAllowed(@Nullable Integer maxValuesAllowed) { this.maxValuesAllowed = maxValuesAllowed; return this; } /** * @see Param#maximumLength() * @since 7.0 */ public NewParam setMaximumLength(@Nullable Integer maximumLength) { this.maximumLength = maximumLength; return this; } /** * @see Param#minimumLength() * @since 7.0 */ public NewParam setMinimumLength(@Nullable Integer minimumLength) { this.minimumLength = minimumLength; return this; } /** * @see Param#maximumValue() * @since 7.0 */ public NewParam setMaximumValue(@Nullable Integer maximumValue) { this.maximumValue = maximumValue; return this; } @Override public String toString() { return key; } } enum SelectionMode { SELECTED("selected"), DESELECTED("deselected"), ALL("all"); private final String paramValue; private static final Map BY_VALUE = stream(values()) .collect(Collectors.toMap(v -> v.paramValue, v -> v)); SelectionMode(String paramValue) { this.paramValue = paramValue; } public String value() { return paramValue; } public static SelectionMode fromParam(String paramValue) { checkArgument(BY_VALUE.containsKey(paramValue)); return BY_VALUE.get(paramValue); } public static Collection possibleValues() { return BY_VALUE.keySet(); } } @Immutable class Param { public static final String TEXT_QUERY = "q"; public static final String PAGE = "p"; public static final String PAGE_SIZE = "ps"; public static final String FIELDS = "f"; public static final String SORT = "s"; public static final String ASCENDING = "asc"; public static final String FACETS = "facets"; public static final String SELECTED = "selected"; private final String key; private final String since; private final String deprecatedSince; private final String deprecatedKey; private final String deprecatedKeySince; private final String description; private final String exampleValue; private final String defaultValue; private final boolean required; private final boolean internal; private final Set possibleValues; private final Integer maximumLength; private final Integer minimumLength; private final Integer maximumValue; private final Integer maxValuesAllowed; protected Param(Action action, NewParam newParam) { this.key = newParam.key; this.since = newParam.since; this.deprecatedSince = newParam.deprecatedSince; this.deprecatedKey = newParam.deprecatedKey; this.deprecatedKeySince = newParam.deprecatedKeySince; this.description = newParam.description; this.exampleValue = newParam.exampleValue; this.defaultValue = newParam.defaultValue; this.required = newParam.required; this.internal = newParam.internal; this.possibleValues = newParam.possibleValues; this.maxValuesAllowed = newParam.maxValuesAllowed; this.maximumLength = newParam.maximumLength; this.minimumLength = newParam.minimumLength; this.maximumValue = newParam.maximumValue; checkArgument(!required || defaultValue == null, "Default value must not be set on parameter '%s?%s' as it's marked as required", action, key); } public String key() { return key; } /** * @since 5.3 */ @CheckForNull public String since() { return since; } /** * @since 5.3 */ @CheckForNull public String deprecatedSince() { return deprecatedSince; } /** * @since 5.0 */ @CheckForNull public String deprecatedKey() { return deprecatedKey; } /** * @since 6.4 */ @CheckForNull public String deprecatedKeySince() { return deprecatedKeySince; } @CheckForNull public String description() { return description; } /** * @since 4.4 */ @CheckForNull public String exampleValue() { return exampleValue; } /** * Is the parameter required or optional ? * * @since 4.4 */ public boolean isRequired() { return required; } /** * Is the parameter internal ? * * @see NewParam#setInternal(boolean) * @since 6.2 */ public boolean isInternal() { return internal; } /** * @since 4.4 */ @CheckForNull public Set possibleValues() { return possibleValues; } /** * @since 4.4 */ @CheckForNull public String defaultValue() { return defaultValue; } /** * Specify the maximum number of values allowed when using {@link Request#multiParam(String)} * * @since 6.4 */ public Integer maxValuesAllowed() { return maxValuesAllowed; } /** * Specify the maximum length of the value used in this parameter * * @since 7.0 */ @CheckForNull public Integer maximumLength() { return maximumLength; } /** * Specify the minimum length of the value used in this parameter * * @since 7.0 */ @CheckForNull public Integer minimumLength() { return minimumLength; } /** * Specify the maximum value of the numeric variable used in this parameter * * @since 7.0 */ @CheckForNull public Integer maximumValue() { return maximumValue; } @Override public String toString() { return key; } } /** * Executed once at server startup. */ @Override void define(Context context); }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy