EMMA Coverage Report (generated Mon Nov 01 16:48:29 PDT 2010)
[all classes][com.google.caja.ancillary.servlet]

COVERAGE SUMMARY FOR SOURCE FILE [CajaWebToolsServlet.java]

nameclass, %method, %block, %line, %
CajaWebToolsServlet.java100% (1/1)50%  (6/12)55%  (441/807)49%  (72/147)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class CajaWebToolsServlet100% (1/1)50%  (6/12)55%  (441/807)49%  (72/147)
CajaWebToolsServlet (String): void 100% (1/1)100% (30/30)100% (4/4)
containsControlChar (String): boolean 0%   (0/1)0%   (0/24)0%   (0/3)
doGet (HttpServletRequest, HttpServletResponse): void 0%   (0/1)0%   (0/71)0%   (0/17)
doPost (HttpServletRequest, HttpServletResponse): void 0%   (0/1)0%   (0/49)0%   (0/12)
errorPage (String, MessageQueue, Request): Result 100% (1/1)100% (7/7)100% (1/1)
errorPage (int, String, MessageQueue, Request): Result 100% (1/1)97%  (74/76)100% (6/6)
handle (String, List): Result 100% (1/1)88%  (259/295)85%  (46/54)
maxMessageLevel (MessageQueue): MessageLevel 100% (1/1)100% (27/27)100% (6/6)
parseQueryString (String): List 0%   (0/1)0%   (0/65)0%   (0/10)
process (String, String, HttpServletResponse): void 0%   (0/1)0%   (0/99)0%   (0/20)
rfc822QuotedString (String): String 100% (1/1)81%  (44/54)82%  (9/11)
uriDecode (String): String 0%   (0/1)0%   (0/10)0%   (0/3)

1// Copyright (C) 2009 Google Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14 
15package com.google.caja.ancillary.servlet;
16 
17import com.google.caja.lexer.CharProducer;
18import com.google.caja.lexer.InputSource;
19import com.google.caja.lexer.ParseException;
20import com.google.caja.parser.html.DomParser;
21import com.google.caja.parser.html.HtmlQuasiBuilder;
22import com.google.caja.parser.html.Nodes;
23import com.google.caja.reporting.Message;
24import com.google.caja.reporting.MessageLevel;
25import com.google.caja.reporting.MessageQueue;
26import com.google.caja.reporting.SimpleMessageQueue;
27import com.google.caja.util.ContentType;
28import com.google.caja.util.Lists;
29import com.google.caja.util.Pair;
30 
31import java.io.IOException;
32import java.io.OutputStream;
33import java.io.Reader;
34import java.io.UnsupportedEncodingException;
35import java.io.Writer;
36import java.net.URI;
37import java.net.URISyntaxException;
38import java.net.URLDecoder;
39import java.util.Collections;
40import java.util.EnumSet;
41import java.util.List;
42import java.util.regex.Matcher;
43import java.util.regex.Pattern;
44 
45import javax.servlet.http.HttpServlet;
46import javax.servlet.http.HttpServletRequest;
47import javax.servlet.http.HttpServletResponse;
48 
49import org.w3c.dom.Document;
50import org.w3c.dom.DocumentFragment;
51import org.w3c.dom.Node;
52 
53/**
54 * Allows web developers to lint, minify, and generate documentation for their
55 * code via a web interface.
56 *
57 * @author mikesamuel@gmail.com
58 */
59public class CajaWebToolsServlet extends HttpServlet {
60  private static final long serialVersionUID = -5232422153254165200L;
61  final StaticFiles staticFiles;
62  private final Pattern staticFilePath;
63 
64  /**
65   * @param cacheId an alphanumeric string that can be added to a directory
66   *     name in a URL to version all resources in that directory.
67   */
68  public CajaWebToolsServlet(String cacheId) {
69    this.staticFiles = new StaticFiles(cacheId);
70    // Matches "favicon.ico" and paths under <tt>/files-.../</tt> that do not
71    // contain any pathname element that starts with a ., so no parent directory
72    // names, and no UNIX hidden files.
73    this.staticFilePath = Pattern.compile(
74        "^/(?:(favicon\\.ico)|"
75        + Pattern.quote("files-" + cacheId) // A directory containing cache Id
76        + "/((?:[^/.]+/)*[^/.]+(?:\\.[^/.]+)))$");
77  }
78 
79  @Override
80  public void doGet(HttpServletRequest req, HttpServletResponse resp)
81      throws IOException {
82    String reqPath = req.getPathInfo();
83    // Redirect to /index preserving any query string.
84    if (null == reqPath || "/".equals(reqPath)) {
85      try {
86        String query = req.getQueryString();
87        URI indexUri = new URI(
88            null, null, Verb.INDEX.relRequestPath, query, null);
89        resp.sendRedirect(indexUri.toString());
90      } catch (URISyntaxException ex) {
91        ex.printStackTrace();
92        // Let process report an error
93      }
94      return;
95    }
96    Matcher m = staticFilePath.matcher(reqPath);
97    if (m.matches()) {
98      // Allow GETs of static files.
99      String path = m.group(2);
100      if (path == null) { path = m.group(1); }
101      staticFiles.serve("files/" + path, req, resp);
102    } else {
103      // Process a dynamic operation.
104      process(reqPath, req.getQueryString(), resp);
105    }
106  }
107 
108  @Override
109  public void doPost(HttpServletRequest req, HttpServletResponse resp)
110      throws IOException {
111    String reqPath = req.getPathInfo();
112    // Special case uploads since they require very different processing.
113    if ("/upload".equals(reqPath)) {
114      UploadPage.doUpload(req, resp);
115      return;
116    }
117    StringBuilder query = new StringBuilder();
118    Reader in = req.getReader();
119    try {
120      char[] buf = new char[1024];
121      for (int n; (n = in.read(buf)) > 0;) { query.append(buf, 0, n); }
122    } finally {
123      in.close();
124    }
125    process(reqPath, query.toString(), resp);
126  }
127 
128  /**
129   * Processes a dynamic request which cannot be satisfied by
130   * {@link StaticFiles} or the special upload handler.
131   */
132  private void process(String reqPath, String query, HttpServletResponse out)
133      throws IOException {
134    Result result = handle(reqPath, parseQueryString(query));
135    // Serve the result
136    if (result.status != 0) { out.setStatus(result.status); }
137    String contentType = result.getContentType();
138    if (contentType != null) { out.setContentType(contentType); }
139    for (Pair<String, String> header : result.headers) {
140      if (containsControlChar(header.b)) {
141        throw new IOException("Split header <<" + header + ">>");
142      }
143      out.setHeader(header.a, header.b);
144    }
145    if (result.content != null) {
146      if (result.content.isText()) {
147        Writer w = out.getWriter();
148        try {
149          result.content.toWriter(w);
150        } finally {
151          w.close();
152        }
153      } else {
154        OutputStream os = out.getOutputStream();
155        try {
156          result.content.toOutputStream(os);
157        } finally {
158          os.close();
159        }
160      }
161    }
162  }
163 
164  /** Expose query parameters in an order-preserving way. */
165  private static List<Pair<String, String>> parseQueryString(String query) {
166    List<Pair<String, String>> out = Lists.newArrayList();
167    if (query != null) {
168      if (query.startsWith("?")) { query = query.substring(1); }
169      if (!"".equals(query)) {
170        for (String kv : query.split("&")) {
171          int eq = kv.indexOf('=');
172          if (eq >= 0) {
173            out.add(Pair.pair(uriDecode(kv.substring(0, eq)),
174                              uriDecode(kv.substring(eq + 1))));
175          } else {
176            out.add(Pair.pair(uriDecode(kv), ""));
177          }
178        }
179      }
180    }
181    return out;
182  }
183 
184  private static String uriDecode(String s) {
185    try {
186      return URLDecoder.decode(s, "UTF-8");
187    } catch (UnsupportedEncodingException ex) {
188      throw new RuntimeException(ex);
189    }
190  }
191 
192  /**
193   * Handles a request.
194   * @param reqPath the URI path requested.
195   * @param params query parameters in the order they appear.
196   * @return the response to send back.
197   */
198  Result handle(String reqPath, List<Pair<String, String>> params) {
199    MessageQueue mq = new SimpleMessageQueue();
200    Request req = null;
201    if (reqPath.startsWith("/")) {
202      // The verb is specified in the path, but in the index page, there is
203      // a select box for the verb, so for /index, the param processing below
204      // might set the verb in request.
205      Verb verb = Verb.fromRelReqPath(reqPath.substring(1));
206      if (verb != null) {
207        req = Request.create(verb, staticFiles);
208      }
209    }
210    if (req == null) {
211      return errorPage(
212          404, "File not found " + reqPath + ".  Expected a path in "
213          + EnumSet.allOf(Verb.class),
214          mq, new Request());
215    }
216 
217    List<Job> inputJobs = Lists.newArrayList();
218    Processor p = new Processor(req, mq);
219    try {
220      Verb v = req.verb;
221      // Process all the parameters
222      for (Pair<String, String> cgiParam : params) {
223        String name = cgiParam.a;
224        String value = cgiParam.b;
225        if ("".equals(value)) { continue; }
226        Request.handler(v, name).handle(name, value, req);
227      }
228      // Parse all the inputs.
229      for (Input input : req.inputs) {
230        if ("".equals(input.code.trim())) { continue; }
231        InputSource is = new InputSource(req.baseUri.resolve(input.path));
232        CharProducer cp = CharProducer.Factory.fromString(input.code, is);
233        req.srcMap.put(is, cp.clone());
234        req.mc.addInputSource(is);
235        URI baseUri = req.baseUri != null ? req.baseUri : is.getUri();
236        try {
237          inputJobs.add(p.parse(cp, input.t, null, baseUri));
238        } catch (ParseException ex) {
239          ex.toMessageQueue(mq);
240        }
241      }
242    } catch (BadInputException ex) {
243      return errorPage(ex.getMessage(), mq, req);
244    }
245 
246    // Take the inputs and generate output jobs.
247    List<Job> jobs;
248    if (req.verb == Verb.INDEX) {
249      jobs = Collections.singletonList(
250          Job.html(IndexPage.render(req), null));
251    } else if (req.verb == Verb.HELP) {
252      jobs = Collections.singletonList(
253          Job.html(HelpPage.render(staticFiles), null));
254    } else {
255      try {
256        jobs = p.process(inputJobs);
257      } catch (IOException ex) {
258        ex.printStackTrace();
259        return errorPage(ex.getMessage(), mq, req);
260      }
261      if (jobs.isEmpty() && !inputJobs.isEmpty()) {
262        return errorPage(null, mq, req);
263      }
264    }
265 
266    // Reduce the output jobs down to one output job.
267    // This may involve concatenating javascript or css files, or combining
268    // heterogenous file types into a single HTML file.
269    Content content = p.reduce(jobs);
270 
271    // Report errors if we have unresolved errors.
272    // For the lint page, which incorporates errors into the regular output,
273    // the message queue has already been drained.
274    if (MessageLevel.ERROR.compareTo(maxMessageLevel(mq)) < 0) {
275      return errorPage(null, mq, req);
276    }
277 
278    Result result = new Result(HttpServletResponse.SC_OK, content, mq);
279    if (req.verb == Verb.ECHO) {
280      // Force a download so that /echo can't be used as an open redirector.
281      String downloadPath = null;
282      if (req.inputs.size() == 1) { downloadPath = req.inputs.get(0).path; }
283      if (downloadPath == null || "".equals(downloadPath)
284          || downloadPath.startsWith("unnamed-")
285          || containsControlChar(downloadPath)) {
286        downloadPath = "caja_tools_output." + content.type.ext;
287      }
288      result.headers.add(Pair.pair(
289          "Content-disposition",
290          "attachment; filename=" + rfc822QuotedString(downloadPath)));
291    }
292    return result;
293  }
294 
295  private static MessageLevel maxMessageLevel(MessageQueue mq) {
296    MessageLevel max = MessageLevel.values()[0];
297    for (Message msg : mq.getMessages()) {
298      MessageLevel lvl = msg.getMessageLevel();
299      if (max.compareTo(lvl) < 0) { max = lvl; }
300    }
301    return max;
302  }
303 
304  private Result errorPage(String title, MessageQueue mq, Request req) {
305    return errorPage(HttpServletResponse.SC_BAD_REQUEST, title, mq, req);
306  }
307 
308  private Result errorPage(
309      int status, String title, MessageQueue mq, Request req) {
310    Document doc = DomParser.makeDocument(null, null);
311    HtmlQuasiBuilder b = HtmlQuasiBuilder.getBuilder(doc);
312    DocumentFragment messages = Reporter.messagesToFragment(mq, req, b);
313    Node errorDoc = b.substV(
314        ""
315        + "<html>"
316        + "<head>"
317        + "<meta http-equiv=Content-Type content=text/html;charset=UTF-8 />"
318        + "<title>@title</title>"
319        + "</head>"
320        + "<body>@header@messages</body>"
321        + "</html>",
322        "title", title != null ? title : "Errors in input",
323        "header", title != null ? b.substV("<h1>@t</h1>", "t", title) : "",
324        "messages", messages);
325    Content errorHtml = new Content(Nodes.render(errorDoc), ContentType.HTML);
326    return new Result(status, errorHtml, mq);
327  }
328 
329  private static boolean containsControlChar(String s) {
330    for (int i = 0, n = s.length(); i < n; ++i) {
331      if (s.charAt(i) < 0x20 && s.charAt(i) != '\t') { return true; }
332    }
333    return false;
334  }
335 
336  // as referenced from rfc 2045 via rfc 2183
337  private static String rfc822QuotedString(String s) {
338    int n = s.length();
339    StringBuilder sb = new StringBuilder(n + 16);
340    sb.append('"');
341    int pos = 0;
342    for (int i = 0; i < n; ++i) {
343      char ch = s.charAt(i);
344      if (ch == '"' || ch == '\\') {
345        sb.append(s, pos, i).append('\\');
346        pos = i;
347      }
348    }
349    sb.append(s, pos, n).append('"');
350    return sb.toString();
351  }
352}

[all classes][com.google.caja.ancillary.servlet]
EMMA 2.0.5312 (C) Vladimir Roubtsov