Skip to content

Commit

Permalink
SONAR-7776 Allow Java WS to stream the output
Browse files Browse the repository at this point in the history
  • Loading branch information
julienlancelot committed Jun 29, 2016
1 parent 46125ae commit 0f42fe4
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 215 deletions.
Expand Up @@ -30,9 +30,9 @@
import org.sonar.api.server.ws.WebService.NewAction; import org.sonar.api.server.ws.WebService.NewAction;
import org.sonar.db.DbClient; import org.sonar.db.DbClient;
import org.sonar.db.DbSession; import org.sonar.db.DbSession;
import org.sonarqube.ws.MediaTypes;
import org.sonar.server.qualityprofile.QProfileBackuper; import org.sonar.server.qualityprofile.QProfileBackuper;
import org.sonar.server.qualityprofile.QProfileFactory; import org.sonar.server.qualityprofile.QProfileFactory;
import org.sonarqube.ws.MediaTypes;


public class BackupAction implements QProfileWsAction { public class BackupAction implements QProfileWsAction {


Expand Down Expand Up @@ -66,12 +66,12 @@ public void define(WebService.NewController controller) {
public void handle(Request request, Response response) throws Exception { public void handle(Request request, Response response) throws Exception {
Stream stream = response.stream(); Stream stream = response.stream();
stream.setMediaType(MediaTypes.XML); stream.setMediaType(MediaTypes.XML);
OutputStreamWriter writer = new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8);
DbSession dbSession = dbClient.openSession(false); DbSession dbSession = dbClient.openSession(false);
OutputStreamWriter writer = new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8);
try { try {
String profileKey = QProfileIdentificationParamUtils.getProfileKeyFromParameters(request, profileFactory, dbSession); String profileKey = QProfileIdentificationParamUtils.getProfileKeyFromParameters(request, profileFactory, dbSession);
backuper.backup(profileKey, writer);
response.setHeader("Content-Disposition", String.format("attachment; filename=%s.xml", profileKey)); response.setHeader("Content-Disposition", String.format("attachment; filename=%s.xml", profileKey));
backuper.backup(profileKey, writer);
} finally { } finally {
dbSession.close(); dbSession.close();
IOUtils.closeQuietly(writer); IOUtils.closeQuietly(writer);
Expand Down
Expand Up @@ -52,6 +52,11 @@ public boolean hasParam(String key) {
return localRequest.hasParam(key); return localRequest.hasParam(key);
} }


@Override
public String getPath() {
return localRequest.getPath();
}

@Override @Override
public String method() { public String method() {
return localRequest.getMethod(); return localRequest.getMethod();
Expand Down
Expand Up @@ -100,4 +100,9 @@ private static String mediaTypeFromUrl(String url) {
String formatSuffix = substringAfterLast(url, "."); String formatSuffix = substringAfterLast(url, ".");
return SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX.get(formatSuffix.toLowerCase(Locale.ENGLISH)); return SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX.get(formatSuffix.toLowerCase(Locale.ENGLISH));
} }

@Override
public String getPath(){
return source.getRequestURI().replaceFirst(source.getContextPath(), "");
}
} }
Expand Up @@ -19,76 +19,78 @@
*/ */
package org.sonar.server.ws; package org.sonar.server.ws;


import java.io.ByteArrayOutputStream; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.sonarqube.ws.MediaTypes.JSON;
import static org.sonarqube.ws.MediaTypes.XML;

import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import javax.annotation.CheckForNull;
import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.Response;
import org.sonar.api.utils.text.JsonWriter; import org.sonar.api.utils.text.JsonWriter;
import org.sonar.api.utils.text.XmlWriter; import org.sonar.api.utils.text.XmlWriter;
import org.sonarqube.ws.MediaTypes;


public class ServletResponse implements Response { public class ServletResponse implements Response {


private Map<String, String> headers = new HashMap<>(); private final ServletStream stream;


public static class ServletStream implements Stream { public ServletResponse(HttpServletResponse response) {
private String mediaType; stream = new ServletStream(response);
private int httpStatus = 200; }
private final ByteArrayOutputStream output = new ByteArrayOutputStream();


@CheckForNull public static class ServletStream implements Stream {
public String mediaType() { private final HttpServletResponse response;
return mediaType;
}


public int httpStatus() { public ServletStream(HttpServletResponse response) {
return httpStatus; this.response = response;
this.response.setStatus(200);
// SONAR-6964 WS should not be cached by browser
this.response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
} }


@Override @Override
public ServletStream setMediaType(String s) { public ServletStream setMediaType(String s) {
this.mediaType = s; this.response.setContentType(s);
return this; return this;
} }


@Override @Override
public ServletStream setStatus(int httpStatus) { public ServletStream setStatus(int httpStatus) {
this.httpStatus = httpStatus; this.response.setStatus(httpStatus);
return this; return this;
} }


@Override @Override
public OutputStream output() { public OutputStream output() {
return output; try {
return response.getOutputStream();
} catch (IOException e) {
throw new IllegalStateException(e);
}
} }


public String outputAsString() { HttpServletResponse response() {
return new String(output.toByteArray(), StandardCharsets.UTF_8); return response;
} }


public ServletStream reset() { public ServletStream reset() {
output.reset(); response.reset();
return this; return this;
} }
} }


private final ServletStream stream = new ServletStream();

@Override @Override
public JsonWriter newJsonWriter() { public JsonWriter newJsonWriter() {
stream.setMediaType(MediaTypes.JSON); stream.setMediaType(JSON);
return JsonWriter.of(new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8)); return JsonWriter.of(new OutputStreamWriter(stream.output(), UTF_8));
} }


@Override @Override
public XmlWriter newXmlWriter() { public XmlWriter newXmlWriter() {
stream.setMediaType(MediaTypes.XML); stream.setMediaType(XML);
return XmlWriter.of(new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8)); return XmlWriter.of(new OutputStreamWriter(stream.output(), UTF_8));
} }


@Override @Override
Expand All @@ -104,17 +106,17 @@ public Response noContent() {


@Override @Override
public Response setHeader(String name, String value) { public Response setHeader(String name, String value) {
headers.put(name, value); stream.response().setHeader(name, value);
return this; return this;
} }


@Override @Override
public Collection<String> getHeaderNames() { public Collection<String> getHeaderNames() {
return headers.keySet(); return stream.response().getHeaderNames();
} }


@Override @Override
public String getHeader(String name) { public String getHeader(String name) {
return headers.get(name); return stream.response().getHeader(name);
} }
} }
Expand Up @@ -19,12 +19,21 @@
*/ */
package org.sonar.server.ws; package org.sonar.server.ws;


import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.apache.commons.lang.StringUtils.substring;
import static org.apache.commons.lang.StringUtils.substringAfterLast;
import static org.apache.commons.lang.StringUtils.substringBeforeLast;
import static org.sonar.server.ws.RequestVerifier.verifyRequest;
import static org.sonar.server.ws.ServletRequest.SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX;

import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.picocontainer.Startable; import org.picocontainer.Startable;
import org.sonar.api.i18n.I18n; import org.sonar.api.i18n.I18n;
import org.sonar.api.server.ServerSide; import org.sonar.api.server.ServerSide;
Expand All @@ -42,13 +51,6 @@
import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSession;
import org.sonarqube.ws.MediaTypes; import org.sonarqube.ws.MediaTypes;


import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.apache.commons.lang.StringUtils.substringAfterLast;
import static org.sonar.server.ws.RequestVerifier.verifyRequest;
import static org.sonar.server.ws.ServletRequest.SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX;

/** /**
* @since 4.2 * @since 4.2
*/ */
Expand Down Expand Up @@ -79,30 +81,26 @@ public void stop() {
// nothing // nothing
} }


/** List<WebService.Controller> controllers() {
* Used by Ruby on Rails to add ws routes. See WEB_INF/lib/java_ws_routing.rb
*/
public List<WebService.Controller> controllers() {
return context.controllers(); return context.controllers();
} }


@Override @Override
public LocalResponse call(LocalRequest request) { public LocalResponse call(LocalRequest request) {
String controller = StringUtils.substringBeforeLast(request.getPath(), "/");
String action = substringAfterLast(request.getPath(), "/");
DefaultLocalResponse localResponse = new DefaultLocalResponse(); DefaultLocalResponse localResponse = new DefaultLocalResponse();
execute(new LocalRequestAdapter(request), localResponse, controller, action, null); execute(new LocalRequestAdapter(request), localResponse);
return localResponse; return localResponse;
} }


public void execute(Request request, Response response, String controllerPath, String actionKey, @Nullable String actionExtension) { public void execute(Request request, Response response) {
try { try {
WebService.Action action = getAction(controllerPath, actionKey); ActionExtractor actionExtractor = new ActionExtractor(request.getPath());
WebService.Action action = getAction(actionExtractor.getController(), actionExtractor.getAction());
if (request instanceof ValidatingRequest) { if (request instanceof ValidatingRequest) {
((ValidatingRequest) request).setAction(action); ((ValidatingRequest) request).setAction(action);
((ValidatingRequest) request).setLocalConnector(this); ((ValidatingRequest) request).setLocalConnector(this);
} }
checkActionExtension(actionExtension); checkActionExtension(actionExtractor.getExtension());
verifyRequest(action, request); verifyRequest(action, request);
action.handler().handle(request, response); action.handler().handle(request, response);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Expand Down Expand Up @@ -156,4 +154,42 @@ private static void checkActionExtension(@Nullable String actionExtension) {
checkArgument(SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX.get(actionExtension.toLowerCase(Locale.ENGLISH)) != null, "Unknown action extension: %s", actionExtension); checkArgument(SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX.get(actionExtension.toLowerCase(Locale.ENGLISH)) != null, "Unknown action extension: %s", actionExtension);
} }


private static class ActionExtractor {
private static final String SLASH = "/";
private static final String POINT = ".";

private final String controller;
private final String action;
private final String extension;

ActionExtractor(String path) {
String pathWithoutExtension = substringBeforeLast(path, POINT);
this.controller = extractController(pathWithoutExtension);
this.action = substringAfterLast(pathWithoutExtension, SLASH);
checkArgument(!action.isEmpty(), "Url is incorrect : '%s'", path);
this.extension = substringAfterLast(path, POINT);
}

private static String extractController(String path) {
String controller = substringBeforeLast(path, SLASH);
if (controller.startsWith(SLASH)) {
return substring(controller, 1);
}
return controller;
}

String getController() {
return controller;
}

String getAction() {
return action;
}

@CheckForNull
String getExtension() {
return extension;
}
}

} }
Expand Up @@ -20,21 +20,14 @@


package org.sonar.server.ws; package org.sonar.server.ws;


import static java.lang.String.format;

import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.FilterConfig; import javax.servlet.FilterConfig;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.sonar.api.server.ws.RailsHandler; import org.sonar.api.server.ws.RailsHandler;
import org.sonar.api.web.ServletFilter; import org.sonar.api.web.ServletFilter;


Expand All @@ -47,71 +40,38 @@
public class WebServiceFilter extends ServletFilter { public class WebServiceFilter extends ServletFilter {


private final WebServiceEngine webServiceEngine; private final WebServiceEngine webServiceEngine;
private final Map<String, WsUrl> wsUrls = new HashMap<>();
private final List<String> includeUrls = new ArrayList<>(); private final List<String> includeUrls = new ArrayList<>();
private final List<String> excludeUrls = new ArrayList<>();


public WebServiceFilter(WebServiceEngine webServiceEngine) { public WebServiceFilter(WebServiceEngine webServiceEngine) {
this.webServiceEngine = webServiceEngine; this.webServiceEngine = webServiceEngine;
webServiceEngine.controllers().stream() webServiceEngine.controllers().stream()
.forEach(controller -> controller.actions().stream() .forEach(controller -> controller.actions().stream()
.filter(action -> !(action.handler() instanceof RailsHandler) && !(action.handler() instanceof ServletFilterHandler))
.forEach(action -> { .forEach(action -> {
String url = "/" + action.path(); // Rails and servlet filter WS should not be executed by the web service engine
wsUrls.put(url, new WsUrl(controller.path(), action.key())); if (!(action.handler() instanceof RailsHandler) && !(action.handler() instanceof ServletFilterHandler)) {
includeUrls.add(url + "*"); includeUrls.add("/" + controller.path() + "/*");
} else {
excludeUrls.add("/" + action.path() + "*");
}
})); }));
} }


@Override @Override
public UrlPattern doGetPattern() { public UrlPattern doGetPattern() {
return UrlPattern.builder() return UrlPattern.builder()
.includes(includeUrls) .includes(includeUrls)
.excludes(excludeUrls)
.build(); .build();
} }


@Override @Override
public void doFilter(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { public void doFilter(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse; 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); ServletRequest wsRequest = new ServletRequest(request);
ServletResponse wsResponse = new ServletResponse(); ServletResponse wsResponse = new ServletResponse(response);
webServiceEngine.execute(wsRequest, wsResponse, url.getController(), url.getAction(), pathWithExtension[1]); webServiceEngine.execute(wsRequest, wsResponse);
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 @Override
Expand Down

0 comments on commit 0f42fe4

Please sign in to comment.