blob: 2aea97d634d29e519cd4ef43eeae9f57c0b21450 [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;
Masaya Suzuki5cecb862019-03-25 17:35:44 -070033import com.google.gitiles.GitilesRequestFailureException.FailureReason;
Dave Borowitz54271462013-11-11 11:43:11 -080034import com.google.gson.FieldNamingPolicy;
35import com.google.gson.GsonBuilder;
Dave Borowitzae171e62017-05-11 17:51:37 -040036import java.io.BufferedWriter;
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;
David Pursehousec0037b82018-03-16 13:56:20 +090043import java.time.Instant;
Dave Borowitz9de65952012-08-13 16:09:45 -070044import java.util.Map;
David Pursehouse7a7f5472016-10-14 09:59:20 +090045import java.util.Optional;
AJ Ross001ea9b2016-08-23 13:40:04 -070046import java.util.regex.Pattern;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080047import java.util.zip.GZIPOutputStream;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080048import javax.servlet.ServletException;
Dave Borowitz9de65952012-08-13 16:09:45 -070049import javax.servlet.http.HttpServlet;
50import javax.servlet.http.HttpServletRequest;
51import javax.servlet.http.HttpServletResponse;
52
53/** Base servlet class for Gitiles servlets that serve Soy templates. */
54public abstract class BaseServlet extends HttpServlet {
Chad Horohoead23f142012-11-12 09:45:39 -080055 private static final long serialVersionUID = 1L;
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");
David Pursehousec0037b82018-03-16 13:56:20 +090063 res.setDateHeader(HttpHeaders.DATE, Instant.now().toEpochMilli());
Dave Borowitz9de65952012-08-13 16:09:45 -070064 }
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:
Masaya Suzuki5cecb862019-03-25 17:35:44 -0700123 throw new GitilesRequestFailureException(FailureReason.UNSUPPORTED_RESPONSE_FORMAT);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800124 }
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 /**
Matthias Sohna0d00c52023-09-30 21:27:05 +0200136 * Get default format
137 *
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800138 * @param req in-progress request.
Dave Borowitz40255d52016-08-19 16:16:22 -0400139 * @return the default {@link FormatType} used when {@code ?format=} is not specified.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800140 */
141 protected FormatType getDefaultFormat(HttpServletRequest req) {
142 return HTML;
143 }
144
145 /**
146 * Handle a GET request when the requested format type was HTML.
147 *
148 * @param req in-progress request.
149 * @param res in-progress response.
David Pursehouse3863a1e2019-06-01 15:34:47 +0900150 * @throws IOException if there was an error rendering the result.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800151 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200152 protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException {
Masaya Suzuki5cecb862019-03-25 17:35:44 -0700153 throw new GitilesRequestFailureException(FailureReason.UNSUPPORTED_RESPONSE_FORMAT);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800154 }
155
156 /**
157 * Handle a GET request when the requested format type was plain text.
158 *
159 * @param req in-progress request.
160 * @param res in-progress response.
David Pursehouse3863a1e2019-06-01 15:34:47 +0900161 * @throws IOException if there was an error rendering the result.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800162 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200163 protected void doGetText(HttpServletRequest req, HttpServletResponse res) throws IOException {
Masaya Suzuki5cecb862019-03-25 17:35:44 -0700164 throw new GitilesRequestFailureException(FailureReason.UNSUPPORTED_RESPONSE_FORMAT);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800165 }
166
167 /**
168 * Handle a GET request when the requested format type was JSON.
169 *
170 * @param req in-progress request.
171 * @param res in-progress response.
David Pursehouse3863a1e2019-06-01 15:34:47 +0900172 * @throws IOException if there was an error rendering the result.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800173 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200174 protected void doGetJson(HttpServletRequest req, HttpServletResponse res) throws IOException {
Masaya Suzuki5cecb862019-03-25 17:35:44 -0700175 throw new GitilesRequestFailureException(FailureReason.UNSUPPORTED_RESPONSE_FORMAT);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800176 }
177
Dave Borowitz9de65952012-08-13 16:09:45 -0700178 protected static Map<String, Object> getData(HttpServletRequest req) {
179 @SuppressWarnings("unchecked")
180 Map<String, Object> data = (Map<String, Object>) req.getAttribute(DATA_ATTRIBUTE);
181 if (data == null) {
182 data = Maps.newHashMap();
183 req.setAttribute(DATA_ATTRIBUTE, data);
184 }
185 return data;
186 }
187
188 protected final Renderer renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700189 private final GitilesAccess.Factory accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700190
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700191 protected BaseServlet(Renderer renderer, GitilesAccess.Factory accessFactory) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700192 this.renderer = renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700193 this.accessFactory = accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700194 }
195
196 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800197 * Render data to HTML using Soy.
198 *
199 * @param req in-progress request.
200 * @param res in-progress response.
Dave Borowitz40255d52016-08-19 16:16:22 -0400201 * @param templateName Soy template name; must be in one of the template files defined in {@link
202 * Renderer}.
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700203 * @param soyData data for Soy.
204 * @throws IOException an error occurred during rendering.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800205 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200206 protected void renderHtml(
207 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
208 throws IOException {
David Pursehouse28726042019-06-27 09:09:30 +0900209 renderer.renderHtml(req, res, templateName, startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700210 }
211
212 /**
213 * Start a streaming HTML response with header and footer rendered by Soy.
Dave Borowitz40255d52016-08-19 16:16:22 -0400214 *
215 * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
216 * the point where data is to be streamed. The template before and after this placeholder is
217 * rendered using the provided data map.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700218 *
219 * @param req in-progress request.
220 * @param res in-progress response.
Dave Borowitz40255d52016-08-19 16:16:22 -0400221 * @param templateName Soy template name; must be in one of the template files defined in {@link
222 * Renderer}.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700223 * @param soyData data for Soy.
Dave Borowitz40255d52016-08-19 16:16:22 -0400224 * @return output stream to render to. The portion of the template before the placeholder is
225 * already written and flushed; the portion after is written only on calling {@code close()}.
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700226 * @throws IOException an error occurred during rendering the header.
227 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200228 protected OutputStream startRenderStreamingHtml(
229 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
230 throws IOException {
Dave Borowitzc99d0bb2014-07-31 15:39:39 -0700231 req.setAttribute(STREAMING_ATTRIBUTE, true);
David Pursehouse28726042019-06-27 09:09:30 +0900232 return renderer.renderHtmlStreaming(
Kamil Musin34d50082022-12-09 15:48:47 +0100233 req, res, false, templateName, startHtmlResponse(req, res, soyData));
Shawn Pearce6451fa52017-06-29 20:47:05 -0700234 }
235
236 /**
237 * Start a compressed, streaming HTML response with header and footer rendered by Soy.
238 *
239 * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
240 * the point where data is to be streamed. The template before and after this placeholder is
241 * rendered using the provided data map.
242 *
243 * <p>The response will be gzip compressed (if the user agent supports it) to reduce bandwidth.
244 * This may delay rendering in the browser.
245 *
246 * @param req in-progress request.
247 * @param res in-progress response.
248 * @param templateName Soy template name; must be in one of the template files defined in {@link
249 * Renderer}.
250 * @param soyData data for Soy.
251 * @return output stream to render to. The portion of the template before the placeholder is
252 * already written and flushed; the portion after is written only on calling {@code close()}.
253 * @throws IOException an error occurred during rendering the header.
254 */
255 protected OutputStream startRenderCompressedStreamingHtml(
David Pursehousef39cadc2017-07-07 08:47:51 +0900256 HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
Shawn Pearce6451fa52017-06-29 20:47:05 -0700257 throws IOException {
258 req.setAttribute(STREAMING_ATTRIBUTE, true);
259 boolean gzip = false;
260 if (acceptsGzipEncoding(req)) {
261 res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
262 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
263 gzip = true;
264 }
David Pursehouse28726042019-06-27 09:09:30 +0900265 return renderer.renderHtmlStreaming(
Kamil Musin34d50082022-12-09 15:48:47 +0100266 req, res, gzip, templateName, startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700267 }
268
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200269 private Map<String, ?> startHtmlResponse(
270 HttpServletRequest req, HttpServletResponse res, Map<String, ?> soyData) throws IOException {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700271 res.setContentType(FormatType.HTML.getMimeType());
David Pletcherd7bdaf32014-08-27 14:50:32 -0700272 res.setCharacterEncoding(UTF_8.name());
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800273 setCacheHeaders(req, res);
Dave Borowitz9de65952012-08-13 16:09:45 -0700274
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700275 Map<String, Object> allData = getData(req);
Dave Borowitz76bbefd2014-03-11 16:57:45 -0700276
Björn Pedersenbc0eaaa2016-03-29 15:30:29 +0200277 // for backwards compatibility, first try to access the old customHeader config var,
278 // then read the new customVariant variable.
279 GitilesConfig.putVariant(getAccess(req).getConfig(), "customHeader", "customVariant", allData);
280 GitilesConfig.putVariant(getAccess(req).getConfig(), "customVariant", "customVariant", allData);
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700281 allData.putAll(soyData);
282 GitilesView view = ViewFilter.getView(req);
283 if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) {
284 allData.put("repositoryName", view.getRepositoryName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700285 }
Shawn Pearce5c34e092017-06-29 21:18:30 -0700286 if (!allData.containsKey("breadcrumbs") && view.getRepositoryName() != null) {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700287 allData.put("breadcrumbs", view.getBreadcrumbs());
288 }
289
290 res.setStatus(HttpServletResponse.SC_OK);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700291 return allData;
Dave Borowitz9de65952012-08-13 16:09:45 -0700292 }
293
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800294 /**
295 * Render data to JSON using GSON.
296 *
297 * @param req in-progress request.
298 * @param res in-progress response.
Matthias Sohn0cda8672023-09-30 22:00:01 +0200299 * @param src source, @see com.google.gson.Gson#toJson(Object, Type, Appendable)
300 * @param typeOfSrc type of source, @see com.google.gson.Gson#toJson(Object, Type, Appendable)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800301 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200302 protected void renderJson(
303 HttpServletRequest req, HttpServletResponse res, Object src, Type typeOfSrc)
304 throws IOException {
AJ Ross001ea9b2016-08-23 13:40:04 -0700305 setApiHeaders(req, res, JSON);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800306 res.setStatus(SC_OK);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800307 try (Writer writer = newWriter(req, res)) {
308 newGsonBuilder(req).create().toJson(src, typeOfSrc, writer);
309 writer.write('\n');
310 }
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800311 }
312
Dave Borowitz438c5282014-07-09 20:15:34 -0700313 @SuppressWarnings("unused") // Used in subclasses.
314 protected GsonBuilder newGsonBuilder(HttpServletRequest req) throws IOException {
315 return new GsonBuilder()
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200316 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
317 .setPrettyPrinting()
318 .generateNonExecutableJson();
Dave Borowitz438c5282014-07-09 20:15:34 -0700319 }
320
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800321 /**
Matthias Sohna0d00c52023-09-30 21:27:05 +0200322 * Start to render text.
323 *
Dave Borowitz4f568702014-05-01 19:54:57 -0700324 * @see #startRenderText(HttpServletRequest, HttpServletResponse)
325 * @param req in-progress request.
326 * @param res in-progress response.
327 * @param contentType contentType to set.
328 * @return the response's writer.
329 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200330 protected Writer startRenderText(
331 HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException {
AJ Ross001ea9b2016-08-23 13:40:04 -0700332 setApiHeaders(req, res, contentType);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800333 return newWriter(req, res);
Dave Borowitz4f568702014-05-01 19:54:57 -0700334 }
335
336 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800337 * Prepare the response to render plain text.
Dave Borowitz40255d52016-08-19 16:16:22 -0400338 *
339 * <p>Unlike {@link #renderHtml(HttpServletRequest, HttpServletResponse, String, Map)} and {@link
340 * #renderJson(HttpServletRequest, HttpServletResponse, Object, Type)}, which assume the data to
341 * render is already completely prepared, this method does not write any data, only headers, and
342 * returns the response's ready-to-use writer.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800343 *
344 * @param req in-progress request.
345 * @param res in-progress response.
346 * @return the response's writer.
347 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700348 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800349 throws IOException {
Dave Borowitz673d1982014-05-02 12:30:49 -0700350 return startRenderText(req, res, TEXT.getMimeType());
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800351 }
352
Dave Borowitzba9c1182013-03-13 14:16:43 -0700353 /**
354 * Render an error as plain text.
355 *
356 * @param req in-progress request.
357 * @param res in-progress response.
358 * @param statusCode HTTP status code.
359 * @param message full message text.
Matthias Sohn0cda8672023-09-30 22:00:01 +0200360 * @throws IOException if an IO error occurred.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700361 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200362 protected void renderTextError(
363 HttpServletRequest req, HttpServletResponse res, int statusCode, String message)
364 throws IOException {
Dave Borowitzba9c1182013-03-13 14:16:43 -0700365 res.setStatus(statusCode);
AJ Ross001ea9b2016-08-23 13:40:04 -0700366 setApiHeaders(req, res, TEXT);
Andrew Bonventrecc3418b2016-12-01 20:18:37 -0800367 setCacheHeaders(req, res);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800368 try (Writer out = newWriter(req, res)) {
369 out.write(message);
370 }
Dave Borowitzba9c1182013-03-13 14:16:43 -0700371 }
372
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700373 protected GitilesAccess getAccess(HttpServletRequest req) {
Shawn Pearcedb394cc2017-07-01 11:45:20 -0700374 return GitilesAccess.getAccess(req, accessFactory);
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700375 }
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}