blob: 476e8cc74e250b9a70c9e12acef9a5d320a5041d [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
Shawn Pearce10e68e62016-01-02 09:37:58 -080028import com.google.common.base.Optional;
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;
35
Dave Borowitz54271462013-11-11 11:43:11 -080036import org.joda.time.Instant;
37
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080038import java.io.ByteArrayOutputStream;
Dave Borowitz9de65952012-08-13 16:09:45 -070039import java.io.IOException;
Dave Borowitzfc2f00a2014-07-29 17:34:43 -070040import java.io.OutputStream;
Dave Borowitz673d1982014-05-02 12:30:49 -070041import java.io.OutputStreamWriter;
42import java.io.Writer;
Dave Borowitzb1c628f2013-01-11 11:28:20 -080043import java.lang.reflect.Type;
Dave Borowitz9de65952012-08-13 16:09:45 -070044import java.util.Map;
Shawn Pearcec4d3fd72015-02-10 14:32:37 -080045import java.util.zip.GZIPOutputStream;
Dave Borowitz9de65952012-08-13 16:09:45 -070046
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 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;
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);
79 } else {
80 return ImmutableMap.of("text", text);
81 }
82 }
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;
121 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.
137 * @return the default {@link FormatType} used when {@code ?format=} is not
138 * specified.
139 */
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 */
150 protected void doGetHtml(HttpServletRequest req, HttpServletResponse res)
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700151 throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800152 res.sendError(SC_BAD_REQUEST);
153 }
154
155 /**
156 * Handle a GET request when the requested format type was plain text.
157 *
158 * @param req in-progress request.
159 * @param res in-progress response.
160 */
161 protected void doGetText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700162 throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800163 res.sendError(SC_BAD_REQUEST);
164 }
165
166 /**
167 * Handle a GET request when the requested format type was JSON.
168 *
169 * @param req in-progress request.
170 * @param res in-progress response.
171 */
172 protected void doGetJson(HttpServletRequest req, HttpServletResponse res)
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700173 throws IOException {
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800174 res.sendError(SC_BAD_REQUEST);
175 }
176
Dave Borowitz9de65952012-08-13 16:09:45 -0700177 protected static Map<String, Object> getData(HttpServletRequest req) {
178 @SuppressWarnings("unchecked")
179 Map<String, Object> data = (Map<String, Object>) req.getAttribute(DATA_ATTRIBUTE);
180 if (data == null) {
181 data = Maps.newHashMap();
182 req.setAttribute(DATA_ATTRIBUTE, data);
183 }
184 return data;
185 }
186
187 protected final Renderer renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700188 private final GitilesAccess.Factory accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700189
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700190 protected BaseServlet(Renderer renderer, GitilesAccess.Factory accessFactory) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700191 this.renderer = renderer;
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700192 this.accessFactory = accessFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -0700193 }
194
195 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800196 * Render data to HTML using Soy.
197 *
198 * @param req in-progress request.
199 * @param res in-progress response.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800200 * @param templateName Soy template name; must be in one of the template files
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700201 * defined in {@link Renderer}.
202 * @param soyData data for Soy.
203 * @throws IOException an error occurred during rendering.
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800204 */
205 protected void renderHtml(HttpServletRequest req, HttpServletResponse res, String templateName,
Dave Borowitz9de65952012-08-13 16:09:45 -0700206 Map<String, ?> soyData) throws IOException {
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800207 renderer.render(req, res, templateName,
208 startHtmlResponse(req, res, soyData));
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700209 }
210
211 /**
212 * Start a streaming HTML response with header and footer rendered by Soy.
213 * <p>
214 * A streaming template includes the special template
215 * {@code gitiles.streamingPlaceholder} at the point where data is to be
216 * streamed. The template before and after this placeholder is rendered using
217 * the provided data map.
218 *
219 * @param req in-progress request.
220 * @param res in-progress response.
221 * @param templateName Soy template name; must be in one of the template files
222 * defined in {@link Renderer}.
223 * @param soyData data for Soy.
224 * @return output stream to render to. The portion of the template before the
225 * placeholder is already written and flushed; the portion after is
226 * written only on calling {@code close()}.
227 * @throws IOException an error occurred during rendering the header.
228 */
229 protected OutputStream startRenderStreamingHtml(HttpServletRequest req,
230 HttpServletResponse res, String templateName, Map<String, ?> soyData) throws IOException {
Dave Borowitzc99d0bb2014-07-31 15:39:39 -0700231 req.setAttribute(STREAMING_ATTRIBUTE, true);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700232 return renderer.renderStreaming(res, templateName, startHtmlResponse(req, res, soyData));
233 }
234
235 private Map<String, ?> startHtmlResponse(HttpServletRequest req, HttpServletResponse res,
236 Map<String, ?> soyData) throws IOException {
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700237 res.setContentType(FormatType.HTML.getMimeType());
David Pletcherd7bdaf32014-08-27 14:50:32 -0700238 res.setCharacterEncoding(UTF_8.name());
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700239 setCacheHeaders(res);
Dave Borowitz9de65952012-08-13 16:09:45 -0700240
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700241 Map<String, Object> allData = getData(req);
Dave Borowitz76bbefd2014-03-11 16:57:45 -0700242
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700243 GitilesConfig.putVariant(
244 getAccess(req).getConfig(), "customHeader", "headerVariant", allData);
245 allData.putAll(soyData);
246 GitilesView view = ViewFilter.getView(req);
247 if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) {
248 allData.put("repositoryName", view.getRepositoryName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700249 }
Dave Borowitzb7fd3f32014-05-01 12:31:25 -0700250 if (!allData.containsKey("breadcrumbs")) {
251 allData.put("breadcrumbs", view.getBreadcrumbs());
252 }
253
254 res.setStatus(HttpServletResponse.SC_OK);
Dave Borowitzfc2f00a2014-07-29 17:34:43 -0700255 return allData;
Dave Borowitz9de65952012-08-13 16:09:45 -0700256 }
257
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800258 /**
259 * Render data to JSON using GSON.
260 *
261 * @param req in-progress request.
262 * @param res in-progress response.
Dave Borowitz876b9812015-09-16 15:17:58 -0400263 * @param src @see com.google.gson.Gson#toJson(Object, Type, Appendable)
264 * @param typeOfSrc @see com.google.gson.Gson#toJson(Object, Type, Appendable)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800265 */
266 protected void renderJson(HttpServletRequest req, HttpServletResponse res, Object src,
267 Type typeOfSrc) throws IOException {
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700268 setApiHeaders(res, JSON);
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800269 res.setStatus(SC_OK);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800270 try (Writer writer = newWriter(req, res)) {
271 newGsonBuilder(req).create().toJson(src, typeOfSrc, writer);
272 writer.write('\n');
273 }
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800274 }
275
Dave Borowitz438c5282014-07-09 20:15:34 -0700276 @SuppressWarnings("unused") // Used in subclasses.
277 protected GsonBuilder newGsonBuilder(HttpServletRequest req) throws IOException {
278 return new GsonBuilder()
279 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
280 .setPrettyPrinting()
281 .generateNonExecutableJson();
282 }
283
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800284 /**
Dave Borowitz4f568702014-05-01 19:54:57 -0700285 * @see #startRenderText(HttpServletRequest, HttpServletResponse)
286 * @param req in-progress request.
287 * @param res in-progress response.
288 * @param contentType contentType to set.
289 * @return the response's writer.
290 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700291 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res,
Dave Borowitz4f568702014-05-01 19:54:57 -0700292 String contentType) throws IOException {
293 setApiHeaders(res, contentType);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800294 return newWriter(req, res);
Dave Borowitz4f568702014-05-01 19:54:57 -0700295 }
296
297 /**
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800298 * Prepare the response to render plain text.
299 * <p>
300 * Unlike
301 * {@link #renderHtml(HttpServletRequest, HttpServletResponse, String, Map)}
302 * and
303 * {@link #renderJson(HttpServletRequest, HttpServletResponse, Object, Type)},
304 * which assume the data to render is already completely prepared, this method
305 * does not write any data, only headers, and returns the response's
306 * ready-to-use writer.
307 *
308 * @param req in-progress request.
309 * @param res in-progress response.
310 * @return the response's writer.
311 */
Dave Borowitz673d1982014-05-02 12:30:49 -0700312 protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res)
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800313 throws IOException {
Dave Borowitz673d1982014-05-02 12:30:49 -0700314 return startRenderText(req, res, TEXT.getMimeType());
Dave Borowitzb1c628f2013-01-11 11:28:20 -0800315 }
316
Dave Borowitzba9c1182013-03-13 14:16:43 -0700317 /**
318 * Render an error as plain text.
319 *
320 * @param req in-progress request.
321 * @param res in-progress response.
322 * @param statusCode HTTP status code.
323 * @param message full message text.
324 *
325 * @throws IOException
326 */
327 protected void renderTextError(HttpServletRequest req, HttpServletResponse res, int statusCode,
328 String message) throws IOException {
329 res.setStatus(statusCode);
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700330 setApiHeaders(res, TEXT);
331 setCacheHeaders(res);
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800332 try (Writer out = newWriter(req, res)) {
333 out.write(message);
334 }
Dave Borowitzba9c1182013-03-13 14:16:43 -0700335 }
336
Dave Borowitz8d6d6872014-03-16 15:18:14 -0700337 protected GitilesAccess getAccess(HttpServletRequest req) {
338 GitilesAccess access = (GitilesAccess) req.getAttribute(ACCESS_ATTRIBUTE);
339 if (access == null) {
340 access = accessFactory.forRequest(req);
341 req.setAttribute(ACCESS_ATTRIBUTE, access);
342 }
343 return access;
344 }
345
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700346 protected void setCacheHeaders(HttpServletResponse res) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700347 setNotCacheable(res);
348 }
Dave Borowitz0c944762013-04-04 11:01:42 -0700349
Dave Borowitz4f568702014-05-01 19:54:57 -0700350 protected void setApiHeaders(HttpServletResponse res, String contentType) {
351 if (!Strings.isNullOrEmpty(contentType)) {
352 res.setContentType(contentType);
353 }
David Pletcherd7bdaf32014-08-27 14:50:32 -0700354 res.setCharacterEncoding(UTF_8.name());
Dave Borowitz0c944762013-04-04 11:01:42 -0700355 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment");
Dave Borowitz06edb502013-04-04 11:02:36 -0700356 res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700357 setCacheHeaders(res);
Dave Borowitz0c944762013-04-04 11:01:42 -0700358 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700359
Dave Borowitz4f568702014-05-01 19:54:57 -0700360 protected void setApiHeaders(HttpServletResponse res, FormatType type) {
361 setApiHeaders(res, type.getMimeType());
362 }
363
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700364 protected void setDownloadHeaders(HttpServletResponse res, String filename, String contentType) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700365 res.setContentType(contentType);
366 res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700367 setCacheHeaders(res);
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700368 }
Dave Borowitz673d1982014-05-02 12:30:49 -0700369
Dave Borowitz29914cb2014-08-20 14:37:57 -0700370 protected static Writer newWriter(OutputStream os, HttpServletResponse res) throws IOException {
371 return new OutputStreamWriter(os, res.getCharacterEncoding());
Dave Borowitz673d1982014-05-02 12:30:49 -0700372 }
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700373
Shawn Pearcec4d3fd72015-02-10 14:32:37 -0800374 private Writer newWriter(HttpServletRequest req, HttpServletResponse res)
375 throws IOException {
376 OutputStream out;
377 if (acceptsGzipEncoding(req)) {
378 res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
379 out = new GZIPOutputStream(res.getOutputStream());
380 } else {
381 out = res.getOutputStream();
382 }
383 return newWriter(out, res);
384 }
385
386 protected static boolean acceptsGzipEncoding(HttpServletRequest req) {
387 String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
388 if (accepts == null) {
389 return false;
390 }
391 for (int b = 0; b < accepts.length();) {
392 int comma = accepts.indexOf(',', b);
393 int e = 0 <= comma ? comma : accepts.length();
394 String term = accepts.substring(b, e).trim();
395 if (term.equals(ENCODING_GZIP)) {
396 return true;
397 }
398 b = e + 1;
399 }
400 return false;
401 }
402
403 protected static byte[] gzip(byte[] raw) throws IOException {
404 ByteArrayOutputStream out = new ByteArrayOutputStream();
405 try (GZIPOutputStream gz = new GZIPOutputStream(out)) {
406 gz.write(raw);
407 }
408 return out.toByteArray();
Dave Borowitzbf72ab72014-09-17 16:15:19 -0700409 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700410}