blob: 5c9fd360147dad3062c205ce379b6f1204cd3190 [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
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.gitiles;
16
Dave Borowitzc410f962014-09-23 10:49:26 -070017import static com.google.common.base.MoreObjects.firstNonNull;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080018import static com.google.gitiles.FormatType.DEFAULT;
19import static com.google.gitiles.FormatType.HTML;
20import static com.google.gitiles.FormatType.JSON;
21import static com.google.gitiles.FormatType.TEXT;
David Pletcherd7bdaf32014-08-27 14:50:32 -070022import static java.nio.charset.StandardCharsets.UTF_8;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080023import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
Dave Borowitz9de65952012-08-13 16:09:45 -070024import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080025import static javax.servlet.http.HttpServletResponse.SC_OK;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080026import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
Dave Borowitz9de65952012-08-13 16:09:45 -070027
Dave Borowitz4f568702014-05-01 19:54:57 -070028import com.google.common.base.Strings;
Dave Borowitz54271462013-11-11 11:43:11 -080029import com.google.common.collect.ImmutableMap;
30import com.google.common.collect.Maps;
31import com.google.common.net.HttpHeaders;
32import com.google.gson.FieldNamingPolicy;
33import com.google.gson.GsonBuilder;
34
Dave Borowitz54271462013-11-11 11:43:11 -080035import org.joda.time.Instant;
36
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080037import java.io.ByteArrayOutputStream;
Dave Borowitz9de65952012-08-13 16:09:45 -070038import java.io.IOException;
Dave Borowitzfc2f00a2014-07-29 17:34:43 -070039import java.io.OutputStream;
Dave Borowitz673d1982014-05-02 12:30:49 -070040import java.io.OutputStreamWriter;
41import java.io.Writer;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080042import java.lang.reflect.Type;
Dave Borowitz9de65952012-08-13 16:09:45 -070043import java.util.Map;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080044import java.util.zip.GZIPOutputStream;
Dave Borowitz9de65952012-08-13 16:09:45 -070045
Dave Borowitzb1c628f2013-01-11 11:28:20 -080046import javax.servlet.ServletException;
Dave Borowitz9de65952012-08-13 16:09:45 -070047import javax.servlet.http.HttpServlet;
48import javax.servlet.http.HttpServletRequest;
49import javax.servlet.http.HttpServletResponse;
50
51/** Base servlet class for Gitiles servlets that serve Soy templates. */
52public abstract class BaseServlet extends HttpServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080053 private static final long serialVersionUID = 1L;
Dave Borowitz8d6d6872014-03-16 15:18:14 -070054 private static final String ACCESS_ATTRIBUTE = BaseServlet.class.getName() + "/GitilesAccess";
Dave Borowitzc99d0bb2014-07-31 15:39:39 -070055 private static final String DATA_ATTRIBUTE = BaseServlet.class.getName() + "/Data";
56 private static final String STREAMING_ATTRIBUTE = BaseServlet.class.getName() + "/Streaming";
Dave Borowitz9de65952012-08-13 16:09:45 -070057
58 static void setNotCacheable(HttpServletResponse res) {
59 res.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate");
60 res.setHeader(HttpHeaders.PRAGMA, "no-cache");
61 res.setHeader(HttpHeaders.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT");
62 res.setDateHeader(HttpHeaders.DATE, new Instant().getMillis());
63 }
64
65 public static BaseServlet notFoundServlet() {
Dave Borowitz8d6d6872014-03-16 15:18:14 -070066 return new BaseServlet(null, null) {
Chad Horohoead23f142012-11-12 09:45:39 -080067 private static final long serialVersionUID = 1L;
Dave Borowitz9de65952012-08-13 16:09:45 -070068 @Override
69 public void service(HttpServletRequest req, HttpServletResponse res) {
70 res.setStatus(SC_NOT_FOUND);
71 }
72 };
73 }
74
75 public static Map<String, String> menuEntry(String text, String url) {
76 if (url != null) {
77 return ImmutableMap.of("text", text, "url", url);
78 } else {
79 return ImmutableMap.of("text", text);
80 }
81 }
82
Dave Borowitzc99d0bb2014-07-31 15:39:39 -070083 public static boolean isStreamingResponse(HttpServletRequest req) {
Dave Borowitzc410f962014-09-23 10:49:26 -070084 return firstNonNull((Boolean) req.getAttribute(STREAMING_ATTRIBUTE), false);
Dave Borowitzc99d0bb2014-07-31 15:39:39 -070085 }
86
Dave Borowitzded109a2014-03-03 15:25:39 -050087 protected static ArchiveFormat getArchiveFormat(GitilesAccess access) throws IOException {
88 return ArchiveFormat.getDefault(access.getConfig());
89 }
90
Dave Borowitz113c9352013-03-27 18:13:41 -040091 /**
92 * Put a value into a request's Soy data map.
93 *
94 * @param req in-progress request.
95 * @param key key.
96 * @param value Soy data value.
97 */
98 public static void putSoyData(HttpServletRequest req, String key, Object value) {
99 getData(req).put(key, value);
100 }
101
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800102 @Override
103 protected void doGet(HttpServletRequest req, HttpServletResponse res)
104 throws IOException, ServletException {
105 FormatType format;
106 try {
107 format = FormatType.getFormatType(req);
108 } catch (IllegalArgumentException err) {
109 res.sendError(SC_BAD_REQUEST);
110 return;
111 }
112 if (format == DEFAULT) {
113 format = getDefaultFormat(req);
114 }
115 switch (format) {
116 case HTML:
117 doGetHtml(req, res);
118 break;
119 case TEXT:
120 doGetText(req, res);
121 break;
122 case JSON:
123 doGetJson(req, res);
124 break;
125 default:
126 res.sendError(SC_BAD_REQUEST);
127 break;
128 }
129 }
130
131 /**
132 * @param req in-progress request.
133 * @return the default {@link FormatType} used when {@code ?format=} is not
134 * specified.
135 */
136 protected FormatType getDefaultFormat(HttpServletRequest req) {
137 return HTML;
138 }
139
140 /**
141 * Handle a GET request when the requested format type was HTML.
142 *
143 * @param req in-progress request.
144 * @param res in-progress response.
145 */
146 protected void doGetHtml(HttpServletRequest req, HttpServletResponse res)
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700147 throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800148 res.sendError(SC_BAD_REQUEST);
149 }
150
151 /**
152 * Handle a GET request when the requested format type was plain text.
153 *
154 * @param req in-progress request.
155 * @param res in-progress response.
156 */
157 protected void doGetText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700158 throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800159 res.sendError(SC_BAD_REQUEST);
160 }
161
162 /**
163 * Handle a GET request when the requested format type was JSON.
164 *
165 * @param req in-progress request.
166 * @param res in-progress response.
167 */
168 protected void doGetJson(HttpServletRequest req, HttpServletResponse res)
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700169 throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800170 res.sendError(SC_BAD_REQUEST);
171 }
172
Dave Borowitz9de65952012-08-13 16:09:45 -0700173 protected static Map<String, Object> getData(HttpServletRequest req) {
174 @SuppressWarnings("unchecked")
175 Map<String, Object> data = (Map<String, Object>) req.getAttribute(DATA_ATTRIBUTE);
176 if (data == null) {
177 data = Maps.newHashMap();
178 req.setAttribute(DATA_ATTRIBUTE, data);
179 }
180 return data;
181 }
182
183 protected final Renderer renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700184 private final GitilesAccess.Factory accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700185
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700186 protected BaseServlet(Renderer renderer, GitilesAccess.Factory accessFactory) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700187 this.renderer = renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700188 this.accessFactory = accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700189 }
190
191 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800192 * Render data to HTML using Soy.
193 *
194 * @param req in-progress request.
195 * @param res in-progress response.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800196 * @param templateName Soy template name; must be in one of the template files
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700197 * defined in {@link Renderer}.
198 * @param soyData data for Soy.
199 * @throws IOException an error occurred during rendering.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800200 */
201 protected void renderHtml(HttpServletRequest req, HttpServletResponse res, String templateName,
Dave Borowitz9de65952012-08-13 16:09:45 -0700202 Map<String, ?> soyData) throws IOException {
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800203 renderer.render(req, res, templateName,
204 startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700205 }
206
207 /**
208 * Start a streaming HTML response with header and footer rendered by Soy.
209 * <p>
210 * A streaming template includes the special template
211 * {@code gitiles.streamingPlaceholder} at the point where data is to be
212 * streamed. The template before and after this placeholder is rendered using
213 * the provided data map.
214 *
215 * @param req in-progress request.
216 * @param res in-progress response.
217 * @param templateName Soy template name; must be in one of the template files
218 * defined in {@link Renderer}.
219 * @param soyData data for Soy.
220 * @return output stream to render to. The portion of the template before the
221 * placeholder is already written and flushed; the portion after is
222 * written only on calling {@code close()}.
223 * @throws IOException an error occurred during rendering the header.
224 */
225 protected OutputStream startRenderStreamingHtml(HttpServletRequest req,
226 HttpServletResponse res, String templateName, Map<String, ?> soyData) throws IOException {
Dave Borowitzc99d0bb2014-07-31 15:39:39 -0700227 req.setAttribute(STREAMING_ATTRIBUTE, true);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700228 return renderer.renderStreaming(res, templateName, startHtmlResponse(req, res, soyData));
229 }
230
231 private Map<String, ?> startHtmlResponse(HttpServletRequest req, HttpServletResponse res,
232 Map<String, ?> soyData) throws IOException {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700233 res.setContentType(FormatType.HTML.getMimeType());
David Pletcherd7bdaf32014-08-27 14:50:32 -0700234 res.setCharacterEncoding(UTF_8.name());
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700235 setCacheHeaders(res);
Dave Borowitz9de65952012-08-13 16:09:45 -0700236
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700237 Map<String, Object> allData = getData(req);
Dave Borowitz76bbefd2014-03-11 16:57:45 -0700238
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700239 GitilesConfig.putVariant(
240 getAccess(req).getConfig(), "customHeader", "headerVariant", allData);
241 allData.putAll(soyData);
242 GitilesView view = ViewFilter.getView(req);
243 if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) {
244 allData.put("repositoryName", view.getRepositoryName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700245 }
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700246 if (!allData.containsKey("breadcrumbs")) {
247 allData.put("breadcrumbs", view.getBreadcrumbs());
248 }
249
250 res.setStatus(HttpServletResponse.SC_OK);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700251 return allData;
Dave Borowitz9de65952012-08-13 16:09:45 -0700252 }
253
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800254 /**
255 * Render data to JSON using GSON.
256 *
257 * @param req in-progress request.
258 * @param res in-progress response.
259 * @param src @see Gson#toJson(Object, Type, Appendable)
260 * @param typeOfSrc @see Gson#toJson(Object, Type, Appendable)
261 */
262 protected void renderJson(HttpServletRequest req, HttpServletResponse res, Object src,
263 Type typeOfSrc) throws IOException {
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700264 setApiHeaders(res, JSON);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800265 res.setStatus(SC_OK);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800266 try (Writer writer = newWriter(req, res)) {
267 newGsonBuilder(req).create().toJson(src, typeOfSrc, writer);
268 writer.write('\n');
269 }
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800270 }
271
Dave Borowitz438c5282014-07-09 20:15:34 -0700272 @SuppressWarnings("unused") // Used in subclasses.
273 protected GsonBuilder newGsonBuilder(HttpServletRequest req) throws IOException {
274 return new GsonBuilder()
275 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
276 .setPrettyPrinting()
277 .generateNonExecutableJson();
278 }
279
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800280 /**
Dave Borowitz4f568702014-05-01 19:54:57 -0700281 * @see #startRenderText(HttpServletRequest, HttpServletResponse)
282 * @param req in-progress request.
283 * @param res in-progress response.
284 * @param contentType contentType to set.
285 * @return the response's writer.
286 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700287 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res,
Dave Borowitz4f568702014-05-01 19:54:57 -0700288 String contentType) throws IOException {
289 setApiHeaders(res, contentType);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800290 return newWriter(req, res);
Dave Borowitz4f568702014-05-01 19:54:57 -0700291 }
292
293 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800294 * Prepare the response to render plain text.
295 * <p>
296 * Unlike
297 * {@link #renderHtml(HttpServletRequest, HttpServletResponse, String, Map)}
298 * and
299 * {@link #renderJson(HttpServletRequest, HttpServletResponse, Object, Type)},
300 * which assume the data to render is already completely prepared, this method
301 * does not write any data, only headers, and returns the response's
302 * ready-to-use writer.
303 *
304 * @param req in-progress request.
305 * @param res in-progress response.
306 * @return the response's writer.
307 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700308 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800309 throws IOException {
Dave Borowitz673d1982014-05-02 12:30:49 -0700310 return startRenderText(req, res, TEXT.getMimeType());
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800311 }
312
Dave Borowitzba9c1182013-03-13 14:16:43 -0700313 /**
314 * Render an error as plain text.
315 *
316 * @param req in-progress request.
317 * @param res in-progress response.
318 * @param statusCode HTTP status code.
319 * @param message full message text.
320 *
321 * @throws IOException
322 */
323 protected void renderTextError(HttpServletRequest req, HttpServletResponse res, int statusCode,
324 String message) throws IOException {
325 res.setStatus(statusCode);
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700326 setApiHeaders(res, TEXT);
327 setCacheHeaders(res);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800328 try (Writer out = newWriter(req, res)) {
329 out.write(message);
330 }
Dave Borowitzba9c1182013-03-13 14:16:43 -0700331 }
332
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700333 protected GitilesAccess getAccess(HttpServletRequest req) {
334 GitilesAccess access = (GitilesAccess) req.getAttribute(ACCESS_ATTRIBUTE);
335 if (access == null) {
336 access = accessFactory.forRequest(req);
337 req.setAttribute(ACCESS_ATTRIBUTE, access);
338 }
339 return access;
340 }
341
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700342 protected void setCacheHeaders(HttpServletResponse res) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700343 setNotCacheable(res);
344 }
Dave Borowitz0c944762013-04-04 11:01:42 -0700345
Dave Borowitz4f568702014-05-01 19:54:57 -0700346 protected void setApiHeaders(HttpServletResponse res, String contentType) {
347 if (!Strings.isNullOrEmpty(contentType)) {
348 res.setContentType(contentType);
349 }
David Pletcherd7bdaf32014-08-27 14:50:32 -0700350 res.setCharacterEncoding(UTF_8.name());
Dave Borowitz0c944762013-04-04 11:01:42 -0700351 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment");
Dave Borowitz06edb502013-04-04 11:02:36 -0700352 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700353 setCacheHeaders(res);
Dave Borowitz0c944762013-04-04 11:01:42 -0700354 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700355
Dave Borowitz4f568702014-05-01 19:54:57 -0700356 protected void setApiHeaders(HttpServletResponse res, FormatType type) {
357 setApiHeaders(res, type.getMimeType());
358 }
359
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700360 protected void setDownloadHeaders(HttpServletResponse res, String filename, String contentType) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700361 res.setContentType(contentType);
362 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700363 setCacheHeaders(res);
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700364 }
Dave Borowitz673d1982014-05-02 12:30:49 -0700365
Dave Borowitz29914cb2014-08-20 14:37:57 -0700366 protected static Writer newWriter(OutputStream os, HttpServletResponse res) throws IOException {
367 return new OutputStreamWriter(os, res.getCharacterEncoding());
Dave Borowitz673d1982014-05-02 12:30:49 -0700368 }
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700369
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800370 private Writer newWriter(HttpServletRequest req, HttpServletResponse res)
371 throws IOException {
372 OutputStream out;
373 if (acceptsGzipEncoding(req)) {
374 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
375 out = new GZIPOutputStream(res.getOutputStream());
376 } else {
377 out = res.getOutputStream();
378 }
379 return newWriter(out, res);
380 }
381
382 protected static boolean acceptsGzipEncoding(HttpServletRequest req) {
383 String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
384 if (accepts == null) {
385 return false;
386 }
387 for (int b = 0; b < accepts.length();) {
388 int comma = accepts.indexOf(',', b);
389 int e = 0 <= comma ? comma : accepts.length();
390 String term = accepts.substring(b, e).trim();
391 if (term.equals(ENCODING_GZIP)) {
392 return true;
393 }
394 b = e + 1;
395 }
396 return false;
397 }
398
399 protected static byte[] gzip(byte[] raw) throws IOException {
400 ByteArrayOutputStream out = new ByteArrayOutputStream();
401 try (GZIPOutputStream gz = new GZIPOutputStream(out)) {
402 gz.write(raw);
403 }
404 return out.toByteArray();
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700405 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700406}