/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ package org.apache.shindig.gadgets.servlet; import com.google.inject.Inject; import com.google.inject.name.Named; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.apache.shindig.common.servlet.HttpUtil; import org.apache.shindig.common.servlet.InjectedServlet; import org.apache.shindig.common.Pair; import org.apache.shindig.common.uri.Uri; import org.apache.shindig.common.uri.UriBuilder; import org.apache.shindig.gadgets.GadgetException; import org.apache.shindig.gadgets.http.HttpRequest; import org.apache.shindig.gadgets.http.HttpResponse; import org.apache.shindig.gadgets.http.RequestPipeline; import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry; import org.apache.shindig.gadgets.rewrite.RewritingException; import org.apache.shindig.gadgets.uri.ConcatUriManager; import org.apache.shindig.gadgets.uri.UriCommon.Param; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet which concatenates the content of several proxied HTTP responses */ public class ConcatProxyServlet extends InjectedServlet { private static final long serialVersionUID = -4390212150673709895L; public static final String JSON_PARAM = Param.JSON.getKey(); private static final Pattern JSON_PARAM_PATTERN = Pattern.compile("^\\w*$"); // TODO: parameterize these. static final Integer LONG_LIVED_REFRESH = (365 * 24 * 60 * 60); // 1 year static final Integer DEFAULT_REFRESH = (60 * 60); // 1 hour private static final Logger LOG = Logger.getLogger(ConcatProxyServlet.class.getName()); private transient RequestPipeline requestPipeline; private transient ConcatUriManager concatUriManager; private transient ResponseRewriterRegistry contentRewriterRegistry; // Sequential version of 'execute' by default. private transient ExecutorService executor = Executors.newSingleThreadExecutor(); @Inject public void setRequestPipeline(RequestPipeline requestPipeline) { checkInitialized(); this.requestPipeline = requestPipeline; } @Inject public void setConcatUriManager(ConcatUriManager concatUriManager) { checkInitialized(); this.concatUriManager = concatUriManager; } @Inject public void setContentRewriterRegistry(ResponseRewriterRegistry contentRewriterRegistry) { checkInitialized(); this.contentRewriterRegistry = contentRewriterRegistry; } @Inject public void setExecutor(@Named("shindig.concat.executor") ExecutorService executor) { checkInitialized(); // Executor is independently named to allow separate configuration of // concat fetch parallelism and other Shindig job execution. this.executor = executor; } @SuppressWarnings("boxing") @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { if (request.getHeader("If-Modified-Since") != null) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } Uri uri = new UriBuilder(request).toUri(); ConcatUriManager.ConcatUri concatUri = concatUriManager.process(uri); ConcatUriManager.Type concatType = concatUri.getType(); try { if (concatType == null) { throw new GadgetException(GadgetException.Code.MISSING_PARAMETER, "Missing type", HttpResponse.SC_BAD_REQUEST); } HttpUtil.setCachingHeaders(response, concatUri.translateStatusRefresh(LONG_LIVED_REFRESH, DEFAULT_REFRESH), false); } catch (GadgetException gex) { response.sendError(HttpResponse.SC_BAD_REQUEST, formatError(gex, uri)); return; } // Throughout this class, wherever output is generated it's done as a UTF8 String. // As such, we affirmatively state that UTF8 is being returned here. response.setHeader("Content-Type", concatType.getMimeType() + "; charset=UTF8"); response.setHeader("Content-Disposition", "attachment;filename=p.txt"); if (doFetchConcatResources(response, concatUri)) { response.setStatus(HttpResponse.SC_OK); } else { response.setStatus(HttpResponse.SC_BAD_REQUEST); } } /** * @param response HttpservletResponse. * @param concatUri URI representing the concatenated list of resources requested. * @return false for cases where concat resources could not be fetched, true for success cases. * @throws IOException */ private boolean doFetchConcatResources(HttpServletResponse response, ConcatUriManager.ConcatUri concatUri) throws IOException { // Check for json concat and set output stream. ConcatOutputStream cos = null; String jsonVar = concatUri.getSplitParam(); if (jsonVar != null) { // JSON-concat mode. if (JSON_PARAM_PATTERN.matcher(jsonVar).matches()) { cos = new JsonConcatOutputStream(response.getOutputStream(), jsonVar); } else { response.getOutputStream().println( formatHttpError(HttpServletResponse.SC_BAD_REQUEST, "Bad json variable name " + jsonVar, null)); return false; } } else { // Standard concat output mode. cos = new VerbatimConcatOutputStream(response.getOutputStream()); } List<Pair<Uri, FutureTask<RequestContext>>> futureTasks = new ArrayList<Pair<Uri, FutureTask<RequestContext>>>(); try { for (Uri resourceUri : concatUri.getBatch()) { try { HttpRequest httpReq = concatUri.makeHttpRequest(resourceUri); FutureTask<RequestContext> httpFetcher = new FutureTask<RequestContext>(new HttpFetchCallable(httpReq)); futureTasks.add(Pair.of(httpReq.getUri(), httpFetcher)); executor.execute(httpFetcher); } catch (GadgetException ge) { if (cos.outputError(resourceUri, ge)) { // True returned from outputError indicates a terminal error. return false; } } } for (Pair<Uri, FutureTask<RequestContext>> futureTask : futureTasks) { RequestContext requestCxt = null; try { try { requestCxt = futureTask.two.get(); } catch (InterruptedException ie) { throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, ie); } catch (ExecutionException ee) { throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, ee); } if (requestCxt.getGadgetException() != null) { throw requestCxt.getGadgetException(); } HttpResponse httpResp = requestCxt.getHttpResp(); if (httpResp != null) { if (contentRewriterRegistry != null) { try { httpResp = contentRewriterRegistry.rewriteHttpResponse(requestCxt.getHttpReq(), httpResp); } catch (RewritingException e) { throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e, e.getHttpStatusCode()); } } cos.output(futureTask.one, httpResp); } else { return false; } } catch (GadgetException ge) { if (cos.outputError(futureTask.one, ge)) { return false; } } } } finally { if (cos != null) { try { cos.close(); } catch (IOException ioe) { // Ignore } } } return true; } private static String formatHttpError(int status, String errorMessage, Uri uri) { StringBuilder err = new StringBuilder(); err.append("/* ---- Error "); err.append(status); if (!StringUtils.isEmpty(errorMessage)) { err.append(", "); err.append(errorMessage); } if (uri != null) { err.append(" (").append(uri.toString()).append(')'); } err.append(" ---- */"); return err.toString(); } private static String formatError(GadgetException excep, Uri uri) throws IOException { StringBuilder err = new StringBuilder(); err.append(excep.getCode().toString()); err.append(" concat("); err.append(uri.toString()); err.append(") "); err.append(excep.getMessage()); // Log the errors here for now. We might want different severity levels // for different error codes. LOG.log(Level.INFO, "Concat proxy request failed", err); return err.toString(); } private static abstract class ConcatOutputStream extends ServletOutputStream { private final ServletOutputStream wrapped; protected ConcatOutputStream(ServletOutputStream wrapped) { this.wrapped = wrapped; } protected abstract void outputJs(Uri uri, String data) throws IOException; public void output(Uri uri, HttpResponse resp) throws IOException { if (resp.getHttpStatusCode() != HttpServletResponse.SC_OK) { println(formatHttpError(resp.getHttpStatusCode(), resp.getResponseAsString(), uri)); } else { outputJs(uri, resp.getResponseAsString()); } } public boolean outputError(Uri uri, GadgetException e) throws IOException { print(formatError(e, uri)); return e.getHttpStatusCode() == HttpResponse.SC_INTERNAL_SERVER_ERROR; } @Override public void write(int b) throws IOException { wrapped.write(b); } @Override public void write(byte b[], int off, int len) throws IOException { wrapped.write(b, off, len); } @Override public void write(byte b[]) throws IOException { wrapped.write(b); } @Override public void close() throws IOException { wrapped.close(); } @Override public void print(String data) throws IOException { write(data.getBytes("UTF8")); } @Override public void println(String data) throws IOException { print(data); write("\r\n".getBytes("UTF8")); } } private static class VerbatimConcatOutputStream extends ConcatOutputStream { public VerbatimConcatOutputStream(ServletOutputStream wrapped) { super(wrapped); } @Override protected void outputJs(Uri uri, String data) throws IOException { println("/* ---- Start " + uri.toString() + " ---- */"); print(data); println("/* ---- End " + uri.toString() + " ---- */"); } } private static class JsonConcatOutputStream extends ConcatOutputStream { public JsonConcatOutputStream(ServletOutputStream wrapped, String tok) throws IOException { super(wrapped); this.println(tok + "={"); } @Override protected void outputJs(Uri uri, String data) throws IOException { print("\""); print(uri.toString()); print("\":\""); print(StringEscapeUtils.escapeJavaScript(data)); println("\","); } @Override public void close() throws IOException { println("};"); super.close(); } } // Encapsulates the response context of a single resource fetch. private static class RequestContext { private HttpRequest httpReq; private HttpResponse httpResp; private GadgetException gadgetException; public HttpRequest getHttpReq() { return httpReq; } public HttpResponse getHttpResp() { return httpResp; } public GadgetException getGadgetException() { return gadgetException; } public RequestContext(HttpRequest httpReq, HttpResponse httpResp, GadgetException ge) { this.httpReq = httpReq; this.httpResp = httpResp; this.gadgetException = ge; } } // Worker class responsible for fetching a single resource. public class HttpFetchCallable implements Callable<RequestContext> { private HttpRequest httpReq; public HttpFetchCallable(HttpRequest httpReq) { this.httpReq = httpReq; } public RequestContext call() { HttpResponse httpResp = null; GadgetException gEx = null; try { httpResp = requestPipeline.execute(httpReq); } catch (GadgetException ge){ gEx = ge; } return new RequestContext(httpReq, httpResp, gEx); } } }