| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 1 | // Copyright 2015 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 | |
| 15 | package com.google.gitiles.doc; |
| 16 | |
| 17 | import static java.nio.charset.StandardCharsets.UTF_8; |
| 18 | import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; |
| 19 | import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; |
| 20 | import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; |
| 21 | import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; |
| 22 | import static org.eclipse.jgit.lib.FileMode.TYPE_FILE; |
| 23 | import static org.eclipse.jgit.lib.FileMode.TYPE_MASK; |
| 24 | import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; |
| 25 | |
| 26 | import com.google.common.base.MoreObjects; |
| 27 | import com.google.common.base.Strings; |
| 28 | import com.google.common.hash.Hasher; |
| 29 | import com.google.common.hash.Hashing; |
| 30 | import com.google.common.net.HttpHeaders; |
| 31 | import com.google.gitiles.BaseServlet; |
| 32 | import com.google.gitiles.FormatType; |
| 33 | import com.google.gitiles.GitilesAccess; |
| 34 | import com.google.gitiles.GitilesView; |
| 35 | import com.google.gitiles.Renderer; |
| 36 | import com.google.gitiles.ViewFilter; |
| 37 | |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 38 | import org.commonmark.node.Node; |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 39 | import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| 40 | import org.eclipse.jgit.http.server.ServletUtils; |
| 41 | import org.eclipse.jgit.lib.Config; |
| 42 | import org.eclipse.jgit.lib.Constants; |
| 43 | import org.eclipse.jgit.lib.ObjectId; |
| 44 | import org.eclipse.jgit.lib.ObjectLoader; |
| 45 | import org.eclipse.jgit.lib.ObjectReader; |
| 46 | import org.eclipse.jgit.lib.Repository; |
| 47 | import org.eclipse.jgit.revwalk.RevTree; |
| 48 | import org.eclipse.jgit.revwalk.RevWalk; |
| 49 | import org.eclipse.jgit.treewalk.TreeWalk; |
| 50 | import org.eclipse.jgit.util.RawParseUtils; |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 51 | |
| 52 | import java.io.IOException; |
| 53 | import java.util.HashMap; |
| 54 | import java.util.Map; |
| 55 | |
| 56 | import javax.servlet.http.HttpServletRequest; |
| 57 | import javax.servlet.http.HttpServletResponse; |
| 58 | |
| 59 | public class DocServlet extends BaseServlet { |
| 60 | private static final long serialVersionUID = 1L; |
| 61 | |
| 62 | private static final String INDEX_MD = "index.md"; |
| 63 | private static final String NAVBAR_MD = "navbar.md"; |
| 64 | private static final String SOY_FILE = "Doc.soy"; |
| 65 | private static final String SOY_TEMPLATE = "gitiles.markdownDoc"; |
| 66 | |
| 67 | // Generation of ETag logic. Bump this only if DocServlet logic changes |
| 68 | // significantly enough to impact cached pages. Soy template and source |
| 69 | // files are automatically hashed as part of the ETag. |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 70 | private static final int ETAG_GEN = 5; |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 71 | |
| 72 | public DocServlet(GitilesAccess.Factory accessFactory, Renderer renderer) { |
| 73 | super(renderer, accessFactory); |
| 74 | } |
| 75 | |
| 76 | @Override |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 77 | protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException { |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 78 | Config cfg = getAccess(req).getConfig(); |
| 79 | if (!cfg.getBoolean("markdown", "render", true)) { |
| 80 | res.setStatus(SC_NOT_FOUND); |
| 81 | return; |
| 82 | } |
| 83 | |
| 84 | GitilesView view = ViewFilter.getView(req); |
| 85 | Repository repo = ServletUtils.getRepository(req); |
| Shawn Pearce | b5ad0a0 | 2015-05-24 20:33:17 -0700 | [diff] [blame] | 86 | try (RevWalk rw = new RevWalk(repo)) { |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 87 | String path = view.getPathPart(); |
| 88 | RevTree root; |
| 89 | try { |
| 90 | root = rw.parseTree(view.getRevision().getId()); |
| 91 | } catch (IncorrectObjectTypeException e) { |
| 92 | res.setStatus(SC_NOT_FOUND); |
| 93 | return; |
| 94 | } |
| 95 | |
| 96 | SourceFile srcmd = findFile(rw, root, path); |
| 97 | if (srcmd == null) { |
| 98 | res.setStatus(SC_NOT_FOUND); |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | SourceFile navmd = findFile(rw, root, NAVBAR_MD); |
| 103 | String reqEtag = req.getHeader(HttpHeaders.IF_NONE_MATCH); |
| 104 | String curEtag = etag(srcmd, navmd); |
| 105 | if (reqEtag != null && reqEtag.equals(curEtag)) { |
| 106 | res.setStatus(SC_NOT_MODIFIED); |
| 107 | return; |
| 108 | } |
| 109 | |
| 110 | view = view.toBuilder().setPathPart(srcmd.path).build(); |
| 111 | int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20); |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 112 | Node doc = GitilesMarkdown.parse(srcmd.read(rw.getObjectReader(), inputLimit)); |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 113 | if (doc == null) { |
| Shawn Pearce | 576d7b6 | 2015-03-30 09:16:19 -0700 | [diff] [blame] | 114 | res.sendRedirect(GitilesView.show().copyFrom(view).toUrl()); |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 115 | return; |
| 116 | } |
| 117 | |
| Shawn Pearce | c32894e | 2016-05-17 15:25:20 -0400 | [diff] [blame] | 118 | String navPath = null; |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 119 | String navMarkdown = null; |
| 120 | Node nav = null; |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 121 | if (navmd != null) { |
| Shawn Pearce | c32894e | 2016-05-17 15:25:20 -0400 | [diff] [blame] | 122 | navPath = navmd.path; |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 123 | navMarkdown = navmd.read(rw.getObjectReader(), inputLimit); |
| 124 | nav = GitilesMarkdown.parse(navMarkdown); |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 125 | if (nav == null) { |
| 126 | res.setStatus(SC_INTERNAL_SERVER_ERROR); |
| 127 | return; |
| 128 | } |
| 129 | } |
| 130 | |
| Shawn Pearce | f75a2c6 | 2015-02-12 21:39:00 -0800 | [diff] [blame] | 131 | int imageLimit = cfg.getInt("markdown", "imageLimit", 256 << 10); |
| 132 | ImageLoader img = null; |
| 133 | if (imageLimit > 0) { |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 134 | img = new ImageLoader(rw.getObjectReader(), view, root, srcmd.path, imageLimit); |
| Shawn Pearce | f75a2c6 | 2015-02-12 21:39:00 -0800 | [diff] [blame] | 135 | } |
| 136 | |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 137 | res.setHeader(HttpHeaders.ETAG, curEtag); |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 138 | showDoc(req, res, view, cfg, img, navPath, navMarkdown, nav, srcmd.path, doc); |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 139 | } |
| 140 | } |
| 141 | |
| 142 | private String etag(SourceFile srcmd, SourceFile navmd) { |
| 143 | byte[] b = new byte[Constants.OBJECT_ID_LENGTH]; |
| 144 | Hasher h = Hashing.sha1().newHasher(); |
| 145 | h.putInt(ETAG_GEN); |
| 146 | |
| 147 | renderer.getTemplateHash(SOY_FILE).writeBytesTo(b, 0, b.length); |
| 148 | h.putBytes(b); |
| 149 | |
| 150 | if (navmd != null) { |
| 151 | navmd.id.copyRawTo(b, 0); |
| 152 | h.putBytes(b); |
| 153 | } |
| 154 | |
| 155 | srcmd.id.copyRawTo(b, 0); |
| 156 | h.putBytes(b); |
| 157 | return h.hash().toString(); |
| 158 | } |
| 159 | |
| 160 | @Override |
| 161 | protected void setCacheHeaders(HttpServletResponse res) { |
| 162 | long now = System.currentTimeMillis(); |
| 163 | res.setDateHeader(HttpHeaders.EXPIRES, now); |
| 164 | res.setDateHeader(HttpHeaders.DATE, now); |
| 165 | res.setHeader(HttpHeaders.CACHE_CONTROL, "private, max-age=0, must-revalidate"); |
| 166 | } |
| 167 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 168 | private void showDoc( |
| 169 | HttpServletRequest req, |
| 170 | HttpServletResponse res, |
| 171 | GitilesView view, |
| 172 | Config cfg, |
| 173 | ImageLoader img, |
| Shawn Pearce | c32894e | 2016-05-17 15:25:20 -0400 | [diff] [blame] | 174 | String navPath, |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 175 | String navMarkdown, |
| 176 | Node nav, |
| Shawn Pearce | c32894e | 2016-05-17 15:25:20 -0400 | [diff] [blame] | 177 | String docPath, |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 178 | Node doc) |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 179 | throws IOException { |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 180 | Map<String, Object> data = new HashMap<>(); |
| Shawn Pearce | 12c8fab | 2016-05-15 16:55:21 -0700 | [diff] [blame] | 181 | |
| 182 | MarkdownToHtml navHtml = new MarkdownToHtml(view, cfg, navPath); |
| 183 | data.putAll(Navbar.bannerSoyData(img, navHtml, navMarkdown, nav)); |
| 184 | data.put("navbarHtml", navHtml.toSoyHtml(nav)); |
| 185 | |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 186 | data.put("pageTitle", MoreObjects.firstNonNull(MarkdownUtil.getTitle(doc), view.getPathPart())); |
| Shawn Pearce | 68311c7 | 2015-06-09 17:01:34 -0700 | [diff] [blame] | 187 | if (view.getType() != GitilesView.Type.ROOTED_DOC) { |
| 188 | data.put("sourceUrl", GitilesView.show().copyFrom(view).toUrl()); |
| 189 | data.put("logUrl", GitilesView.log().copyFrom(view).toUrl()); |
| 190 | data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl()); |
| 191 | } |
| Shawn Pearce | c32894e | 2016-05-17 15:25:20 -0400 | [diff] [blame] | 192 | data.put("bodyHtml", new MarkdownToHtml(view, cfg, docPath).setImageLoader(img).toSoyHtml(doc)); |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 193 | |
| Shawn Pearce | bc381a4 | 2015-06-22 12:17:43 -0700 | [diff] [blame] | 194 | String analyticsId = cfg.getString("google", null, "analyticsId"); |
| 195 | if (!Strings.isNullOrEmpty(analyticsId)) { |
| 196 | data.put("analyticsId", analyticsId); |
| 197 | } |
| 198 | |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 199 | String page = renderer.render(SOY_TEMPLATE, data); |
| 200 | byte[] raw = page.getBytes(UTF_8); |
| 201 | res.setContentType(FormatType.HTML.getMimeType()); |
| 202 | res.setCharacterEncoding(UTF_8.name()); |
| 203 | setCacheHeaders(res); |
| 204 | if (acceptsGzipEncoding(req)) { |
| 205 | res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); |
| 206 | raw = gzip(raw); |
| 207 | } |
| 208 | res.setContentLength(raw.length); |
| 209 | res.setStatus(HttpServletResponse.SC_OK); |
| 210 | res.getOutputStream().write(raw); |
| 211 | } |
| 212 | |
| 213 | private static SourceFile findFile(RevWalk rw, RevTree root, String path) throws IOException { |
| 214 | if (Strings.isNullOrEmpty(path)) { |
| 215 | path = INDEX_MD; |
| 216 | } |
| 217 | |
| 218 | ObjectReader reader = rw.getObjectReader(); |
| 219 | TreeWalk tw = TreeWalk.forPath(reader, path, root); |
| 220 | if (tw == null) { |
| 221 | return null; |
| 222 | } |
| 223 | if ((tw.getRawMode(0) & TYPE_MASK) == TYPE_TREE) { |
| 224 | if (findIndexFile(tw)) { |
| 225 | path = tw.getPathString(); |
| 226 | } else { |
| 227 | return null; |
| 228 | } |
| 229 | } |
| 230 | if ((tw.getRawMode(0) & TYPE_MASK) == TYPE_FILE) { |
| 231 | if (!path.endsWith(".md")) { |
| 232 | return null; |
| 233 | } |
| 234 | return new SourceFile(path, tw.getObjectId(0)); |
| 235 | } |
| 236 | return null; |
| 237 | } |
| 238 | |
| 239 | private static boolean findIndexFile(TreeWalk tw) throws IOException { |
| 240 | tw.enterSubtree(); |
| 241 | while (tw.next()) { |
| Han-Wen Nienhuys | c0200f6 | 2016-05-02 17:34:51 +0200 | [diff] [blame] | 242 | if ((tw.getRawMode(0) & TYPE_MASK) == TYPE_FILE && INDEX_MD.equals(tw.getNameString())) { |
| Shawn Pearce | 374f184 | 2015-02-10 15:36:54 -0800 | [diff] [blame] | 243 | return true; |
| 244 | } |
| 245 | } |
| 246 | return false; |
| 247 | } |
| 248 | |
| 249 | private static class SourceFile { |
| 250 | final String path; |
| 251 | final ObjectId id; |
| 252 | |
| 253 | SourceFile(String path, ObjectId id) { |
| 254 | this.path = path; |
| 255 | this.id = id; |
| 256 | } |
| 257 | |
| 258 | String read(ObjectReader reader, int inputLimit) throws IOException { |
| 259 | ObjectLoader obj = reader.open(id, OBJ_BLOB); |
| 260 | byte[] raw = obj.getCachedBytes(inputLimit); |
| 261 | return RawParseUtils.decode(raw); |
| 262 | } |
| 263 | } |
| 264 | } |