]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7759 Java WS are not executed by Rails
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 27 Jun 2016 09:07:27 +0000 (11:07 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 29 Jun 2016 06:41:53 +0000 (08:41 +0200)
16 files changed:
it/it-tests/src/test/java/it/component/ProjectSearchTest.java
server/sonar-server/src/main/java/org/sonar/server/app/TomcatConnectors.java
server/sonar-server/src/main/java/org/sonar/server/app/TomcatContexts.java
server/sonar-server/src/main/java/org/sonar/server/platform/RoutesFilter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java
server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceFilter.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java
server/sonar-server/src/test/java/org/sonar/server/platform/RoutesFilterTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java
server/sonar-server/src/test/java/org/sonar/server/ws/WebServiceFilterTest.java [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/java_ws_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
server/sonar-web/src/main/webapp/WEB-INF/lib/java_ws_routing.rb
server/sonar-web/src/main/webapp/WEB-INF/web.xml

index 0bd2172702292e4be5ee9cd91db5f63cfa44f492..01827629ba1e3abe4ed27bd6c3aff44d4fb2a624 100644 (file)
@@ -19,6 +19,9 @@
  */
 package it.component;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.build.SonarRunner;
 import it.Category4Suite;
@@ -35,9 +38,6 @@ import org.junit.Test;
 import org.junit.experimental.categories.Category;
 import util.QaOnly;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static util.ItUtils.projectDir;
-
 @Category(QaOnly.class)
 public class ProjectSearchTest {
 
@@ -58,7 +58,7 @@ public class ProjectSearchTest {
     SonarRunner build = SonarRunner.create(projectDir("shared/xoo-sample"));
     orchestrator.executeBuild(build);
 
-    String url = orchestrator.getServer().getUrl() + "/api/projects?key=sample&versions=true";
+    String url = orchestrator.getServer().getUrl() + "/api/projects/index?key=sample&versions=true";
     HttpClient httpclient = new DefaultHttpClient();
     try {
       HttpGet get = new HttpGet(url);
index bcf14fed9ecbb2253cab399ecc1dbd6038b7e37d..986f2a8a4f2c1d87698b6f37a862196697ae696e 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.server.app;
 
+import static com.google.common.collect.FluentIterable.from;
+import static java.util.Arrays.asList;
+
 import com.google.common.base.Predicates;
 import java.util.HashSet;
 import java.util.List;
@@ -29,9 +32,6 @@ import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.Tomcat;
 import org.sonar.process.Props;
 
-import static com.google.common.collect.FluentIterable.from;
-import static java.util.Arrays.asList;
-
 /**
  * Configuration of Tomcat connectors
  */
@@ -41,6 +41,7 @@ class TomcatConnectors {
   public static final String HTTP_PROTOCOL = "HTTP/1.1";
   public static final String AJP_PROTOCOL = "AJP/1.3";
   public static final int MAX_HTTP_HEADER_SIZE_BYTES = 48 * 1024;
+  private static final int MAX_POST_SIZE = -1;
 
   private TomcatConnectors() {
     // only static stuff
@@ -82,6 +83,7 @@ class TomcatConnectors {
       connector = newConnector(props, HTTP_PROTOCOL, "http");
       configureMaxHttpHeaderSize(connector);
       connector.setPort(port);
+      connector.setMaxPostSize(MAX_POST_SIZE);
     }
     return connector;
   }
index abd0dd2df4ee8c78019dba4ee1fb93ad01c52788..56dcf66f5ff7d1107bfc019cb8a5be00b1b20522 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.server.app;
 
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.isEmpty;
+
 import com.google.common.annotations.VisibleForTesting;
 import java.io.File;
 import java.io.IOException;
@@ -33,9 +36,6 @@ import org.sonar.api.utils.log.Loggers;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
 
-import static java.lang.String.format;
-import static org.apache.commons.lang.StringUtils.isEmpty;
-
 /**
  * Configures Tomcat contexts:
  * <ul>
@@ -111,6 +111,7 @@ public class TomcatContexts {
       context.setUseNaming(false);
       context.setDelegate(true);
       context.setJarScanner(new NullJarScanner());
+      context.setAllowCasualMultipartParsing(true);
       return context;
     } catch (ServletException e) {
       throw new IllegalStateException("Fail to configure webapp from " + dir, e);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/RoutesFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/RoutesFilter.java
new file mode 100644 (file)
index 0000000..2cb7ad6
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.server.platform;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class RoutesFilter implements Filter {
+
+  private static final String EMPTY = "";
+  private static final String BATCH_WS = "/batch";
+  private static final String API_SOURCES_WS = "/api/sources";
+
+  @Override
+  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest request = (HttpServletRequest) servletRequest;
+    HttpServletResponse response = (HttpServletResponse) servletResponse;
+    String path = request.getRequestURI().replaceFirst(request.getContextPath(), EMPTY);
+    if (path.startsWith(BATCH_WS + "/") && path.endsWith(".jar")) {
+      // Scanner is still using /batch/file.jar url
+      response.sendRedirect(format("%s%s/file?name=%s", request.getContextPath(), BATCH_WS, path.replace(BATCH_WS + "/", EMPTY)));
+    } else if ("/batch_bootstrap/index".equals(path)) {
+      // Scanner is still using /batch_bootstrap url
+      response.sendRedirect(format("%s%s/index", request.getContextPath(), BATCH_WS));
+    } else if (API_SOURCES_WS.equals(path)) {
+      // SONAR-7852 /api/sources?resource url is still used
+      response.sendRedirect(format("%s%s/index?%s", request.getContextPath(), API_SOURCES_WS, request.getQueryString()));
+    } else {
+      chain.doFilter(request, response);
+    }
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    // Nothing
+  }
+
+  @Override
+  public void destroy() {
+    // Nothing
+  }
+}
index ef7a11a4c0c8650938b17e8b3103d80894500f39..f1dcdddb3a8e262cf42a18b922cf3ff114915181 100644 (file)
@@ -295,6 +295,7 @@ import org.sonar.server.view.index.ViewIndex;
 import org.sonar.server.view.index.ViewIndexDefinition;
 import org.sonar.server.view.index.ViewIndexer;
 import org.sonar.server.ws.WebServiceEngine;
+import org.sonar.server.ws.WebServiceFilter;
 import org.sonar.server.ws.WebServicesWs;
 import org.sonar.server.ws.WsResponseCommonFormat;
 
@@ -494,6 +495,7 @@ public class PlatformLevel4 extends PlatformLevel {
       // web services
       WebServiceEngine.class,
       WebServicesWs.class,
+      WebServiceFilter.class,
 
       // localization
       L10nWs.class,
index 1fb52f66f9ee121d7872076dd2e80df6b3c96731..f003f7d7bab67a47a0d0af90b28a9e29861d6ba9 100644 (file)
@@ -24,6 +24,7 @@ import org.sonar.server.platform.ws.MigrateDbAction;
 import org.sonar.server.platform.ws.StatusAction;
 import org.sonar.server.platform.ws.SystemWs;
 import org.sonar.server.ws.WebServiceEngine;
+import org.sonar.server.ws.WebServiceFilter;
 import org.sonar.server.ws.WebServicesWs;
 
 public class PlatformLevelSafeMode extends PlatformLevel {
@@ -44,6 +45,7 @@ public class PlatformLevelSafeMode extends PlatformLevel {
       WebServicesWs.class,
 
       // WS engine
-      WebServiceEngine.class);
+      WebServiceEngine.class,
+      WebServiceFilter.class);
   }
 }
index d2b535b16b2ca0188edb9e835368574387bc3248..c171df2a078a286fa99c23096d54f28f93c52616 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.server.ws;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static org.apache.commons.lang.StringUtils.substringAfterLast;
+
 import com.google.common.collect.ImmutableMap;
 import com.google.common.net.HttpHeaders;
 import java.io.InputStream;
@@ -26,26 +29,21 @@ import java.util.Locale;
 import java.util.Map;
 import javax.annotation.CheckForNull;
 import javax.servlet.http.HttpServletRequest;
-import org.jruby.RubyFile;
+import javax.servlet.http.Part;
 import org.sonar.api.server.ws.internal.ValidatingRequest;
 import org.sonarqube.ws.MediaTypes;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static org.apache.commons.lang.StringUtils.substringAfterLast;
-
 public class ServletRequest extends ValidatingRequest {
 
   private final HttpServletRequest source;
-  private final Map<String, Object> params;
 
   static final Map<String, String> SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX = ImmutableMap.of(
     "json", MediaTypes.JSON,
     "protobuf", MediaTypes.PROTOBUF,
     "text", MediaTypes.TXT);
 
-  public ServletRequest(HttpServletRequest source, Map<String, Object> params) {
+  public ServletRequest(HttpServletRequest source) {
     this.source = source;
-    this.params = params;
   }
 
   @Override
@@ -64,28 +62,22 @@ public class ServletRequest extends ValidatingRequest {
 
   @Override
   public boolean hasParam(String key) {
-    return source.getParameterMap().containsKey(key) || params.keySet().contains(key);
+    return source.getParameterMap().containsKey(key);
   }
 
   @Override
   protected String readParam(String key) {
-    String value = source.getParameter(key);
-    if (value == null) {
-      Object string = params.get(key);
-      if (string != null && string instanceof String) {
-        value = (String) string;
-      }
-    }
-    return value;
+    return source.getParameter(key);
   }
 
   @Override
   protected InputStream readInputStreamParam(String key) {
-    Object file = params.get(key);
-    if (file != null && file instanceof RubyFile) {
-      return ((RubyFile) file).getInStream();
+    try {
+      Part part = source.getPart(key);
+      return part == null ? null : part.getInputStream();
+    } catch (Exception e) {
+      throw new IllegalStateException("Can't read file part", e);
     }
-    return null;
   }
 
   @Override
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceFilter.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceFilter.java
new file mode 100644 (file)
index 0000000..7bc8ce4
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.server.ws;
+
+import static java.lang.String.format;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.server.ws.RailsHandler;
+import org.sonar.api.web.ServletFilter;
+
+/**
+ * This filter is used to execute Java WS.
+ *
+ * If the url match a Java WS, the output of the WS is returned and no other filers are executed.
+ * If the url doesn't match a Java WS, then it's calling remaining filters (for instance to execute Rails WS).
+ */
+public class WebServiceFilter extends ServletFilter {
+
+  private final WebServiceEngine webServiceEngine;
+  private final Map<String, WsUrl> wsUrls = new HashMap<>();
+  private final List<String> includeUrls = new ArrayList<>();
+
+  public WebServiceFilter(WebServiceEngine webServiceEngine) {
+    this.webServiceEngine = webServiceEngine;
+    webServiceEngine.controllers().stream()
+      .forEach(controller -> controller.actions().stream()
+        .filter(action -> !(action.handler() instanceof RailsHandler) && !(action.handler() instanceof ServletFilterHandler))
+        .forEach(action -> {
+          String url = "/" + action.path();
+          wsUrls.put(url, new WsUrl(controller.path(), action.key()));
+          includeUrls.add(url + "*");
+        }));
+  }
+
+  @Override
+  public UrlPattern doGetPattern() {
+    return UrlPattern.builder()
+      .includes(includeUrls)
+      .build();
+  }
+
+  @Override
+  public void doFilter(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest request = (HttpServletRequest) servletRequest;
+    HttpServletResponse response = (HttpServletResponse) servletResponse;
+    String path = request.getRequestURI().replaceFirst(request.getContextPath(), "");
+
+    String[] pathWithExtension = getPathWithExtension(path);
+    WsUrl url = wsUrls.get(pathWithExtension[0]);
+    if (url == null) {
+      throw new IllegalStateException(format("Unknown path : %s", path));
+    }
+    ServletRequest wsRequest = new ServletRequest(request);
+    ServletResponse wsResponse = new ServletResponse();
+    webServiceEngine.execute(wsRequest, wsResponse, url.getController(), url.getAction(), pathWithExtension[1]);
+    writeResponse(wsResponse, response);
+  }
+
+  private static void writeResponse(ServletResponse wsResponse, HttpServletResponse response) throws IOException {
+    // SONAR-6964 WS should not be cached by browser
+    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+    for (String header : wsResponse.getHeaderNames()) {
+      response.setHeader(header, wsResponse.getHeader(header));
+    }
+
+    response.setContentType(wsResponse.stream().mediaType());
+    response.setStatus(wsResponse.stream().httpStatus());
+
+    OutputStream responseOutput = response.getOutputStream();
+    ByteArrayOutputStream wsOutputStream = (ByteArrayOutputStream) wsResponse.stream().output();
+    IOUtils.write(wsOutputStream.toByteArray(), responseOutput);
+    responseOutput.flush();
+    responseOutput.close();
+  }
+
+  private static String[] getPathWithExtension(String fullPath) {
+    String path = fullPath;
+    String extension = null;
+    int semiColonPos = fullPath.lastIndexOf('.');
+    if (semiColonPos > 0) {
+      path = fullPath.substring(0, semiColonPos);
+      extension = fullPath.substring(semiColonPos + 1);
+    }
+    return new String[] {path, extension};
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    // Nothing to do
+  }
+
+  @Override
+  public void destroy() {
+    // Nothing to do
+  }
+
+  private static class WsUrl {
+    private final String controller;
+    private final String action;
+
+    WsUrl(String controller, String action) {
+      this.controller = controller;
+      this.action = action;
+    }
+
+    String getController() {
+      return controller;
+    }
+
+    String getAction() {
+      return action;
+    }
+  }
+}
index ee7bd281f38946116be40f21e9bb3f7e905dd2ca..d7cc1c5e696cafa5a719e41533edb8e0ad90bc66 100644 (file)
  */
 package org.sonar.server.app;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
 import com.google.common.collect.ImmutableMap;
 import java.net.InetAddress;
 import java.util.Map;
@@ -49,12 +55,6 @@ import org.mockito.ArgumentMatcher;
 import org.mockito.Mockito;
 import org.sonar.process.Props;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
 public class TomcatConnectorsTest {
 
   Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
@@ -206,8 +206,23 @@ public class TomcatConnectorsTest {
     verifyConnectorProperty(tomcat, "http", "maxHttpHeaderSize", TomcatConnectors.MAX_HTTP_HEADER_SIZE_BYTES);
   }
 
+  @Test
+  public void test_max_post_size_for_http_connection() throws Exception {
+    Properties properties = new Properties();
+
+    Props props = new Props(properties);
+    TomcatConnectors.configure(tomcat, props);
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getMaxPostSize() == -1;
+      }
+    }));
+  }
+
   private static void verifyConnectorProperty(Tomcat tomcat, final String connectorScheme,
-    final String property, final Object propertyValue) {
+                                              final String property, final Object propertyValue) {
     verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
       @Override
       public boolean matches(Object o) {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/RoutesFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/RoutesFilterTest.java
new file mode 100644 (file)
index 0000000..a9c4630
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.server.platform;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RoutesFilterTest {
+
+  HttpServletRequest request = mock(HttpServletRequest.class);
+  HttpServletResponse response = mock(HttpServletResponse.class);
+  FilterChain chain = mock(FilterChain.class);
+
+  RoutesFilter underTest = new RoutesFilter();
+
+  @Before
+  public void setUp() throws Exception {
+    when(request.getContextPath()).thenReturn("/sonarqube");
+  }
+
+  @Test
+  public void send_redirect_when_url_contains_batch_with_jar() throws Exception {
+    when(request.getRequestURI()).thenReturn("/batch/file.jar");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sonarqube/batch/file?name=file.jar");
+    verifyZeroInteractions(chain);
+  }
+
+  @Test
+  public void send_redirect_when_url_contains_batch_bootstrap() throws Exception {
+    when(request.getRequestURI()).thenReturn("/batch_bootstrap/index");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sonarqube/batch/index");
+    verifyZeroInteractions(chain);
+  }
+
+  @Test
+  public void send_redirect_when_url_contains_api_sources() throws Exception {
+    when(request.getRequestURI()).thenReturn("/api/sources");
+    when(request.getQueryString()).thenReturn("resource=my.project");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sonarqube/api/sources/index?resource=my.project");
+    verifyZeroInteractions(chain);
+  }
+
+  @Test
+  public void does_not_redirect_and_execute_remaining_filter_on_unknown_path() throws Exception {
+    when(request.getRequestURI()).thenReturn("/api/issues/search");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(chain).doFilter(request, response);
+    verifyZeroInteractions(response);
+  }
+}
index 09520f948d4c8eafafe19958ae90b8843e62c9d9..0ea4f49addc9a61cba788899c75d1e37d780a4f7 100644 (file)
  */
 package org.sonar.server.ws;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import com.google.common.collect.ImmutableMap;
 import com.google.common.net.HttpHeaders;
-import java.util.Collections;
+import java.io.InputStream;
 import javax.servlet.http.HttpServletRequest;
-import org.jruby.RubyFile;
+import javax.servlet.http.Part;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.sonarqube.ws.MediaTypes;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 public class ServletRequestTest {
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   HttpServletRequest source = mock(HttpServletRequest.class);
 
   @Test
   public void call_method() {
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    ServletRequest request = new ServletRequest(source);
     request.method();
     verify(source).getMethod();
   }
@@ -47,20 +53,20 @@ public class ServletRequestTest {
   public void getMediaType() throws Exception {
     when(source.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaTypes.JSON);
     when(source.getRequestURI()).thenReturn("/path/to/resource/search");
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    ServletRequest request = new ServletRequest(source);
     assertThat(request.getMediaType()).isEqualTo(MediaTypes.JSON);
   }
 
   @Test
   public void default_media_type_is_octet_stream() throws Exception {
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    ServletRequest request = new ServletRequest(source);
     when(source.getRequestURI()).thenReturn("/path/to/resource/search");
     assertThat(request.getMediaType()).isEqualTo(MediaTypes.DEFAULT);
   }
 
   @Test
   public void media_type_taken_in_url_first() throws Exception {
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    ServletRequest request = new ServletRequest(source);
     when(source.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaTypes.JSON);
     when(source.getRequestURI()).thenReturn("/path/to/resource/search.protobuf");
     assertThat(request.getMediaType()).isEqualTo(MediaTypes.PROTOBUF);
@@ -69,49 +75,51 @@ public class ServletRequestTest {
   @Test
   public void has_param_from_source() {
     when(source.getParameterMap()).thenReturn(ImmutableMap.of("param", new String[] {"value"}));
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
-    assertThat(request.hasParam("param")).isTrue();
-  }
-
-  @Test
-  public void has_param_from_params() {
-    ServletRequest request = new ServletRequest(source, ImmutableMap.<String, Object>of("param", "value"));
+    ServletRequest request = new ServletRequest(source);
     assertThat(request.hasParam("param")).isTrue();
   }
 
   @Test
   public void read_param_from_source() {
     when(source.getParameter("param")).thenReturn("value");
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    ServletRequest request = new ServletRequest(source);
     assertThat(request.readParam("param")).isEqualTo("value");
   }
 
   @Test
-  public void read_param_from_param() {
-    ServletRequest request = new ServletRequest(source, ImmutableMap.<String, Object>of("param1", "value", "param2", 1));
-    assertThat(request.readParam("param1")).isEqualTo("value");
-    assertThat(request.readParam("param2")).isNull();
-    assertThat(request.readParam("param3")).isNull();
+  public void read_input_stream() throws Exception {
+    InputStream file = mock(InputStream.class);
+    Part part = mock(Part.class);
+    when(part.getInputStream()).thenReturn(file);
+    when(source.getPart("param1")).thenReturn(part);
+
+    ServletRequest request = new ServletRequest(source);
+    assertThat(request.readInputStreamParam("param1")).isEqualTo(file);
+
+    assertThat(request.readInputStreamParam("param2")).isNull();
   }
 
   @Test
-  public void read_input_stream() {
-    RubyFile file = mock(RubyFile.class);
-    ServletRequest request = new ServletRequest(source, ImmutableMap.<String, Object>of("param1", file, "param2", "value"));
+  public void throw_ISE_when_invalid_part() throws Exception {
+    InputStream file = mock(InputStream.class);
+    Part part = mock(Part.class);
+    when(part.getInputStream()).thenReturn(file);
+    doThrow(IllegalArgumentException.class).when(source).getPart("param1");
+    ServletRequest request = new ServletRequest(source);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Can't read file part");
     request.readInputStreamParam("param1");
-    verify(file).getInStream();
-
-    assertThat(request.readInputStreamParam("param2")).isNull();
   }
 
   @Test
   public void to_string() {
     when(source.getRequestURL()).thenReturn(new StringBuffer("http:localhost:9000/api/issues"));
-    ServletRequest request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    ServletRequest request = new ServletRequest(source);
     assertThat(request.toString()).isEqualTo("http:localhost:9000/api/issues");
 
     when(source.getQueryString()).thenReturn("components=sonar");
-    request = new ServletRequest(source, Collections.<String, Object>emptyMap());
+    request = new ServletRequest(source);
     assertThat(request.toString()).isEqualTo("http:localhost:9000/api/issues?components=sonar");
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/WebServiceFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/WebServiceFilterTest.java
new file mode 100644 (file)
index 0000000..8d3f011
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.server.ws;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.ws.WebServiceFilterTest.WsUrl.newWsUrl;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.ws.RailsHandler;
+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;
+
+public class WebServiceFilterTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  WebServiceEngine webServiceEngine = mock(WebServiceEngine.class);
+
+  HttpServletRequest request = mock(HttpServletRequest.class);
+  HttpServletResponse response = mock(HttpServletResponse.class);
+  FilterChain chain = mock(FilterChain.class);
+  ServletOutputStream responseOutput = mock(ServletOutputStream.class);
+
+  WebServiceFilter underTest;
+
+  @Before
+  public void setUp() throws Exception {
+    when(request.getContextPath()).thenReturn("");
+    when(response.getOutputStream()).thenReturn(responseOutput);
+  }
+
+  @Test
+  public void do_get_pattern() throws Exception {
+    initWebServiceEngine(
+      newWsUrl("api/issues", "search"),
+      newWsUrl("batch", "index"),
+      newWsUrl("api/authentication", "login").setHandler(ServletFilterHandler.INSTANCE),
+      newWsUrl("api/resources", "index").setHandler(RailsHandler.INSTANCE));
+
+    assertThat(underTest.doGetPattern().matches("/api/issues/search")).isTrue();
+    assertThat(underTest.doGetPattern().matches("/api/issues/search.protobuf")).isTrue();
+    assertThat(underTest.doGetPattern().matches("/batch/index")).isTrue();
+
+    assertThat(underTest.doGetPattern().matches("/api/resources/index")).isFalse();
+    assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isFalse();
+    assertThat(underTest.doGetPattern().matches("/foo")).isFalse();
+  }
+
+  @Test
+  public void execute() throws Exception {
+    initWebServiceEngine(newWsUrl("api/issues", "search"));
+    when(request.getRequestURI()).thenReturn("/api/issues/search");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(webServiceEngine).execute(any(ServletRequest.class), any(org.sonar.server.ws.ServletResponse.class), eq("api/issues"), eq("search"), eq(null));
+
+    verify(response).setStatus(200);
+    verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+
+    verify(responseOutput).flush();
+    verify(responseOutput).close();
+    verifyZeroInteractions(chain);
+  }
+
+  @Test
+  public void execute_with_extension() throws Exception {
+    initWebServiceEngine(newWsUrl("api/issues", "search"));
+    when(request.getRequestURI()).thenReturn("/api/issues/search.protobuff");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(webServiceEngine).execute(any(ServletRequest.class), any(org.sonar.server.ws.ServletResponse.class), eq("api/issues"), eq("search"), eq("protobuff"));
+  }
+
+  @Test
+  public void fail_on_unknown_path() throws Exception {
+    initWebServiceEngine(newWsUrl("api/issues", "search"));
+    when(request.getRequestURI()).thenReturn("/api/resources/index");
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Unknown path : /api/resources/index");
+    underTest.doFilter(request, response, chain);
+  }
+
+  private void initWebServiceEngine(WsUrl... wsUrls) {
+    List<WebService.Controller> controllers = new ArrayList<>();
+
+    for (WsUrl wsUrl : wsUrls) {
+      String controller = wsUrl.getController();
+      WebService.Controller wsController = mock(WebService.Controller.class);
+      when(wsController.path()).thenReturn(controller);
+
+      List<WebService.Action> actions = new ArrayList<>();
+      for (String action : wsUrl.getActions()) {
+        WebService.Action wsAction = mock(WebService.Action.class);
+        when(wsAction.path()).thenReturn(controller + "/" + action);
+        when(wsAction.key()).thenReturn(action);
+        when(wsAction.handler()).thenReturn(wsUrl.getRequestHandler());
+        actions.add(wsAction);
+      }
+      when(wsController.actions()).thenReturn(actions);
+      controllers.add(wsController);
+    }
+    when(webServiceEngine.controllers()).thenReturn(controllers);
+    underTest = new WebServiceFilter(webServiceEngine);
+  }
+
+  static final class WsUrl {
+    private String controller;
+    private String[] actions;
+    private RequestHandler requestHandler = EmptyRequestHandler.INSTANCE;
+
+    WsUrl(String controller, String... actions) {
+      this.controller = controller;
+      this.actions = actions;
+    }
+
+    WsUrl setHandler(RequestHandler requestHandler) {
+      this.requestHandler = requestHandler;
+      return this;
+    }
+
+    String getController() {
+      return controller;
+    }
+
+    String[] getActions() {
+      return actions;
+    }
+
+    RequestHandler getRequestHandler() {
+      return requestHandler;
+    }
+
+    static WsUrl newWsUrl(String controller, String... actions) {
+      return new WsUrl(controller, actions);
+    }
+  }
+
+  private enum EmptyRequestHandler implements RequestHandler {
+    INSTANCE;
+
+    @Override
+    public void handle(Request request, Response response) throws Exception {
+      // Nothing to do
+    }
+  }
+
+}
index cb74a0700b32b2d5279de97fd51b942ded074ddd..245ed2b2a0f61575779264e5190670f434f9ebf3 100644 (file)
 # since 4.2
 class Api::JavaWsController < Api::ApiController
 
-  before_filter :check_authentication, :unless => ['skip_authentication_check_for_batch']
-
-  # no need to check if WS can be accessed when DB is not up-to-date, this is dealt with in
-  # Platform and ServerComponents classes
-  skip_before_filter :check_database_version
-
-  def index
-    ws_request = Java::OrgSonarServerWs::ServletRequest.new(servlet_request, params.to_java)
-    ws_response = Java::OrgSonarServerWs::ServletResponse.new()
-    engine = Java::OrgSonarServerPlatform::Platform.component(Java::OrgSonarServerWs::WebServiceEngine.java_class)
-    engine.execute(ws_request, ws_response, params[:wspath], params[:wsaction], params[:responseFormat])
-
-    ws_response.getHeaderNames().to_a.each do |name|
-      response.header[name] = ws_response.getHeader(name)
-    end
-
-    # response is already written to HttpServletResponse
-    render :text => ws_response.stream().output().toByteArray(),
-           :status => ws_response.stream().httpStatus(),
-           :content_type => ws_response.stream().mediaType()
-  end
-
   def redirect_to_ws_listing
-    redirect_to :action => 'index', :wspath => 'api/webservices', :wsaction => 'list'
-  end
-
-
-  def skip_authentication_check_for_batch
-    (params[:wspath]=='batch' && params[:wsaction]=='index') ||
-      (params[:wspath]=='batch' && params[:wsaction]=='file') ||
-      (params[:wspath]=='api/system' && params[:wsaction]=='db_migration_status') ||
-      (params[:wspath]=='api/system' && params[:wsaction]=='migrate_db') ||
-      (params[:wspath]=='api/system' && params[:wsaction]=='status')
+    redirect_to :controller => 'api/webservices', :action => 'list'
   end
 
 end
index 77d8bcd6e14fe664d129f7dd5135b8712d8997bc..1af5e16edacc16a68e6476098c8b12190f14e12e 100644 (file)
@@ -8,13 +8,8 @@ ActionController::Routing::Routes.draw do |map|
 
   map.connect 'api', :controller => 'api/java_ws', :action => 'redirect_to_ws_listing'
 
-  # deprecated, sonar-runner should use batch/index and batch/file?name=xxx
-  map.connect 'batch_bootstrap/index', :controller => 'api/java_ws', :action => 'index', :wspath => 'batch', :wsaction => 'index'
-  map.connect 'batch/:name', :controller => 'api/java_ws', :action => 'index', :wspath => 'batch', :wsaction => 'file', :requirements => { :name => /.*/ }
-
   map.connect 'api/server/:action', :controller => 'api/server'
   map.connect 'api/resoures', :controller => 'api/resources', :action => 'index'
-  map.connect 'api/sources', :controller => 'api/sources', :action => 'index'
 
   map.resources 'properties', :path_prefix => 'api', :controller => 'api/properties', :requirements => { :id => /.*/ }
 
index f0cfacd6287f3b2cd0730b2c651a34aacab60609..96c45d64c20d139428f77f6d9f32206e81b9c4e3 100644 (file)
@@ -28,20 +28,6 @@ module ActionController
           eval(ws.getTemplate())
           prepend_route("api/plugins/#{ws.getId()}/:action/:id", {:controller => "api/#{ws.getId()}", :requirements => {:id => /.*/}})
         end
-
-        # Full Java web services
-        ws_engine = Java::OrgSonarServerPlatform::Platform.component(Java::OrgSonarServerWs::WebServiceEngine.java_class)
-        ws_engine.controllers().each do |controller|
-          controller.actions.each do |action|
-            if (!action.handler().java_kind_of?(Java::OrgSonarApiServerWs::RailsHandler))
-              prepend_route("#{controller.path()}/#{action.key()}.:responseFormat/:id", {:controller => 'api/java_ws', :action => 'index', :wsaction => action.key(), :wspath => controller.path()})
-              prepend_route("#{controller.path()}/#{action.key()}/:id", {:controller => 'api/java_ws', :action => 'index', :wsaction => action.key(), :wspath => controller.path()})
-              if action.key()=='index'
-                prepend_route("#{controller.path()}", {:controller => 'api/java_ws', :action => 'index', :wsaction => action.key(), :wspath => controller.path()})
-              end
-            end
-          end
-        end
       end
     end
   end
index d4402205359c1d0521f29a924ec2d46093432fff..bed9e8b5cece964b3c4706933a53873ec11c7281 100644 (file)
       <param-value>/images,/javascripts,/stylesheets</param-value>
     </init-param>
   </filter>
+  <filter>
+    <filter-name>RoutesFilter</filter-name>
+    <filter-class>org.sonar.server.platform.RoutesFilter</filter-class>
+  </filter>
 
   <!-- order of execution is important -->
   <filter-mapping>
     <filter-name>ProfilingFilter</filter-name>
     <url-pattern>/*</url-pattern>
   </filter-mapping>
+  <filter-mapping>
+    <filter-name>RoutesFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+  </filter-mapping>
   <filter-mapping>
     <filter-name>UserSessionFilter</filter-name>
     <url-pattern>/*</url-pattern>