blob: 874e59334e03a58fbc77e2a353a1dd3fa601c2f5 [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
AJ Ross001ea9b2016-08-23 13:40:04 -070028import com.google.common.base.Joiner;
Dave Borowitz4f568702014-05-01 19:54:57 -070029import com.google.common.base.Strings;
Dave Borowitz54271462013-11-11 11:43:11 -080030import com.google.common.collect.ImmutableMap;
31import com.google.common.collect.Maps;
32import com.google.common.net.HttpHeaders;
33import com.google.gson.FieldNamingPolicy;
34import com.google.gson.GsonBuilder;
Dave Borowitzae171e62017-05-11 17:51:37 -040035import java.io.BufferedWriter;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080036import java.io.ByteArrayOutputStream;
Dave Borowitz9de65952012-08-13 16:09:45 -070037import java.io.IOException;
Dave Borowitzfc2f00a2014-07-29 17:34:43 -070038import java.io.OutputStream;
Dave Borowitz673d1982014-05-02 12:30:49 -070039import java.io.OutputStreamWriter;
40import java.io.Writer;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080041import java.lang.reflect.Type;
Dave Borowitz9de65952012-08-13 16:09:45 -070042import java.util.Map;
David Pursehouse7a7f5472016-10-14 09:59:20 +090043import java.util.Optional;
AJ Ross001ea9b2016-08-23 13:40:04 -070044import java.util.regex.Pattern;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080045import java.util.zip.GZIPOutputStream;
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;
Dave Borowitz3b744b12016-08-19 16:11:10 -040050import org.joda.time.Instant;
Dave Borowitz9de65952012-08-13 16:09:45 -070051
52/** Base servlet class for Gitiles servlets that serve Soy templates. */
53public abstract class BaseServlet extends HttpServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080054 private static final long serialVersionUID = 1L;
Dave Borowitz8d6d6872014-03-16 15:18:14 -070055 private static final String ACCESS_ATTRIBUTE = BaseServlet.class.getName() + "/GitilesAccess";
Dave Borowitzc99d0bb2014-07-31 15:39:39 -070056 private static final String DATA_ATTRIBUTE = BaseServlet.class.getName() + "/Data";
57 private static final String STREAMING_ATTRIBUTE = BaseServlet.class.getName() + "/Streaming";
Dave Borowitz9de65952012-08-13 16:09:45 -070058
59 static void setNotCacheable(HttpServletResponse res) {
60 res.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate");
61 res.setHeader(HttpHeaders.PRAGMA, "no-cache");
Dave Borowitz70d84542015-08-16 15:08:35 -040062 res.setHeader(HttpHeaders.EXPIRES, "Mon, 01 Jan 1990 00:00:00 GMT");
Dave Borowitz9de65952012-08-13 16:09:45 -070063 res.setDateHeader(HttpHeaders.DATE, new Instant().getMillis());
64 }
65
66 public static BaseServlet notFoundServlet() {
Dave Borowitz8d6d6872014-03-16 15:18:14 -070067 return new BaseServlet(null, null) {
Chad Horohoead23f142012-11-12 09:45:39 -080068 private static final long serialVersionUID = 1L;
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020069
Dave Borowitz9de65952012-08-13 16:09:45 -070070 @Override
71 public void service(HttpServletRequest req, HttpServletResponse res) {
72 res.setStatus(SC_NOT_FOUND);
73 }
74 };
75 }
76
77 public static Map<String, String> menuEntry(String text, String url) {
78 if (url != null) {
79 return ImmutableMap.of("text", text, "url", url);
Dave Borowitz9de65952012-08-13 16:09:45 -070080 }
David Pursehouseb3b630f2016-06-15 21:51:18 +090081 return ImmutableMap.of("text", text);
Dave Borowitz9de65952012-08-13 16:09:45 -070082 }
83
Dave Borowitzc99d0bb2014-07-31 15:39:39 -070084 public static boolean isStreamingResponse(HttpServletRequest req) {
Dave Borowitzc410f962014-09-23 10:49:26 -070085 return firstNonNull((Boolean) req.getAttribute(STREAMING_ATTRIBUTE), false);
Dave Borowitzc99d0bb2014-07-31 15:39:39 -070086 }
87
Dave Borowitzded109a2014-03-03 15:25:39 -050088 protected static ArchiveFormat getArchiveFormat(GitilesAccess access) throws IOException {
89 return ArchiveFormat.getDefault(access.getConfig());
90 }
91
Dave Borowitz113c9352013-03-27 18:13:41 -040092 /**
93 * Put a value into a request's Soy data map.
94 *
95 * @param req in-progress request.
96 * @param key key.
97 * @param value Soy data value.
98 */
99 public static void putSoyData(HttpServletRequest req, String key, Object value) {
100 getData(req).put(key, value);
101 }
102
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800103 @Override
104 protected void doGet(HttpServletRequest req, HttpServletResponse res)
105 throws IOException, ServletException {
Shawn Pearce10e68e62016-01-02 09:37:58 -0800106 Optional<FormatType> format = getFormat(req);
107 if (!format.isPresent()) {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800108 res.sendError(SC_BAD_REQUEST);
109 return;
110 }
Shawn Pearce10e68e62016-01-02 09:37:58 -0800111 switch (format.get()) {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800112 case HTML:
113 doGetHtml(req, res);
114 break;
115 case TEXT:
116 doGetText(req, res);
117 break;
118 case JSON:
119 doGetJson(req, res);
120 break;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900121 case DEFAULT:
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800122 default:
123 res.sendError(SC_BAD_REQUEST);
124 break;
125 }
126 }
127
Shawn Pearce10e68e62016-01-02 09:37:58 -0800128 protected Optional<FormatType> getFormat(HttpServletRequest req) {
129 Optional<FormatType> format = FormatType.getFormatType(req);
130 if (format.isPresent() && format.get() == DEFAULT) {
131 return Optional.of(getDefaultFormat(req));
132 }
133 return format;
134 }
135
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800136 /**
137 * @param req in-progress request.
Dave Borowitz40255d52016-08-19 16:16:22 -0400138 * @return the default {@link FormatType} used when {@code ?format=} is not specified.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800139 */
140 protected FormatType getDefaultFormat(HttpServletRequest req) {
141 return HTML;
142 }
143
144 /**
145 * Handle a GET request when the requested format type was HTML.
146 *
147 * @param req in-progress request.
148 * @param res in-progress response.
149 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200150 protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800151 res.sendError(SC_BAD_REQUEST);
152 }
153
154 /**
155 * Handle a GET request when the requested format type was plain text.
156 *
157 * @param req in-progress request.
158 * @param res in-progress response.
159 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200160 protected void doGetText(HttpServletRequest req, HttpServletResponse res) throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800161 res.sendError(SC_BAD_REQUEST);
162 }
163
164 /**
165 * Handle a GET request when the requested format type was JSON.
166 *
167 * @param req in-progress request.
168 * @param res in-progress response.
169 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200170 protected void doGetJson(HttpServletRequest req, HttpServletResponse res) throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800171 res.sendError(SC_BAD_REQUEST);
172 }
173
Dave Borowitz9de65952012-08-13 16:09:45 -0700174 protected static Map<String, Object> getData(HttpServletRequest req) {
175 @SuppressWarnings("unchecked")
176 Map<String, Object> data = (Map<String, Object>) req.getAttribute(DATA_ATTRIBUTE);
177 if (data == null) {
178 data = Maps.newHashMap();
179 req.setAttribute(DATA_ATTRIBUTE, data);
180 }
181 return data;
182 }
183
184 protected final Renderer renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700185 private final GitilesAccess.Factory accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700186
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700187 protected BaseServlet(Renderer renderer, GitilesAccess.Factory accessFactory) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700188 this.renderer = renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700189 this.accessFactory = accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700190 }
191
192 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800193 * Render data to HTML using Soy.
194 *
195 * @param req in-progress request.
196 * @param res in-progress response.
Dave Borowitz40255d52016-08-19 16:16:22 -0400197 * @param templateName Soy template name; must be in one of the template files defined in {@link
198 * Renderer}.
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700199 * @param soyData data for Soy.
200 * @throws IOException an error occurred during rendering.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800201 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200202 protected void renderHtml(
203 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
204 throws IOException {
205 renderer.render(req, res, templateName, startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700206 }
207
208 /**
209 * Start a streaming HTML response with header and footer rendered by Soy.
Dave Borowitz40255d52016-08-19 16:16:22 -0400210 *
211 * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
212 * the point where data is to be streamed. The template before and after this placeholder is
213 * rendered using the provided data map.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700214 *
215 * @param req in-progress request.
216 * @param res in-progress response.
Dave Borowitz40255d52016-08-19 16:16:22 -0400217 * @param templateName Soy template name; must be in one of the template files defined in {@link
218 * Renderer}.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700219 * @param soyData data for Soy.
Dave Borowitz40255d52016-08-19 16:16:22 -0400220 * @return output stream to render to. The portion of the template before the placeholder is
221 * already written and flushed; the portion after is written only on calling {@code close()}.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700222 * @throws IOException an error occurred during rendering the header.
223 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200224 protected OutputStream startRenderStreamingHtml(
225 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
226 throws IOException {
Dave Borowitzc99d0bb2014-07-31 15:39:39 -0700227 req.setAttribute(STREAMING_ATTRIBUTE, true);
Shawn Pearce6451fa52017-06-29 20:47:05 -0700228 return renderer.renderStreaming(res, false, templateName, startHtmlResponse(req, res, soyData));
229 }
230
231 /**
232 * Start a compressed, streaming HTML response with header and footer rendered by Soy.
233 *
234 * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
235 * the point where data is to be streamed. The template before and after this placeholder is
236 * rendered using the provided data map.
237 *
238 * <p>The response will be gzip compressed (if the user agent supports it) to reduce bandwidth.
239 * This may delay rendering in the browser.
240 *
241 * @param req in-progress request.
242 * @param res in-progress response.
243 * @param templateName Soy template name; must be in one of the template files defined in {@link
244 * Renderer}.
245 * @param soyData data for Soy.
246 * @return output stream to render to. The portion of the template before the placeholder is
247 * already written and flushed; the portion after is written only on calling {@code close()}.
248 * @throws IOException an error occurred during rendering the header.
249 */
250 protected OutputStream startRenderCompressedStreamingHtml(
251 HttpServletRequest req,
252 HttpServletResponse res,
253 String templateName,
254 Map<String, ?> soyData)
255 throws IOException {
256 req.setAttribute(STREAMING_ATTRIBUTE, true);
257 boolean gzip = false;
258 if (acceptsGzipEncoding(req)) {
259 res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
260 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
261 gzip = true;
262 }
263 return renderer.renderStreaming(res, gzip, templateName, startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700264 }
265
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200266 private Map<String, ?> startHtmlResponse(
267 HttpServletRequest req, HttpServletResponse res, Map<String, ?> soyData) throws IOException {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700268 res.setContentType(FormatType.HTML.getMimeType());
David Pletcherd7bdaf32014-08-27 14:50:32 -0700269 res.setCharacterEncoding(UTF_8.name());
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800270 setCacheHeaders(req, res);
Dave Borowitz9de65952012-08-13 16:09:45 -0700271
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700272 Map<String, Object> allData = getData(req);
Dave Borowitz76bbefd2014-03-11 16:57:45 -0700273
Björn Pedersenbc0eaaa2016-03-29 15:30:29 +0200274 // for backwards compatibility, first try to access the old customHeader config var,
275 // then read the new customVariant variable.
276 GitilesConfig.putVariant(getAccess(req).getConfig(), "customHeader", "customVariant", allData);
277 GitilesConfig.putVariant(getAccess(req).getConfig(), "customVariant", "customVariant", allData);
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700278 allData.putAll(soyData);
279 GitilesView view = ViewFilter.getView(req);
280 if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) {
281 allData.put("repositoryName", view.getRepositoryName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700282 }
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700283 if (!allData.containsKey("breadcrumbs")) {
284 allData.put("breadcrumbs", view.getBreadcrumbs());
285 }
286
287 res.setStatus(HttpServletResponse.SC_OK);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700288 return allData;
Dave Borowitz9de65952012-08-13 16:09:45 -0700289 }
290
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800291 /**
292 * Render data to JSON using GSON.
293 *
294 * @param req in-progress request.
295 * @param res in-progress response.
Dave Borowitz876b9812015-09-16 15:17:58 -0400296 * @param src @see com.google.gson.Gson#toJson(Object, Type, Appendable)
297 * @param typeOfSrc @see com.google.gson.Gson#toJson(Object, Type, Appendable)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800298 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200299 protected void renderJson(
300 HttpServletRequest req, HttpServletResponse res, Object src, Type typeOfSrc)
301 throws IOException {
AJ Ross001ea9b2016-08-23 13:40:04 -0700302 setApiHeaders(req, res, JSON);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800303 res.setStatus(SC_OK);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800304 try (Writer writer = newWriter(req, res)) {
305 newGsonBuilder(req).create().toJson(src, typeOfSrc, writer);
306 writer.write('\n');
307 }
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800308 }
309
Dave Borowitz438c5282014-07-09 20:15:34 -0700310 @SuppressWarnings("unused") // Used in subclasses.
311 protected GsonBuilder newGsonBuilder(HttpServletRequest req) throws IOException {
312 return new GsonBuilder()
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200313 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
314 .setPrettyPrinting()
315 .generateNonExecutableJson();
Dave Borowitz438c5282014-07-09 20:15:34 -0700316 }
317
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800318 /**
Dave Borowitz4f568702014-05-01 19:54:57 -0700319 * @see #startRenderText(HttpServletRequest, HttpServletResponse)
320 * @param req in-progress request.
321 * @param res in-progress response.
322 * @param contentType contentType to set.
323 * @return the response's writer.
324 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200325 protected Writer startRenderText(
326 HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException {
AJ Ross001ea9b2016-08-23 13:40:04 -0700327 setApiHeaders(req, res, contentType);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800328 return newWriter(req, res);
Dave Borowitz4f568702014-05-01 19:54:57 -0700329 }
330
331 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800332 * Prepare the response to render plain text.
Dave Borowitz40255d52016-08-19 16:16:22 -0400333 *
334 * <p>Unlike {@link #renderHtml(HttpServletRequest, HttpServletResponse, String, Map)} and {@link
335 * #renderJson(HttpServletRequest, HttpServletResponse, Object, Type)}, which assume the data to
336 * render is already completely prepared, this method does not write any data, only headers, and
337 * returns the response's ready-to-use writer.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800338 *
339 * @param req in-progress request.
340 * @param res in-progress response.
341 * @return the response's writer.
342 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700343 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800344 throws IOException {
Dave Borowitz673d1982014-05-02 12:30:49 -0700345 return startRenderText(req, res, TEXT.getMimeType());
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800346 }
347
Dave Borowitzba9c1182013-03-13 14:16:43 -0700348 /**
349 * Render an error as plain text.
350 *
351 * @param req in-progress request.
352 * @param res in-progress response.
353 * @param statusCode HTTP status code.
354 * @param message full message text.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700355 * @throws IOException
356 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200357 protected void renderTextError(
358 HttpServletRequest req, HttpServletResponse res, int statusCode, String message)
359 throws IOException {
Dave Borowitzba9c1182013-03-13 14:16:43 -0700360 res.setStatus(statusCode);
AJ Ross001ea9b2016-08-23 13:40:04 -0700361 setApiHeaders(req, res, TEXT);
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800362 setCacheHeaders(req, res);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800363 try (Writer out = newWriter(req, res)) {
364 out.write(message);
365 }
Dave Borowitzba9c1182013-03-13 14:16:43 -0700366 }
367
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700368 protected GitilesAccess getAccess(HttpServletRequest req) {
369 GitilesAccess access = (GitilesAccess) req.getAttribute(ACCESS_ATTRIBUTE);
370 if (access == null) {
371 access = accessFactory.forRequest(req);
372 req.setAttribute(ACCESS_ATTRIBUTE, access);
373 }
374 return access;
375 }
376
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800377 protected void setCacheHeaders(HttpServletRequest req, HttpServletResponse res) {
David Pursehouseccaa85d2017-05-30 10:47:27 +0900378 if (Strings.nullToEmpty(req.getHeader(HttpHeaders.PRAGMA)).equalsIgnoreCase("no-cache")
379 || Strings.nullToEmpty(req.getHeader(HttpHeaders.CACHE_CONTROL))
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800380 .equalsIgnoreCase("no-cache")) {
381 setNotCacheable(res);
382 return;
383 }
384
385 GitilesView view = ViewFilter.getView(req);
386 Revision rev = view.getRevision();
387 if (rev.nameIsId()) {
David Pursehouseccaa85d2017-05-30 10:47:27 +0900388 res.setHeader(
389 HttpHeaders.CACHE_CONTROL, "private, max-age=7200, stale-while-revalidate=604800");
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800390 return;
391 }
392
Dave Borowitz9de65952012-08-13 16:09:45 -0700393 setNotCacheable(res);
394 }
Dave Borowitz0c944762013-04-04 11:01:42 -0700395
David Pursehouseccaa85d2017-05-30 10:47:27 +0900396 protected void setApiHeaders(HttpServletRequest req, HttpServletResponse res, String contentType)
397 throws IOException {
Dave Borowitz4f568702014-05-01 19:54:57 -0700398 if (!Strings.isNullOrEmpty(contentType)) {
399 res.setContentType(contentType);
400 }
David Pletcherd7bdaf32014-08-27 14:50:32 -0700401 res.setCharacterEncoding(UTF_8.name());
Dave Borowitz0c944762013-04-04 11:01:42 -0700402 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment");
AJ Ross001ea9b2016-08-23 13:40:04 -0700403
404 GitilesAccess access = getAccess(req);
405 String[] allowOrigin = access.getConfig().getStringList("gitiles", null, "allowOriginRegex");
406
407 if (allowOrigin.length > 0) {
408 String origin = req.getHeader(HttpHeaders.ORIGIN);
409 Pattern allowOriginPattern = Pattern.compile(Joiner.on("|").join(allowOrigin));
410
411 if (!Strings.isNullOrEmpty(origin) && allowOriginPattern.matcher(origin).matches()) {
412 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
AJ Ross066d93c2016-08-23 18:12:46 -0700413 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "X-Requested-With");
414 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
415 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET");
AJ Ross001ea9b2016-08-23 13:40:04 -0700416 }
417 } else {
418 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
419 }
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800420 setCacheHeaders(req, res);
Dave Borowitz0c944762013-04-04 11:01:42 -0700421 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700422
AJ Ross001ea9b2016-08-23 13:40:04 -0700423 protected void setApiHeaders(HttpServletRequest req, HttpServletResponse res, FormatType type)
424 throws IOException {
425 setApiHeaders(req, res, type.getMimeType());
Dave Borowitz4f568702014-05-01 19:54:57 -0700426 }
427
David Pursehouseccaa85d2017-05-30 10:47:27 +0900428 protected void setDownloadHeaders(
429 HttpServletRequest req, HttpServletResponse res, String filename, String contentType) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700430 res.setContentType(contentType);
431 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800432 setCacheHeaders(req, res);
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700433 }
Dave Borowitz673d1982014-05-02 12:30:49 -0700434
Dave Borowitz29914cb2014-08-20 14:37:57 -0700435 protected static Writer newWriter(OutputStream os, HttpServletResponse res) throws IOException {
Dave Borowitzae171e62017-05-11 17:51:37 -0400436 // StreamEncoder#write(int) is wasteful with its allocations, and we don't have much control
437 // over whether library code calls that variant as opposed to the saner write(char[], int, int).
438 // Protect against this by buffering.
439 return new BufferedWriter(new OutputStreamWriter(os, res.getCharacterEncoding()));
Dave Borowitz673d1982014-05-02 12:30:49 -0700440 }
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700441
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200442 private Writer newWriter(HttpServletRequest req, HttpServletResponse res) throws IOException {
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800443 OutputStream out;
444 if (acceptsGzipEncoding(req)) {
Shawn Pearceed3c2d12016-05-30 15:59:02 -0700445 res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800446 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
447 out = new GZIPOutputStream(res.getOutputStream());
448 } else {
449 out = res.getOutputStream();
450 }
451 return newWriter(out, res);
452 }
453
454 protected static boolean acceptsGzipEncoding(HttpServletRequest req) {
455 String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
456 if (accepts == null) {
457 return false;
458 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200459 for (int b = 0; b < accepts.length(); ) {
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800460 int comma = accepts.indexOf(',', b);
461 int e = 0 <= comma ? comma : accepts.length();
462 String term = accepts.substring(b, e).trim();
463 if (term.equals(ENCODING_GZIP)) {
464 return true;
465 }
466 b = e + 1;
467 }
468 return false;
469 }
470
471 protected static byte[] gzip(byte[] raw) throws IOException {
472 ByteArrayOutputStream out = new ByteArrayOutputStream();
473 try (GZIPOutputStream gz = new GZIPOutputStream(out)) {
474 gz.write(raw);
475 }
476 return out.toByteArray();
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700477 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700478}