From: Simon Brandhof Date: Tue, 29 Apr 2014 19:23:43 +0000 (+0200) Subject: Add response example to WebService extension point X-Git-Tag: 4.4-RC1~1337 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=4fdd8cb3ba9ac65ef9413f4a006b9f4aa6b758d9;p=sonarqube.git Add response example to WebService extension point --- diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RailsHandler.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RailsHandler.java index 53c9ad12a4d..87f798b5250 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RailsHandler.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RailsHandler.java @@ -21,13 +21,13 @@ package org.sonar.api.server.ws; /** - * Used to declare web services that are still implemented in rails. + * Used to declare web services that are still implemented in Ruby on Rails. * * @since 4.4 */ public class RailsHandler implements RequestHandler { - public static final RequestHandler INSTANCE = new RailsHandler(){}; + public static final RequestHandler INSTANCE = new RailsHandler(); private RailsHandler() { // Nothing diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RequestHandler.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RequestHandler.java index 94a3e050ae9..84eb2af2e90 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RequestHandler.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RequestHandler.java @@ -26,6 +26,6 @@ import org.sonar.api.ServerExtension; */ public interface RequestHandler extends ServerExtension { - void handle(Request request, Response response); + void handle(Request request, Response response) throws Exception; } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java index 3f75ee3777f..3da8be70df1 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java @@ -19,16 +19,19 @@ */ package org.sonar.api.server.ws; +import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.sonar.api.ServerExtension; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; - +import java.io.IOException; +import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Map; @@ -248,6 +251,8 @@ public interface WebService extends ServerExtension { private boolean post = false, isInternal = false; private RequestHandler handler; private Map newParams = Maps.newHashMap(); + private URL responseExample = null; + private String responseExampleFormat = null; private NewAction(String key) { this.key = key; @@ -278,6 +283,31 @@ public interface WebService extends ServerExtension { 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; + } + + /** + * Used only if {@link #setResponseExample(java.net.URL)} is set. Example of values: "xml", "json", "txt", "csv". + * + * @since 4.4 + */ + public NewAction setResponseExampleFormat(@Nullable String format) { + this.responseExampleFormat = format; + return this; + } + public NewParam createParam(String paramKey) { if (newParams.containsKey(paramKey)) { throw new IllegalStateException( @@ -305,6 +335,8 @@ public interface WebService extends ServerExtension { private final boolean post, isInternal; private final RequestHandler handler; private final Map params; + private final URL responseExample; + private final String responseExampleFormat; private Action(Controller controller, NewAction newAction) { this.key = newAction.key; @@ -313,9 +345,11 @@ public interface WebService extends ServerExtension { this.since = StringUtils.defaultIfBlank(newAction.since, controller.since); this.post = newAction.post; this.isInternal = newAction.isInternal; + this.responseExample = newAction.responseExample; + this.responseExampleFormat = newAction.responseExampleFormat; if (newAction.handler == null) { - throw new IllegalStateException("RequestHandler is not set on action " + path); + throw new IllegalArgumentException("RequestHandler is not set on action " + path); } this.handler = newAction.handler; @@ -359,6 +393,37 @@ public interface WebService extends ServerExtension { 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 IOUtils.toString(responseExample, Charsets.UTF_8); + } + return null; + } catch (IOException e) { + throw new IllegalStateException("Fail to load " + responseExample, e); + } + } + + /** + * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExampleFormat(String) + */ + @CheckForNull + public String responseExampleFormat() { + return responseExampleFormat; + } + @CheckForNull public Param param(String key) { return params.get(key); @@ -390,6 +455,7 @@ public interface WebService extends ServerExtension { /** * Is the parameter required or optional ? Default value is false (optional). + * * @since 4.4 */ public NewParam setRequired(boolean b) { @@ -408,6 +474,7 @@ public interface WebService extends ServerExtension { /** * Exhaustive list of possible values when it makes sense, for example * list of severities. + * * @since 4.4 */ public NewParam setPossibleValues(@Nullable String... s) { @@ -463,6 +530,7 @@ public interface WebService extends ServerExtension { /** * Is the parameter required or optional ? + * * @since 4.4 */ public boolean isRequired() { diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java index c089bfbf2e0..431e21a2e9f 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java @@ -19,8 +19,12 @@ */ package org.sonar.api.server.ws; +import org.apache.commons.lang.StringUtils; import org.junit.Test; +import java.net.MalformedURLException; +import java.net.URL; + import static org.fest.assertions.Assertions.assertThat; import static org.fest.assertions.Fail.fail; import static org.mockito.Mockito.mock; @@ -50,6 +54,8 @@ public class WebServiceTest { .setSince("4.1") .setPost(true) .setInternal(true) + .setResponseExampleFormat("txt") + .setResponseExample(getClass().getResource("/org/sonar/api/server/ws/WebServiceTest/response-example.txt")) .setHandler(new RequestHandler() { @Override public void handle(Request request, Response response) { @@ -142,7 +148,7 @@ public class WebServiceTest { } }.define(context); fail(); - } catch (IllegalStateException e) { + } catch (IllegalArgumentException e) { assertThat(e).hasMessage("RequestHandler is not set on action rule/show"); } } @@ -298,4 +304,44 @@ public class WebServiceTest { assertThat(context.controller("api/rule").isInternal()).isTrue(); } + + @Test + public void response_example() { + MetricWebService metricWs = new MetricWebService(); + metricWs.define(context); + WebService.Action action = context.controller("api/metric").action("create"); + + assertThat(action.responseExampleFormat()).isEqualTo("txt"); + assertThat(action.responseExample()).isNotNull(); + assertThat(StringUtils.trim(action.responseExampleAsString())).isEqualTo("example of WS response"); + } + + @Test + public void fail_to_open_response_example() { + WebService ws = new WebService() { + @Override + public void define(Context context) { + try { + NewController controller = context.createController("foo"); + controller + .createAction("bar") + .setHandler(mock(RequestHandler.class)) + .setResponseExample(new URL("file:/does/not/exist")); + controller.done(); + + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + }; + ws.define(context); + + WebService.Action action = context.controller("foo").action("bar"); + try { + action.responseExampleAsString(); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Fail to load file:/does/not/exist"); + } + } } diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/server/ws/WebServiceTest/response-example.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/server/ws/WebServiceTest/response-example.txt new file mode 100644 index 00000000000..21c78ddf02b --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/server/ws/WebServiceTest/response-example.txt @@ -0,0 +1 @@ +example of WS response diff --git a/sonar-server/src/main/java/org/sonar/server/rule2/RuleService.java b/sonar-server/src/main/java/org/sonar/server/rule2/RuleService.java index 197edb791cf..35d1c0ee18b 100644 --- a/sonar-server/src/main/java/org/sonar/server/rule2/RuleService.java +++ b/sonar-server/src/main/java/org/sonar/server/rule2/RuleService.java @@ -19,8 +19,6 @@ */ package org.sonar.server.rule2; -import org.apache.commons.beanutils.BeanUtils; - import org.sonar.api.ServerComponent; import org.sonar.api.rule.RuleKey; import org.sonar.core.rule.RuleDao; @@ -28,7 +26,6 @@ import org.sonar.core.rule.RuleDto; import org.sonar.server.search.Hit; import javax.annotation.CheckForNull; - import java.util.Collection; import java.util.Collections; @@ -40,7 +37,7 @@ public class RuleService implements ServerComponent { private RuleDao dao; private RuleIndex index; - public RuleService(RuleDao dao, RuleIndex index){ + public RuleService(RuleDao dao, RuleIndex index) { this.dao = dao; this.index = index; } @@ -48,23 +45,23 @@ public class RuleService implements ServerComponent { @CheckForNull public Rule getByKey(RuleKey key) { Hit hit = index.getByKey(key); - if(hit != null){ + if (hit != null) { return toRule(hit); } else { return null; } } - public Collection search(RuleQuery query){ + public Collection search(RuleQuery query) { return Collections.emptyList(); } - public static Rule toRule(RuleDto ruleDto){ + public static Rule toRule(RuleDto ruleDto) { return new RuleImpl(); } - public static Rule toRule(Hit hit){ + public static Rule toRule(Hit hit) { // BeanUtils.setProperty(bean, name, value); return new RuleImpl(); } diff --git a/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java b/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java index 0acdcbe82e3..08463a3ff0a 100644 --- a/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java +++ b/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java @@ -44,6 +44,8 @@ public class ShowAction implements RequestHandler { .setDescription("Get source code. Parameter 'output' with value 'raw' is missing before being marked as a public WS.") .setSince("4.2") .setInternal(true) + .setResponseExampleFormat("json") + .setResponseExample(getClass().getResource("/org/sonar/server/source/ws/example-show.json")) .setHandler(this); action diff --git a/sonar-server/src/main/java/org/sonar/server/ws/ListingWs.java b/sonar-server/src/main/java/org/sonar/server/ws/ListingWs.java index 010f3f19d84..c67c5f107f2 100644 --- a/sonar-server/src/main/java/org/sonar/server/ws/ListingWs.java +++ b/sonar-server/src/main/java/org/sonar/server/ws/ListingWs.java @@ -19,14 +19,17 @@ */ package org.sonar.server.ws; +import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.collect.Ordering; +import org.apache.commons.io.IOUtils; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.RequestHandler; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.text.JsonWriter; +import java.io.IOException; import java.util.List; /** @@ -39,20 +42,56 @@ public class ListingWs implements WebService { @Override public void define(final Context context) { - NewController controller = context.createController("api/webservices") - .setDescription("List web services") - .setSince("4.2"); - controller.createAction("list") + NewController controller = context + .createController("api/webservices") + .setDescription("List web services"); + defineList(context, controller); + defineResponseExample(context, controller); + controller.done(); + } + + private void defineList(final Context context, NewController controller) { + controller + .createAction("list") + .setSince("4.2") .setHandler(new RequestHandler() { @Override public void handle(Request request, Response response) { - list(context.controllers(), response); + handleList(context.controllers(), response); } }); - controller.done(); } - void list(List controllers, Response response) { + private void defineResponseExample(final Context context, NewController controller) { + NewAction action = controller + .createAction("responseExample") + .setHandler(new RequestHandler() { + @Override + public void handle(Request request, Response response) throws Exception { + Controller controller = context.controller(request.mandatoryParam("controller")); + Action action = controller.action(request.mandatoryParam("action")); + handleResponseExample(action, response); + } + }); + action.createParam("controller").setRequired(true); + action.createParam("action").setRequired(true); + } + + private void handleResponseExample(Action action, Response response) throws IOException { + if (action.responseExample() != null) { + response + .newJsonWriter() + .beginObject() + .prop("format", action.responseExampleFormat()) + .prop("example", IOUtils.toString(action.responseExample(), Charsets.UTF_8)) + .endObject() + .close(); + } else { + response.noContent(); + } + } + + void handleList(List controllers, Response response) { JsonWriter writer = response.newJsonWriter(); writer.beginObject(); writer.name("webServices").beginArray(); @@ -64,14 +103,14 @@ public class ListingWs implements WebService { } }); for (Controller controller : ordering.sortedCopy(controllers)) { - write(writer, controller); + writeController(writer, controller); } writer.endArray(); writer.endObject(); writer.close(); } - private void write(JsonWriter writer, Controller controller) { + private void writeController(JsonWriter writer, Controller controller) { writer.beginObject(); writer.prop("path", controller.path()); writer.prop("since", controller.since()); @@ -84,19 +123,20 @@ public class ListingWs implements WebService { }); writer.name("actions").beginArray(); for (Action action : ordering.sortedCopy(controller.actions())) { - write(writer, action); + writeAction(writer, action); } writer.endArray(); writer.endObject(); } - private void write(JsonWriter writer, Action action) { + private void writeAction(JsonWriter writer, Action action) { writer.beginObject(); writer.prop("key", action.key()); writer.prop("description", action.description()); writer.prop("since", action.since()); writer.prop("internal", action.isInternal()); writer.prop("post", action.isPost()); + writer.prop("hasResponseExample", action.responseExample()!=null); if (!action.params().isEmpty()) { // sort parameters by key Ordering ordering = Ordering.natural().onResultOf(new Function() { @@ -106,14 +146,14 @@ public class ListingWs implements WebService { }); writer.name("params").beginArray(); for (Param param : ordering.sortedCopy(action.params())) { - write(writer, param); + writeParam(writer, param); } writer.endArray(); } writer.endObject(); } - private void write(JsonWriter writer, Param param) { + private void writeParam(JsonWriter writer, Param param) { writer.beginObject(); writer.prop("key", param.key()); writer.prop("description", param.description()); diff --git a/sonar-server/src/main/resources/org/sonar/server/source/ws/example-show.json b/sonar-server/src/main/resources/org/sonar/server/source/ws/example-show.json new file mode 100644 index 00000000000..f4218223887 --- /dev/null +++ b/sonar-server/src/main/resources/org/sonar/server/source/ws/example-show.json @@ -0,0 +1,13 @@ +{ + "source": { + "20": "package org.sonar.check;", + "21": "", + "22": "public enum Priority {", + "23": " /**", + "24": " * WARNING : DO NOT CHANGE THE ENUMERATION ORDER", + "25": " * the enum ordinal is used for db persistence" + }, + "scm": { + "20": ["simon.brandhof@gmail.com", "2010-09-06"] + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/source/ws/ShowActionTest.java b/sonar-server/src/test/java/org/sonar/server/source/ws/ShowActionTest.java index 7e45810dbc3..07f8c14edac 100644 --- a/sonar-server/src/test/java/org/sonar/server/source/ws/ShowActionTest.java +++ b/sonar-server/src/test/java/org/sonar/server/source/ws/ShowActionTest.java @@ -19,8 +19,10 @@ */ package org.sonar.server.source.ws; +import org.apache.commons.io.IOUtils; import org.junit.Before; import org.junit.Test; +import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.text.JsonWriter; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.source.SourceService; @@ -67,6 +69,13 @@ public class ShowActionTest { request.execute().assertJson(getClass(), "show_source.json"); } + @Test + public void response_example_exists() throws Exception { + WebService.Action show = tester.controller("api/sources").action("show"); + assertThat(show.responseExampleFormat()).isEqualTo("json"); + assertThat(show.responseExampleAsString()).isNotEmpty(); + } + @Test public void fail_to_show_source_if_no_source_found() throws Exception { String componentKey = "src/Foo.java"; diff --git a/sonar-server/src/test/java/org/sonar/server/ws/ListingWsTest.java b/sonar-server/src/test/java/org/sonar/server/ws/ListingWsTest.java index a6525564341..b60cdbc0ea3 100644 --- a/sonar-server/src/test/java/org/sonar/server/ws/ListingWsTest.java +++ b/sonar-server/src/test/java/org/sonar/server/ws/ListingWsTest.java @@ -38,8 +38,7 @@ public class ListingWsTest { assertThat(controller).isNotNull(); assertThat(controller.path()).isEqualTo("api/webservices"); assertThat(controller.description()).isNotEmpty(); - assertThat(controller.since()).isEqualTo("4.2"); - assertThat(controller.actions()).hasSize(1); + assertThat(controller.actions()).hasSize(2); WebService.Action index = controller.action("list"); assertThat(index).isNotNull(); @@ -48,14 +47,26 @@ public class ListingWsTest { assertThat(index.since()).isEqualTo("4.2"); assertThat(index.isPost()).isFalse(); assertThat(index.isInternal()).isFalse(); + + assertThat(controller.action("responseExample")).isNotNull(); } @Test - public void index() throws Exception { + public void list() throws Exception { WsTester tester = new WsTester(ws, new MetricWebService()); tester.newRequest("api/webservices", "list").execute().assertJson(getClass(), "list.json"); } + @Test + public void response_example() throws Exception { + WsTester tester = new WsTester(ws, new MetricWebService()); + tester + .newRequest("api/webservices", "responseExample") + .setParam("controller", "api/metric") + .setParam("action", "create") + .execute().assertJson(getClass(), "response_example.json"); + } + static class MetricWebService implements WebService { @Override public void define(Context context) { @@ -78,6 +89,8 @@ public class ListingWsTest { .setSince("4.1") .setPost(true) .setInternal(true) + .setResponseExample(getClass().getResource("/org/sonar/server/ws/ListingWsTest/metrics_example.json")) + .setResponseExampleFormat("json") .setHandler(new RequestHandler() { @Override public void handle(Request request, Response response) { diff --git a/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java b/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java index 890699710d1..ec27a53760f 100644 --- a/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java +++ b/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java @@ -87,7 +87,7 @@ public class WsTester { return params.get(key); } - public Result execute() { + public Result execute() throws Exception { TestResponse response = new TestResponse(); action.handler().handle(this, response); return new Result(response); @@ -181,7 +181,7 @@ public class WsTester { * at src/test/resources/org/foo/BarTest/index.json. * * @param clazz the test class - * @param jsonResourceFilename name of the file containing the expected JSON + * @param expectedJsonFilename name of the file containing the expected JSON */ public Result assertJson(Class clazz, String expectedJsonFilename) throws Exception { String path = clazz.getSimpleName() + "/" + expectedJsonFilename; diff --git a/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/list.json b/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/list.json index fe48e902ea3..da0b96b4647 100644 --- a/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/list.json +++ b/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/list.json @@ -1,51 +1,67 @@ -{ - "webServices": [ - { - "path": "api/metric", - "since": "3.2", - "description": "Metrics", - "actions": [ - { - "key": "create", - "description": "Create metric", - "since": "4.1", - "internal": true, - "post": true, - "params": [ - { - "key": "name", - "required": false - }, - { - "key": "severity", - "description": "Severity", - "required": true, - "defaultValue": "BLOCKER", - "exampleValue": "INFO", - "possibleValues": ["BLOCKER", "INFO"] - - } - ] - }, - { - "key": "show", - "since": "3.2", - "internal": false, - "post": false - } - ] - }, - { - "path": "api/webservices", - "since": "4.2", - "description": "List web services", - "actions": [ - { - "key": "list", - "since": "4.2", - "internal": false, - "post": false - } - ] - } - ]} +{"webServices": [ + { + "path": "api/metric", + "since": "3.2", + "description": "Metrics", + "actions": [ + { + "key": "create", + "description": "Create metric", + "since": "4.1", + "internal": true, + "post": true, + "hasResponseExample": true, + "params": [ + { + "key": "name", + "required": false + }, + { + "key": "severity", + "description": "Severity", + "required": true, + "defaultValue": "BLOCKER", + "exampleValue": "INFO", + "possibleValues": ["BLOCKER", "INFO"] + } + ] + }, + { + "key": "show", + "since": "3.2", + "internal": false, + "post": false, + "hasResponseExample": false + } + ] + }, + { + "path": "api/webservices", + "description": "List web services", + "actions": [ + { + "key": "list", + "since": "4.2", + "internal": false, + "post": false, + "hasResponseExample": false + }, + { + "key": "responseExample", + "internal": false, + "post": false, + "hasResponseExample": false, + "params": [ + { + "key": "action", + "required": true + }, + { + "key": "controller", + "required": true + } + ] + } + ] + } +]} diff --git a/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/metrics_example.json b/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/metrics_example.json new file mode 100644 index 00000000000..2516f68de60 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/metrics_example.json @@ -0,0 +1 @@ +{"metrics":[]} diff --git a/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/response_example.json b/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/response_example.json new file mode 100644 index 00000000000..dac39a0c0f5 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/ws/ListingWsTest/response_example.json @@ -0,0 +1 @@ +{"format":"json","example":"{\"metrics\":[]}\n"}