blob: 4b8a139b0353171e38252c973ead1eb822f7db01 [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;
David Pursehousec0037b82018-03-16 13:56:20 +090042import java.time.Instant;
Dave Borowitz9de65952012-08-13 16:09:45 -070043import java.util.Map;
David Pursehouse7a7f5472016-10-14 09:59:20 +090044import java.util.Optional;
AJ Ross001ea9b2016-08-23 13:40:04 -070045import java.util.regex.Pattern;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080046import java.util.zip.GZIPOutputStream;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080047import javax.servlet.ServletException;
Dave Borowitz9de65952012-08-13 16:09:45 -070048import javax.servlet.http.HttpServlet;
49import javax.servlet.http.HttpServletRequest;
50import javax.servlet.http.HttpServletResponse;
51
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 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");
Dave Borowitz70d84542015-08-16 15:08:35 -040061 res.setHeader(HttpHeaders.EXPIRES, "Mon, 01 Jan 1990 00:00:00 GMT");
David Pursehousec0037b82018-03-16 13:56:20 +090062 res.setDateHeader(HttpHeaders.DATE, Instant.now().toEpochMilli());
Dave Borowitz9de65952012-08-13 16:09:45 -070063 }
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;
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020068
Dave Borowitz9de65952012-08-13 16:09:45 -070069 @Override
70 public void service(HttpServletRequest req, HttpServletResponse res) {
71 res.setStatus(SC_NOT_FOUND);
72 }
73 };
74 }
75
76 public static Map<String, String> menuEntry(String text, String url) {
77 if (url != null) {
78 return ImmutableMap.of("text", text, "url", url);
Dave Borowitz9de65952012-08-13 16:09:45 -070079 }
David Pursehouseb3b630f2016-06-15 21:51:18 +090080 return ImmutableMap.of("text", text);
Dave Borowitz9de65952012-08-13 16:09:45 -070081 }
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 {
Shawn Pearce10e68e62016-01-02 09:37:58 -0800105 Optional<FormatType> format = getFormat(req);
106 if (!format.isPresent()) {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800107 res.sendError(SC_BAD_REQUEST);
108 return;
109 }
Shawn Pearce10e68e62016-01-02 09:37:58 -0800110 switch (format.get()) {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800111 case HTML:
112 doGetHtml(req, res);
113 break;
114 case TEXT:
115 doGetText(req, res);
116 break;
117 case JSON:
118 doGetJson(req, res);
119 break;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900120 case DEFAULT:
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800121 default:
122 res.sendError(SC_BAD_REQUEST);
123 break;
124 }
125 }
126
Shawn Pearce10e68e62016-01-02 09:37:58 -0800127 protected Optional<FormatType> getFormat(HttpServletRequest req) {
128 Optional<FormatType> format = FormatType.getFormatType(req);
129 if (format.isPresent() && format.get() == DEFAULT) {
130 return Optional.of(getDefaultFormat(req));
131 }
132 return format;
133 }
134
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800135 /**
136 * @param req in-progress request.
Dave Borowitz40255d52016-08-19 16:16:22 -0400137 * @return the default {@link FormatType} used when {@code ?format=} is not specified.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800138 */
139 protected FormatType getDefaultFormat(HttpServletRequest req) {
140 return HTML;
141 }
142
143 /**
144 * Handle a GET request when the requested format type was HTML.
145 *
146 * @param req in-progress request.
147 * @param res in-progress response.
148 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200149 protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800150 res.sendError(SC_BAD_REQUEST);
151 }
152
153 /**
154 * Handle a GET request when the requested format type was plain text.
155 *
156 * @param req in-progress request.
157 * @param res in-progress response.
158 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200159 protected void doGetText(HttpServletRequest req, HttpServletResponse res) throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800160 res.sendError(SC_BAD_REQUEST);
161 }
162
163 /**
164 * Handle a GET request when the requested format type was JSON.
165 *
166 * @param req in-progress request.
167 * @param res in-progress response.
168 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200169 protected void doGetJson(HttpServletRequest req, HttpServletResponse res) 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 Borowitz40255d52016-08-19 16:16:22 -0400196 * @param templateName Soy template name; must be in one of the template files defined in {@link
197 * Renderer}.
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700198 * @param soyData data for Soy.
199 * @throws IOException an error occurred during rendering.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800200 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200201 protected void renderHtml(
202 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
203 throws IOException {
204 renderer.render(req, res, templateName, 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.
Dave Borowitz40255d52016-08-19 16:16:22 -0400209 *
210 * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
211 * the point where data is to be streamed. The template before and after this placeholder is
212 * rendered using the provided data map.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700213 *
214 * @param req in-progress request.
215 * @param res in-progress response.
Dave Borowitz40255d52016-08-19 16:16:22 -0400216 * @param templateName Soy template name; must be in one of the template files defined in {@link
217 * Renderer}.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700218 * @param soyData data for Soy.
Dave Borowitz40255d52016-08-19 16:16:22 -0400219 * @return output stream to render to. The portion of the template before the placeholder is
220 * already written and flushed; the portion after is written only on calling {@code close()}.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700221 * @throws IOException an error occurred during rendering the header.
222 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200223 protected OutputStream startRenderStreamingHtml(
224 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
225 throws IOException {
Dave Borowitzc99d0bb2014-07-31 15:39:39 -0700226 req.setAttribute(STREAMING_ATTRIBUTE, true);
Shawn Pearce6451fa52017-06-29 20:47:05 -0700227 return renderer.renderStreaming(res, false, templateName, startHtmlResponse(req, res, soyData));
228 }
229
230 /**
231 * Start a compressed, streaming HTML response with header and footer rendered by Soy.
232 *
233 * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
234 * the point where data is to be streamed. The template before and after this placeholder is
235 * rendered using the provided data map.
236 *
237 * <p>The response will be gzip compressed (if the user agent supports it) to reduce bandwidth.
238 * This may delay rendering in the browser.
239 *
240 * @param req in-progress request.
241 * @param res in-progress response.
242 * @param templateName Soy template name; must be in one of the template files defined in {@link
243 * Renderer}.
244 * @param soyData data for Soy.
245 * @return output stream to render to. The portion of the template before the placeholder is
246 * already written and flushed; the portion after is written only on calling {@code close()}.
247 * @throws IOException an error occurred during rendering the header.
248 */
249 protected OutputStream startRenderCompressedStreamingHtml(
David Pursehousef39cadc2017-07-07 08:47:51 +0900250 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
Shawn Pearce6451fa52017-06-29 20:47:05 -0700251 throws IOException {
252 req.setAttribute(STREAMING_ATTRIBUTE, true);
253 boolean gzip = false;
254 if (acceptsGzipEncoding(req)) {
255 res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
256 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
257 gzip = true;
258 }
259 return renderer.renderStreaming(res, gzip, templateName, startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700260 }
261
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200262 private Map<String, ?> startHtmlResponse(
263 HttpServletRequest req, HttpServletResponse res, Map<String, ?> soyData) throws IOException {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700264 res.setContentType(FormatType.HTML.getMimeType());
David Pletcherd7bdaf32014-08-27 14:50:32 -0700265 res.setCharacterEncoding(UTF_8.name());
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800266 setCacheHeaders(req, res);
Dave Borowitz9de65952012-08-13 16:09:45 -0700267
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700268 Map<String, Object> allData = getData(req);
Dave Borowitz76bbefd2014-03-11 16:57:45 -0700269
Björn Pedersenbc0eaaa2016-03-29 15:30:29 +0200270 // for backwards compatibility, first try to access the old customHeader config var,
271 // then read the new customVariant variable.
272 GitilesConfig.putVariant(getAccess(req).getConfig(), "customHeader", "customVariant", allData);
273 GitilesConfig.putVariant(getAccess(req).getConfig(), "customVariant", "customVariant", allData);
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700274 allData.putAll(soyData);
275 GitilesView view = ViewFilter.getView(req);
276 if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) {
277 allData.put("repositoryName", view.getRepositoryName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700278 }
Shawn Pearce5c34e092017-06-29 21:18:30 -0700279 if (!allData.containsKey("breadcrumbs") && view.getRepositoryName() != null) {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700280 allData.put("breadcrumbs", view.getBreadcrumbs());
281 }
282
283 res.setStatus(HttpServletResponse.SC_OK);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700284 return allData;
Dave Borowitz9de65952012-08-13 16:09:45 -0700285 }
286
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800287 /**
288 * Render data to JSON using GSON.
289 *
290 * @param req in-progress request.
291 * @param res in-progress response.
Dave Borowitz876b9812015-09-16 15:17:58 -0400292 * @param src @see com.google.gson.Gson#toJson(Object, Type, Appendable)
293 * @param typeOfSrc @see com.google.gson.Gson#toJson(Object, Type, Appendable)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800294 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200295 protected void renderJson(
296 HttpServletRequest req, HttpServletResponse res, Object src, Type typeOfSrc)
297 throws IOException {
AJ Ross001ea9b2016-08-23 13:40:04 -0700298 setApiHeaders(req, res, JSON);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800299 res.setStatus(SC_OK);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800300 try (Writer writer = newWriter(req, res)) {
301 newGsonBuilder(req).create().toJson(src, typeOfSrc, writer);
302 writer.write('\n');
303 }
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800304 }
305
Dave Borowitz438c5282014-07-09 20:15:34 -0700306 @SuppressWarnings("unused") // Used in subclasses.
307 protected GsonBuilder newGsonBuilder(HttpServletRequest req) throws IOException {
308 return new GsonBuilder()
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200309 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
310 .setPrettyPrinting()
311 .generateNonExecutableJson();
Dave Borowitz438c5282014-07-09 20:15:34 -0700312 }
313
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800314 /**
Dave Borowitz4f568702014-05-01 19:54:57 -0700315 * @see #startRenderText(HttpServletRequest, HttpServletResponse)
316 * @param req in-progress request.
317 * @param res in-progress response.
318 * @param contentType contentType to set.
319 * @return the response's writer.
320 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200321 protected Writer startRenderText(
322 HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException {
AJ Ross001ea9b2016-08-23 13:40:04 -0700323 setApiHeaders(req, res, contentType);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800324 return newWriter(req, res);
Dave Borowitz4f568702014-05-01 19:54:57 -0700325 }
326
327 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800328 * Prepare the response to render plain text.
Dave Borowitz40255d52016-08-19 16:16:22 -0400329 *
330 * <p>Unlike {@link #renderHtml(HttpServletRequest, HttpServletResponse, String, Map)} and {@link
331 * #renderJson(HttpServletRequest, HttpServletResponse, Object, Type)}, which assume the data to
332 * render is already completely prepared, this method does not write any data, only headers, and
333 * returns the response's ready-to-use writer.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800334 *
335 * @param req in-progress request.
336 * @param res in-progress response.
337 * @return the response's writer.
338 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700339 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800340 throws IOException {
Dave Borowitz673d1982014-05-02 12:30:49 -0700341 return startRenderText(req, res, TEXT.getMimeType());
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800342 }
343
Dave Borowitzba9c1182013-03-13 14:16:43 -0700344 /**
345 * Render an error as plain text.
346 *
347 * @param req in-progress request.
348 * @param res in-progress response.
349 * @param statusCode HTTP status code.
350 * @param message full message text.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700351 * @throws IOException
352 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200353 protected void renderTextError(
354 HttpServletRequest req, HttpServletResponse res, int statusCode, String message)
355 throws IOException {
Dave Borowitzba9c1182013-03-13 14:16:43 -0700356 res.setStatus(statusCode);
AJ Ross001ea9b2016-08-23 13:40:04 -0700357 setApiHeaders(req, res, TEXT);
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800358 setCacheHeaders(req, res);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800359 try (Writer out = newWriter(req, res)) {
360 out.write(message);
361 }
Dave Borowitzba9c1182013-03-13 14:16:43 -0700362 }
363
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700364 protected GitilesAccess getAccess(HttpServletRequest req) {
Shawn Pearcedb394cc2017-07-01 11:45:20 -0700365 return GitilesAccess.getAccess(req, accessFactory);
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700366 }
367
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800368 protected void setCacheHeaders(HttpServletRequest req, HttpServletResponse res) {
David Pursehouseccaa85d2017-05-30 10:47:27 +0900369 if (Strings.nullToEmpty(req.getHeader(HttpHeaders.PRAGMA)).equalsIgnoreCase("no-cache")
370 || Strings.nullToEmpty(req.getHeader(HttpHeaders.CACHE_CONTROL))
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800371 .equalsIgnoreCase("no-cache")) {
372 setNotCacheable(res);
373 return;
374 }
375
376 GitilesView view = ViewFilter.getView(req);
377 Revision rev = view.getRevision();
378 if (rev.nameIsId()) {
David Pursehouseccaa85d2017-05-30 10:47:27 +0900379 res.setHeader(
380 HttpHeaders.CACHE_CONTROL, "private, max-age=7200, stale-while-revalidate=604800");
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800381 return;
382 }
383
Dave Borowitz9de65952012-08-13 16:09:45 -0700384 setNotCacheable(res);
385 }
Dave Borowitz0c944762013-04-04 11:01:42 -0700386
David Pursehouseccaa85d2017-05-30 10:47:27 +0900387 protected void setApiHeaders(HttpServletRequest req, HttpServletResponse res, String contentType)
388 throws IOException {
Dave Borowitz4f568702014-05-01 19:54:57 -0700389 if (!Strings.isNullOrEmpty(contentType)) {
390 res.setContentType(contentType);
391 }
David Pletcherd7bdaf32014-08-27 14:50:32 -0700392 res.setCharacterEncoding(UTF_8.name());
Dave Borowitz0c944762013-04-04 11:01:42 -0700393 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment");
AJ Ross001ea9b2016-08-23 13:40:04 -0700394
395 GitilesAccess access = getAccess(req);
396 String[] allowOrigin = access.getConfig().getStringList("gitiles", null, "allowOriginRegex");
397
398 if (allowOrigin.length > 0) {
399 String origin = req.getHeader(HttpHeaders.ORIGIN);
400 Pattern allowOriginPattern = Pattern.compile(Joiner.on("|").join(allowOrigin));
401
402 if (!Strings.isNullOrEmpty(origin) && allowOriginPattern.matcher(origin).matches()) {
403 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
AJ Ross066d93c2016-08-23 18:12:46 -0700404 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "X-Requested-With");
405 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
406 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET");
AJ Ross001ea9b2016-08-23 13:40:04 -0700407 }
408 } else {
409 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
410 }
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800411 setCacheHeaders(req, res);
Dave Borowitz0c944762013-04-04 11:01:42 -0700412 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700413
AJ Ross001ea9b2016-08-23 13:40:04 -0700414 protected void setApiHeaders(HttpServletRequest req, HttpServletResponse res, FormatType type)
415 throws IOException {
416 setApiHeaders(req, res, type.getMimeType());
Dave Borowitz4f568702014-05-01 19:54:57 -0700417 }
418
David Pursehouseccaa85d2017-05-30 10:47:27 +0900419 protected void setDownloadHeaders(
420 HttpServletRequest req, HttpServletResponse res, String filename, String contentType) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700421 res.setContentType(contentType);
422 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800423 setCacheHeaders(req, res);
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700424 }
Dave Borowitz673d1982014-05-02 12:30:49 -0700425
Dave Borowitz29914cb2014-08-20 14:37:57 -0700426 protected static Writer newWriter(OutputStream os, HttpServletResponse res) throws IOException {
Dave Borowitzae171e62017-05-11 17:51:37 -0400427 // StreamEncoder#write(int) is wasteful with its allocations, and we don't have much control
428 // over whether library code calls that variant as opposed to the saner write(char[], int, int).
429 // Protect against this by buffering.
430 return new BufferedWriter(new OutputStreamWriter(os, res.getCharacterEncoding()));
Dave Borowitz673d1982014-05-02 12:30:49 -0700431 }
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700432
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200433 private Writer newWriter(HttpServletRequest req, HttpServletResponse res) throws IOException {
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800434 OutputStream out;
435 if (acceptsGzipEncoding(req)) {
Shawn Pearceed3c2d12016-05-30 15:59:02 -0700436 res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800437 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
438 out = new GZIPOutputStream(res.getOutputStream());
439 } else {
440 out = res.getOutputStream();
441 }
442 return newWriter(out, res);
443 }
444
445 protected static boolean acceptsGzipEncoding(HttpServletRequest req) {
446 String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
447 if (accepts == null) {
448 return false;
449 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200450 for (int b = 0; b < accepts.length(); ) {
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800451 int comma = accepts.indexOf(',', b);
452 int e = 0 <= comma ? comma : accepts.length();
453 String term = accepts.substring(b, e).trim();
454 if (term.equals(ENCODING_GZIP)) {
455 return true;
456 }
457 b = e + 1;
458 }
459 return false;
460 }
461
462 protected static byte[] gzip(byte[] raw) throws IOException {
463 ByteArrayOutputStream out = new ByteArrayOutputStream();
464 try (GZIPOutputStream gz = new GZIPOutputStream(out)) {
465 gz.write(raw);
466 }
467 return out.toByteArray();
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700468 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700469}