From 2d511c42c97c6507b4cee94966c26e266a4c43f6 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Sat, 11 Jan 2014 14:32:39 +0100 Subject: [PATCH] SONAR-5010 internal extension point to implement Java web services Not available yet in web routing. --- .../java/org/sonar/server/ws/Request.java | 51 ++++ .../org/sonar/server/ws/RequestHandler.java | 31 +++ .../java/org/sonar/server/ws/Response.java | 84 ++++++ .../java/org/sonar/server/ws/WebService.java | 244 ++++++++++++++++++ .../org/sonar/server/ws/package-info.java | 23 ++ .../java/org/sonar/server/ws/RequestTest.java | 44 ++++ .../org/sonar/server/ws/WebServiceTest.java | 171 ++++++++++++ 7 files changed, 648 insertions(+) create mode 100644 sonar-server/src/main/java/org/sonar/server/ws/Request.java create mode 100644 sonar-server/src/main/java/org/sonar/server/ws/RequestHandler.java create mode 100644 sonar-server/src/main/java/org/sonar/server/ws/Response.java create mode 100644 sonar-server/src/main/java/org/sonar/server/ws/WebService.java create mode 100644 sonar-server/src/main/java/org/sonar/server/ws/package-info.java create mode 100644 sonar-server/src/test/java/org/sonar/server/ws/RequestTest.java create mode 100644 sonar-server/src/test/java/org/sonar/server/ws/WebServiceTest.java diff --git a/sonar-server/src/main/java/org/sonar/server/ws/Request.java b/sonar-server/src/main/java/org/sonar/server/ws/Request.java new file mode 100644 index 00000000000..483f0ff7f4a --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/ws/Request.java @@ -0,0 +1,51 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.ws; + +import javax.annotation.CheckForNull; +import java.util.Map; + +/** + * @since 4.2 + */ +public class Request { + + private final Map params; + + public Request(Map params) { + this.params = params; + } + + @CheckForNull + public String param(String key) { + return params.get(key); + } + + @CheckForNull + public Integer intParam(String key) { + String s = params.get(key); + return s == null ? null : Integer.parseInt(s); + } + + public int intParam(String key, int defaultValue) { + String s = params.get(key); + return s == null ? defaultValue : Integer.parseInt(s); + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/ws/RequestHandler.java b/sonar-server/src/main/java/org/sonar/server/ws/RequestHandler.java new file mode 100644 index 00000000000..9d94e4312e3 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/ws/RequestHandler.java @@ -0,0 +1,31 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.ws; + +import org.sonar.api.ServerExtension; + +/** + * @since 4.2 + */ +public interface RequestHandler extends ServerExtension { + + void handle(Request request, Response response); + +} diff --git a/sonar-server/src/main/java/org/sonar/server/ws/Response.java b/sonar-server/src/main/java/org/sonar/server/ws/Response.java new file mode 100644 index 00000000000..93e37377619 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/ws/Response.java @@ -0,0 +1,84 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.ws; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +/** + * HTTP response + * + * @since 4.2 + */ +public class Response { + + /** + * HTTP Status-Code 200: OK. + */ + public static final int HTTP_OK = 200; + + /** + * HTTP Status-Code 400: Bad Request. + */ + public static final int HTTP_BAD_REQUEST = 400; + + /** + * HTTP Status-Code 401: Unauthorized. + */ + public static final int HTTP_UNAUTHORIZED = 401; + + /** + * HTTP Status-Code 403: Forbidden. + */ + public static final int HTTP_FORBIDDEN = 403; + + /** + * HTTP Status-Code 404: Not Found. + */ + public static final int HTTP_NOT_FOUND = 404; + + /** + * HTTP Status-Code 500: Internal Server Error. + */ + public static final int HTTP_INTERNAL_ERROR = 500; + + + private int httpStatus = HTTP_OK; + private String body; + + @CheckForNull + public String body() { + return body; + } + + public Response setBody(@Nullable String body) { + this.body = body; + return this; + } + + public int status() { + return httpStatus; + } + + public Response setStatus(int httpStatus) { + this.httpStatus = httpStatus; + return this; + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/ws/WebService.java b/sonar-server/src/main/java/org/sonar/server/ws/WebService.java new file mode 100644 index 00000000000..46432041680 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/ws/WebService.java @@ -0,0 +1,244 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.ws; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import org.sonar.api.ServerExtension; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @since 4.2 + */ +public interface WebService extends ServerExtension { + + static class Context { + private final Map controllers = Maps.newHashMap(); + + public NewController newController(String key) { + return new NewController(this, key); + } + + private void register(NewController newController) { + if (controllers.containsKey(newController.key)) { + throw new IllegalStateException( + String.format("The web service '%s' is defined multiple times", newController.key) + ); + } + controllers.put(newController.key, new Controller(newController)); + } + + @CheckForNull + public Controller controller(String key) { + return controllers.get(key); + } + + // TODO sort by keys + public List controllers() { + return ImmutableList.copyOf(controllers.values()); + } + } + + static class NewController { + private final Context context; + private final String key; + private String description, since; + private boolean api = false; + private final Map actions = Maps.newHashMap(); + + private NewController(Context context, String key) { + this.context = context; + this.key = key; + } + + public void done() { + context.register(this); + } + + public NewController setDescription(@Nullable String s) { + this.description = s; + return this; + } + + public NewController setApi(boolean b) { + this.api = b; + return this; + } + + public NewController setSince(@Nullable String s) { + this.since = s; + return this; + } + + public NewAction newAction(String actionKey) { + if (actions.containsKey(actionKey)) { + throw new IllegalStateException( + String.format("The action '%s' is defined multiple times in the web service '%s'", actionKey, key) + ); + } + NewAction action = new NewAction(actionKey); + actions.put(actionKey, action); + return action; + } + } + + static class Controller { + private final String key, description, since; + private final boolean api; + private final Map actions; + + private Controller(NewController newController) { + if (newController.actions.isEmpty()) { + throw new IllegalStateException( + String.format("At least one action must be declared in the web service '%s'", newController.key) + ); + } + this.key = newController.key; + this.description = newController.description; + this.since = newController.since; + this.api = newController.api; + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + for (NewAction newAction : newController.actions.values()) { + mapBuilder.put(newAction.key, new Action(this, newAction)); + } + this.actions = mapBuilder.build(); + } + + public String key() { + return key; + } + + public String path() { + return String.format("%s/%s", WebServiceEngine.BASE_PATH, key); + } + + @CheckForNull + public String description() { + return description; + } + + public boolean isApi() { + return api; + } + + @CheckForNull + public String since() { + return since; + } + + @CheckForNull + public Action action(String actionKey) { + return actions.get(actionKey); + } + + public Collection actions() { + return actions.values(); + } + } + + static class NewAction { + private final String key; + private String description, since; + private boolean post = false; + private RequestHandler handler; + + private NewAction(String key) { + this.key = key; + } + + public NewAction setDescription(@Nullable String s) { + this.description = s; + return this; + } + + public NewAction setSince(@Nullable String s) { + this.since = s; + return this; + } + + public NewAction setPost(boolean b) { + this.post = b; + return this; + } + + public NewAction setHandler(RequestHandler h) { + this.handler = h; + return this; + } + } + + static class Action { + private final Controller controller; + private final String key, description, since; + private final boolean post; + private final RequestHandler handler; + + private Action(Controller controller, NewAction newAction) { + this.controller = controller; + this.key = newAction.key; + this.description = newAction.description; + this.since = newAction.since; + this.post = newAction.post; + this.handler = newAction.handler; + } + + public String key() { + return key; + } + + public String path() { + return String.format("%s/%s", controller.path(), key); + } + + @CheckForNull + public String description() { + return description; + } + + /** + * Set if different than controller. + */ + @CheckForNull + public String since() { + return since; + } + + public boolean isPost() { + return post; + } + + @CheckForNull + public RequestHandler handler() { + return handler; + } + } + + /** + * Executed at server startup. + */ + void define(Context context); + +} diff --git a/sonar-server/src/main/java/org/sonar/server/ws/package-info.java b/sonar-server/src/main/java/org/sonar/server/ws/package-info.java new file mode 100644 index 00000000000..6b4332971cf --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/ws/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.ws; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-server/src/test/java/org/sonar/server/ws/RequestTest.java b/sonar-server/src/test/java/org/sonar/server/ws/RequestTest.java new file mode 100644 index 00000000000..dbd196d16ac --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/ws/RequestTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.ws; + +import com.google.common.collect.ImmutableMap; +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class RequestTest { + @Test + public void string_params() { + Request request = new Request(ImmutableMap.of("foo", "bar")); + assertThat(request.param("none")).isNull(); + assertThat(request.param("foo")).isEqualTo("bar"); + } + + @Test + public void int_params() { + Request request = new Request(ImmutableMap.of("foo", "123")); + assertThat(request.intParam("none")).isNull(); + assertThat(request.intParam("foo")).isEqualTo(123); + + assertThat(request.intParam("none", 456)).isEqualTo(456); + assertThat(request.intParam("foo", 456)).isEqualTo(123); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/ws/WebServiceTest.java b/sonar-server/src/test/java/org/sonar/server/ws/WebServiceTest.java new file mode 100644 index 00000000000..705c7060720 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/ws/WebServiceTest.java @@ -0,0 +1,171 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.ws; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; + +public class WebServiceTest { + + static class MetricWebService implements WebService { + boolean showCalled = false, createCalled = false; + @Override + public void define(Context context) { + NewController newController = context.newController("metric") + .setApi(true) + .setDescription("Metrics") + .setSince("3.2"); + newController.newAction("show") + .setDescription("Show metric") + .setSince("4.1") + .setHandler(new RequestHandler() { + @Override + public void handle(Request request, Response response) { + show(request, response); + } + }); + newController.newAction("create") + .setDescription("Create metric") + .setPost(true) + .setHandler(new RequestHandler() { + @Override + public void handle(Request request, Response response) { + create(request, response); + } + }); + newController.done(); + } + + void show(Request request, Response response) { + showCalled = true; + } + + void create(Request request, Response response) { + createCalled = true; + } + } + + + WebService.Context context = new WebService.Context(); + + @Test + public void no_web_services_by_default() { + assertThat(context.controllers()).isEmpty(); + assertThat(context.controller("metric")).isNull(); + } + + @Test + public void define_web_service() { + MetricWebService metricWs = new MetricWebService(); + + metricWs.define(context); + + WebService.Controller controller = context.controller("metric"); + assertThat(controller).isNotNull(); + assertThat(controller.key()).isEqualTo("metric"); + assertThat(controller.description()).isEqualTo("Metrics"); + assertThat(controller.since()).isEqualTo("3.2"); + assertThat(controller.isApi()).isTrue(); + assertThat(controller.path()).isEqualTo("ws/metric"); + assertThat(controller.actions()).hasSize(2); + WebService.Action showAction = controller.action("show"); + assertThat(showAction).isNotNull(); + assertThat(showAction.key()).isEqualTo("show"); + assertThat(showAction.description()).isEqualTo("Show metric"); + assertThat(showAction.handler()).isNotNull(); + assertThat(showAction.since()).isEqualTo("4.1"); + assertThat(showAction.isPost()).isFalse(); + assertThat(showAction.path()).isEqualTo("ws/metric/show"); + WebService.Action createAction = controller.action("create"); + assertThat(createAction).isNotNull(); + assertThat(createAction.key()).isEqualTo("create"); + assertThat(createAction.isPost()).isTrue(); + } + + @Test + public void fail_if_duplicated_ws_keys() { + MetricWebService metricWs = new MetricWebService(); + metricWs.define(context); + try { + new WebService() { + @Override + public void define(Context context) { + NewController newController = context.newController("metric"); + newController.newAction("delete"); + newController.done(); + } + }.define(context); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("The web service 'metric' is defined multiple times"); + } + } + + @Test + public void fail_if_duplicated_action_keys() { + try { + new WebService() { + @Override + public void define(Context context) { + NewController newController = context.newController("rule"); + newController.newAction("create"); + newController.newAction("delete"); + newController.newAction("delete"); + newController.done(); + } + }.define(context); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("The action 'delete' is defined multiple times in the web service 'rule'"); + } + } + + @Test + public void fail_if_no_actions() { + try { + new WebService() { + @Override + public void define(Context context) { + context.newController("rule").done(); + } + }.define(context); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("At least one action must be declared in the web service 'rule'"); + } + } + + @Test + public void handle_request() { + MetricWebService metricWs = new MetricWebService(); + metricWs.define(context); + + assertThat(metricWs.showCalled).isFalse(); + assertThat(metricWs.createCalled).isFalse(); + context.controller("metric").action("show").handler().handle(mock(Request.class), mock(Response.class)); + assertThat(metricWs.showCalled).isTrue(); + assertThat(metricWs.createCalled).isFalse(); + context.controller("metric").action("create").handler().handle(mock(Request.class), mock(Response.class)); + assertThat(metricWs.createCalled).isTrue(); + } +} -- 2.39.5