blob: e721c26997361c886381a4c51b6c688a9013ecc0 [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;
18import static com.google.common.base.MoreObjects.toStringHelper;
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080019import static com.google.common.base.Preconditions.checkArgument;
Dave Borowitz9de65952012-08-13 16:09:45 -070020import static com.google.common.base.Preconditions.checkNotNull;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070021import static com.google.common.base.Preconditions.checkState;
David Pursehousefa845722016-10-04 16:26:17 +090022import static com.google.gitiles.GitilesUrls.escapeName;
David Pletcherd7bdaf32014-08-27 14:50:32 -070023import static java.nio.charset.StandardCharsets.UTF_8;
Dave Borowitz9de65952012-08-13 16:09:45 -070024
Dave Borowitze8a5e362013-01-14 16:07:26 -080025import com.google.common.annotations.VisibleForTesting;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070026import com.google.common.base.Joiner;
Dave Borowitzc410f962014-09-23 10:49:26 -070027import com.google.common.base.MoreObjects.ToStringHelper;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070028import com.google.common.base.Splitter;
Dave Borowitz9de65952012-08-13 16:09:45 -070029import com.google.common.base.Strings;
30import com.google.common.collect.ImmutableList;
31import com.google.common.collect.ImmutableMap;
32import com.google.common.collect.LinkedListMultimap;
33import com.google.common.collect.ListMultimap;
34import com.google.common.collect.Multimaps;
Dave Borowitz9de65952012-08-13 16:09:45 -070035import java.io.UnsupportedEncodingException;
36import java.net.URLEncoder;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070037import java.util.ArrayList;
Dave Borowitz27058932014-12-03 15:44:46 -080038import java.util.Arrays;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070039import java.util.EnumSet;
Dave Borowitz9de65952012-08-13 16:09:45 -070040import java.util.List;
41import java.util.Map;
Dave Borowitz9de65952012-08-13 16:09:45 -070042import javax.servlet.http.HttpServletRequest;
Dave Borowitz3b744b12016-08-19 16:11:10 -040043import org.eclipse.jgit.lib.Constants;
44import org.eclipse.jgit.revwalk.RevObject;
Dave Borowitz9de65952012-08-13 16:09:45 -070045
46/**
47 * Information about a view in Gitiles.
Dave Borowitz40255d52016-08-19 16:16:22 -040048 *
49 * <p>Views are uniquely identified by a type, and dispatched to servlet types by {@link
50 * GitilesServlet}. This class contains the list of all types, as well as some methods containing
51 * basic information parsed from the URL. Construction happens in {@link ViewFilter}.
Dave Borowitz9de65952012-08-13 16:09:45 -070052 */
53public class GitilesView {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070054 private static final String DEFAULT_ARCHIVE_EXTENSION = ".tar.gz";
55
Dave Borowitz9de65952012-08-13 16:09:45 -070056 /** All the possible view types supported in the application. */
David Pursehousee3d3ec82016-06-15 22:10:48 +090057 public enum Type {
Dave Borowitz9de65952012-08-13 16:09:45 -070058 HOST_INDEX,
59 REPOSITORY_INDEX,
Dave Borowitz209d0aa2012-12-28 14:28:53 -080060 REFS,
Dave Borowitz9de65952012-08-13 16:09:45 -070061 REVISION,
62 PATH,
Shawn Pearce353ba2f2015-02-12 10:22:37 -080063 SHOW,
Dave Borowitz9de65952012-08-13 16:09:45 -070064 DIFF,
Dave Borowitzba9c1182013-03-13 14:16:43 -070065 LOG,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070066 DESCRIBE,
Dave Borowitz68c7a9b2014-01-28 12:13:21 -080067 ARCHIVE,
Shawn Pearce374f1842015-02-10 15:36:54 -080068 BLAME,
Shawn Pearce68311c72015-06-09 17:01:34 -070069 DOC,
70 ROOTED_DOC;
Dave Borowitz9de65952012-08-13 16:09:45 -070071 }
72
Dave Borowitz6221d982013-01-10 10:39:20 -080073 /** Exception thrown when building a view that is invalid. */
74 public static class InvalidViewException extends IllegalStateException {
75 private static final long serialVersionUID = 1L;
76
77 public InvalidViewException(String msg) {
78 super(msg);
79 }
80 }
81
Dave Borowitz9de65952012-08-13 16:09:45 -070082 /** Builder for views. */
83 public static class Builder {
Shawn Pearce68311c72015-06-09 17:01:34 -070084 private Type type;
Dave Borowitz9de65952012-08-13 16:09:45 -070085 private final ListMultimap<String, String> params = LinkedListMultimap.create();
86
87 private String hostName;
88 private String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070089 private String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -070090 private String repositoryName;
91 private Revision revision = Revision.NULL;
92 private Revision oldRevision = Revision.NULL;
93 private String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070094 private String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -070095 private String anchor;
96
97 private Builder(Type type) {
98 this.type = type;
99 }
100
101 public Builder copyFrom(GitilesView other) {
Shawn Pearce68311c72015-06-09 17:01:34 -0700102 if (type == Type.DOC && other.type == Type.ROOTED_DOC) {
103 type = Type.ROOTED_DOC;
104 }
105
Dave Borowitz9de65952012-08-13 16:09:45 -0700106 hostName = other.hostName;
107 servletPath = other.servletPath;
108 switch (type) {
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700109 case HOST_INDEX:
110 repositoryPrefix = other.repositoryPrefix;
111 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700112 case LOG:
113 case DIFF:
114 oldRevision = other.oldRevision;
David Pursehouse0138abf2018-07-25 10:00:17 +0100115 // $FALL-THROUGH$
Dave Borowitz9de65952012-08-13 16:09:45 -0700116 case PATH:
Shawn Pearce374f1842015-02-10 15:36:54 -0800117 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700118 case ROOTED_DOC:
Dave Borowitz5051e672013-11-11 11:09:40 -0800119 case ARCHIVE:
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800120 case BLAME:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800121 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700122 path = other.path;
David Pursehouse0138abf2018-07-25 10:00:17 +0100123 // $FALL-THROUGH$
Dave Borowitz9de65952012-08-13 16:09:45 -0700124 case REVISION:
125 revision = other.revision;
David Pursehouse0138abf2018-07-25 10:00:17 +0100126 // $FALL-THROUGH$
Dave Borowitzba9c1182013-03-13 14:16:43 -0700127 case DESCRIBE:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800128 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700129 case REPOSITORY_INDEX:
130 repositoryName = other.repositoryName;
David Pursehouse0138abf2018-07-25 10:00:17 +0100131 // $FALL-THROUGH$
Chad Horohoead23f142012-11-12 09:45:39 -0800132 default:
133 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700134 }
Dave Borowitz58a96f22014-05-06 14:29:24 -0700135 if (other.type == type) {
136 // Only copy params for matching type.
137 params.putAll(other.params);
138 if (type == Type.ARCHIVE) {
139 extension = other.extension;
140 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700141 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700142 return this;
143 }
144
145 public Builder copyFrom(HttpServletRequest req) {
146 return copyFrom(ViewFilter.getView(req));
147 }
148
149 public Builder setHostName(String hostName) {
150 this.hostName = checkNotNull(hostName);
151 return this;
152 }
153
154 public String getHostName() {
155 return hostName;
156 }
157
158 public Builder setServletPath(String servletPath) {
159 this.servletPath = checkNotNull(servletPath);
160 return this;
161 }
162
163 public String getServletPath() {
164 return servletPath;
165 }
166
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700167 public Builder setRepositoryPrefix(String prefix) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900168 if (type == Type.HOST_INDEX) {
Dave Borowitz40255d52016-08-19 16:16:22 -0400169 this.repositoryPrefix =
170 prefix != null ? Strings.emptyToNull(maybeTrimLeadingAndTrailingSlash(prefix)) : null;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900171 return this;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700172 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900173 throw new IllegalStateException(
174 String.format("cannot set repository prefix on %s view", type));
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700175 }
176
Dave Borowitz9de65952012-08-13 16:09:45 -0700177 public Builder setRepositoryName(String repositoryName) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900178 if (type == Type.HOST_INDEX) {
179 throw new IllegalStateException(
180 String.format("cannot set repository name on %s view", type));
Dave Borowitz9de65952012-08-13 16:09:45 -0700181 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900182 this.repositoryName = checkNotNull(repositoryName);
183 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700184 }
185
186 public String getRepositoryName() {
187 return repositoryName;
188 }
189
190 public Builder setRevision(Revision revision) {
191 switch (type) {
192 case HOST_INDEX:
193 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800194 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700195 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700196 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
David Pursehousecb91aaf2016-06-15 22:05:24 +0900197 case ARCHIVE:
198 case BLAME:
199 case DIFF:
200 case DOC:
201 case LOG:
202 case PATH:
203 case REVISION:
204 case ROOTED_DOC:
205 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700206 default:
207 this.revision = checkNotNull(revision);
208 return this;
209 }
210 }
211
212 public Builder setRevision(String name) {
213 return setRevision(Revision.named(name));
214 }
215
216 public Builder setRevision(RevObject obj) {
217 return setRevision(Revision.peeled(obj.name(), obj));
218 }
219
220 public Builder setRevision(String name, RevObject obj) {
221 return setRevision(Revision.peeled(name, obj));
222 }
223
224 public Revision getRevision() {
225 return revision;
226 }
227
228 public Builder setOldRevision(Revision revision) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900229 if (type != Type.DIFF && type != Type.LOG) {
230 revision = firstNonNull(revision, Revision.NULL);
David Pursehousec53de2b2019-06-01 15:27:25 +0900231 checkState(Revision.isNull(revision), "cannot set old revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700232 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600233 this.oldRevision = revision;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700234 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700235 }
236
237 public Builder setOldRevision(RevObject obj) {
238 return setOldRevision(Revision.peeled(obj.name(), obj));
239 }
240
241 public Builder setOldRevision(String name, RevObject obj) {
242 return setOldRevision(Revision.peeled(name, obj));
243 }
244
245 public Revision getOldRevision() {
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700246 return oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700247 }
248
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700249 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700250 switch (type) {
251 case PATH:
252 case DIFF:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800253 case SHOW:
Dave Borowitz1488fed2013-06-26 11:11:40 -0600254 checkState(path != null, "cannot set null path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700255 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800256 case BLAME:
Dave Borowitz5051e672013-11-11 11:09:40 -0800257 case ARCHIVE:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700258 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800259 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700260 case LOG:
Shawn Pearce374f1842015-02-10 15:36:54 -0800261 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700262 case ROOTED_DOC:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700263 break;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900264 case HOST_INDEX:
265 case REPOSITORY_INDEX:
266 case REVISION:
Dave Borowitz9de65952012-08-13 16:09:45 -0700267 default:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700268 checkState(path == null, "cannot set path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700269 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700270 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600271 this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700272 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700273 }
274
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700275 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700276 return path;
277 }
278
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700279 public Builder setExtension(String extension) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900280 if (type != Type.ARCHIVE) {
281 checkState(extension == null, "cannot set extension on %s view", type);
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700282 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900283 this.extension = extension;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700284 return this;
285 }
286
287 public String getExtension() {
288 return extension;
289 }
290
Dave Borowitz9de65952012-08-13 16:09:45 -0700291 public Builder putParam(String key, String value) {
292 params.put(key, value);
293 return this;
294 }
295
296 public Builder replaceParam(String key, String value) {
297 params.replaceValues(key, ImmutableList.of(value));
298 return this;
299 }
300
301 public Builder putAllParams(Map<String, String[]> params) {
302 for (Map.Entry<String, String[]> e : params.entrySet()) {
Dave Borowitz27058932014-12-03 15:44:46 -0800303 this.params.putAll(e.getKey(), Arrays.asList(e.getValue()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700304 }
305 return this;
306 }
307
308 public ListMultimap<String, String> getParams() {
309 return params;
310 }
311
312 public Builder setAnchor(String anchor) {
313 this.anchor = anchor;
314 return this;
315 }
316
317 public String getAnchor() {
318 return anchor;
319 }
320
321 public GitilesView build() {
322 switch (type) {
323 case HOST_INDEX:
324 checkHostIndex();
325 break;
326 case REPOSITORY_INDEX:
327 checkRepositoryIndex();
328 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800329 case REFS:
330 checkRefs();
331 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700332 case DESCRIBE:
333 checkDescribe();
334 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700335 case REVISION:
336 checkRevision();
337 break;
338 case PATH:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800339 case SHOW:
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700340 case DOC:
Dave Borowitz9de65952012-08-13 16:09:45 -0700341 checkPath();
342 break;
343 case DIFF:
344 checkDiff();
345 break;
346 case LOG:
347 checkLog();
348 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700349 case ARCHIVE:
350 checkArchive();
351 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800352 case BLAME:
353 checkBlame();
354 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700355 case ROOTED_DOC:
356 checkRootedDoc();
Shawn Pearce374f1842015-02-10 15:36:54 -0800357 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700358 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200359 return new GitilesView(
360 type,
361 hostName,
362 servletPath,
363 repositoryPrefix,
364 repositoryName,
365 revision,
366 oldRevision,
367 path,
368 extension,
369 params,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700370 anchor);
Dave Borowitz9de65952012-08-13 16:09:45 -0700371 }
372
373 public String toUrl() {
374 return build().toUrl();
375 }
376
Dave Borowitz6221d982013-01-10 10:39:20 -0800377 private void checkView(boolean expr, String msg, Object... args) {
378 if (!expr) {
379 throw new InvalidViewException(String.format(msg, args));
380 }
381 }
382
Dave Borowitz9de65952012-08-13 16:09:45 -0700383 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800384 checkView(hostName != null, "missing hostName on %s view", type);
Shawn Pearce1b8322a2016-05-16 13:14:46 -0600385 checkView(servletPath != null, "missing servletPath on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700386 }
387
388 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800389 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700390 checkHostIndex();
391 }
392
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800393 private void checkRefs() {
394 checkRepositoryIndex();
395 }
396
Dave Borowitzba9c1182013-03-13 14:16:43 -0700397 private void checkDescribe() {
398 checkRepositoryIndex();
399 }
400
Dave Borowitz9de65952012-08-13 16:09:45 -0700401 private void checkRevision() {
David Pursehousec53de2b2019-06-01 15:27:25 +0900402 checkView(!Revision.isNull(revision), "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700403 checkRepositoryIndex();
404 }
405
406 private void checkDiff() {
407 checkPath();
408 }
409
410 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800411 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700412 }
413
414 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800415 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700416 checkRevision();
417 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700418
419 private void checkArchive() {
420 checkRevision();
421 }
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800422
423 private void checkBlame() {
424 checkPath();
425 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800426
Shawn Pearce68311c72015-06-09 17:01:34 -0700427 private void checkRootedDoc() {
428 checkView(hostName != null, "missing hostName on %s view", type);
429 checkView(servletPath != null, "missing hostName on %s view", type);
David Pursehousec53de2b2019-06-01 15:27:25 +0900430 checkView(!Revision.isNull(revision), "missing revision on %s view", type);
Shawn Pearce68311c72015-06-09 17:01:34 -0700431 checkView(path != null, "missing path on %s view", type);
432 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700433 }
434
435 public static Builder hostIndex() {
436 return new Builder(Type.HOST_INDEX);
437 }
438
439 public static Builder repositoryIndex() {
440 return new Builder(Type.REPOSITORY_INDEX);
441 }
442
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800443 public static Builder refs() {
444 return new Builder(Type.REFS);
445 }
446
Dave Borowitzba9c1182013-03-13 14:16:43 -0700447 public static Builder describe() {
448 return new Builder(Type.DESCRIBE);
449 }
450
Dave Borowitz9de65952012-08-13 16:09:45 -0700451 public static Builder revision() {
452 return new Builder(Type.REVISION);
453 }
454
455 public static Builder path() {
456 return new Builder(Type.PATH);
457 }
458
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800459 public static Builder show() {
460 return new Builder(Type.SHOW);
461 }
462
Dave Borowitz9de65952012-08-13 16:09:45 -0700463 public static Builder diff() {
464 return new Builder(Type.DIFF);
465 }
466
467 public static Builder log() {
468 return new Builder(Type.LOG);
469 }
470
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700471 public static Builder archive() {
472 return new Builder(Type.ARCHIVE);
473 }
474
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800475 public static Builder blame() {
476 return new Builder(Type.BLAME);
477 }
478
Shawn Pearce374f1842015-02-10 15:36:54 -0800479 public static Builder doc() {
480 return new Builder(Type.DOC);
481 }
482
Shawn Pearce68311c72015-06-09 17:01:34 -0700483 public static Builder rootedDoc() {
484 return new Builder(Type.ROOTED_DOC);
485 }
486
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800487 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700488 if (str.startsWith("/")) {
489 str = str.substring(1);
490 }
491 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
492 }
493
494 private final Type type;
495 private final String hostName;
496 private final String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700497 private final String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700498 private final String repositoryName;
499 private final Revision revision;
500 private final Revision oldRevision;
501 private final String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700502 private final String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700503 private final ListMultimap<String, String> params;
504 private final String anchor;
505
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200506 private GitilesView(
507 Type type,
Dave Borowitz9de65952012-08-13 16:09:45 -0700508 String hostName,
509 String servletPath,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700510 String repositoryPrefix,
Dave Borowitz9de65952012-08-13 16:09:45 -0700511 String repositoryName,
512 Revision revision,
513 Revision oldRevision,
514 String path,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700515 String extension,
Dave Borowitz9de65952012-08-13 16:09:45 -0700516 ListMultimap<String, String> params,
517 String anchor) {
518 this.type = type;
519 this.hostName = hostName;
520 this.servletPath = servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700521 this.repositoryPrefix = repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700522 this.repositoryName = repositoryName;
Dave Borowitzc410f962014-09-23 10:49:26 -0700523 this.revision = firstNonNull(revision, Revision.NULL);
524 this.oldRevision = firstNonNull(oldRevision, Revision.NULL);
Dave Borowitz9de65952012-08-13 16:09:45 -0700525 this.path = path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700526 this.extension = extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700527 this.params = Multimaps.unmodifiableListMultimap(params);
528 this.anchor = anchor;
529 }
530
Dave Borowitze02bd422014-05-01 11:44:39 -0700531 public Builder copyFrom(GitilesView other) {
532 return new Builder(other.type).copyFrom(this);
533 }
534
535 public Builder toBuilder() {
536 return copyFrom(this);
537 }
538
Dave Borowitz9de65952012-08-13 16:09:45 -0700539 public String getHostName() {
540 return hostName;
541 }
542
543 public String getServletPath() {
544 return servletPath;
545 }
546
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700547 public String getRepositoryPrefix() {
548 return repositoryPrefix;
549 }
550
Dave Borowitz9de65952012-08-13 16:09:45 -0700551 public String getRepositoryName() {
552 return repositoryName;
553 }
554
555 public Revision getRevision() {
556 return revision;
557 }
558
559 public Revision getOldRevision() {
560 return oldRevision;
561 }
562
563 public String getRevisionRange() {
David Pursehousec53de2b2019-06-01 15:27:25 +0900564 if (Revision.isNull(oldRevision)) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900565 if (type == Type.LOG || type == Type.DIFF) {
566 // For types that require two revisions, NULL indicates the empty
567 // tree/commit.
568 return revision.getName() + "^!";
Dave Borowitz9de65952012-08-13 16:09:45 -0700569 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900570 // For everything else NULL indicates it is not a range, just a single
571 // revision.
572 return null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700573 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
574 return revision.getName() + "^!";
575 } else {
576 return oldRevision.getName() + ".." + revision.getName();
577 }
578 }
579
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700580 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700581 return path;
582 }
583
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700584 public String getExtension() {
585 return extension;
586 }
587
Dave Borowitz9de65952012-08-13 16:09:45 -0700588 public ListMultimap<String, String> getParameters() {
589 return params;
590 }
591
592 public String getAnchor() {
593 return anchor;
594 }
595
596 public Type getType() {
597 return type;
598 }
599
Dave Borowitz5530a162013-06-19 15:14:47 -0700600 @Override
601 public String toString() {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200602 ToStringHelper b =
603 toStringHelper(type.toString())
604 .omitNullValues()
605 .add("host", hostName)
606 .add("servlet", servletPath)
607 .add("prefix", repositoryPrefix)
608 .add("repo", repositoryName)
609 .add("rev", revision)
610 .add("old", oldRevision)
611 .add("path", path)
612 .add("extension", extension);
Dave Borowitz5530a162013-06-19 15:14:47 -0700613 if (!params.isEmpty()) {
614 b.add("params", params);
615 }
616 b.add("anchor", anchor);
617 return b.toString();
618 }
619
Dave Borowitz9de65952012-08-13 16:09:45 -0700620 /** @return an escaped, relative URL representing this view. */
621 public String toUrl() {
622 StringBuilder url = new StringBuilder(servletPath).append('/');
623 ListMultimap<String, String> params = this.params;
624 switch (type) {
625 case HOST_INDEX:
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700626 if (repositoryPrefix != null) {
627 url.append(repositoryPrefix).append('/');
628 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700629 params = LinkedListMultimap.create();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700630 if (repositoryPrefix == null && !this.params.containsKey("format")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700631 params.put("format", FormatType.HTML.toString());
632 }
633 params.putAll(this.params);
634 break;
635 case REPOSITORY_INDEX:
636 url.append(repositoryName).append('/');
637 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800638 case REFS:
639 url.append(repositoryName).append("/+refs");
640 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700641 case DESCRIBE:
642 url.append(repositoryName).append("/+describe");
643 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700644 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800645 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700646 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700647 case ARCHIVE:
Dave Borowitz5051e672013-11-11 11:09:40 -0800648 url.append(repositoryName).append("/+archive/").append(revision.getName());
649 if (path != null) {
650 url.append('/').append(path);
651 }
Dave Borowitzc410f962014-09-23 10:49:26 -0700652 url.append(firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700653 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700654 case PATH:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200655 url.append(repositoryName)
656 .append("/+/")
657 .append(revision.getName())
658 .append('/')
Dave Borowitz9de65952012-08-13 16:09:45 -0700659 .append(path);
660 break;
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800661 case SHOW:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200662 url.append(repositoryName)
663 .append("/+show/")
664 .append(revision.getName())
665 .append('/')
666 .append(path);
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800667 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700668 case DIFF:
669 url.append(repositoryName).append("/+/");
670 if (isFirstParent(revision, oldRevision)) {
671 url.append(revision.getName()).append("^!");
672 } else {
673 url.append(oldRevision.getName()).append("..").append(revision.getName());
674 }
675 url.append('/').append(path);
676 break;
677 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800678 url.append(repositoryName).append("/+log");
David Pursehousec53de2b2019-06-01 15:27:25 +0900679 if (!Revision.isNull(revision)) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800680 url.append('/');
David Pursehousec53de2b2019-06-01 15:27:25 +0900681 if (!Revision.isNull(oldRevision)) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800682 url.append(oldRevision.getName()).append("..");
683 }
684 url.append(revision.getName());
685 if (path != null) {
686 url.append('/').append(path);
687 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700688 }
689 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800690 case BLAME:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200691 url.append(repositoryName)
692 .append("/+blame/")
693 .append(revision.getName())
694 .append('/')
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800695 .append(path);
696 break;
Shawn Pearce374f1842015-02-10 15:36:54 -0800697 case DOC:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800698 url.append(repositoryName);
699 if (path != null && path.endsWith(".md")) {
700 url.append("/+/");
701 } else {
702 url.append("/+doc/");
703 }
704 url.append(revision.getName());
Shawn Pearce374f1842015-02-10 15:36:54 -0800705 if (path != null) {
706 url.append('/').append(path);
707 }
708 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700709 case ROOTED_DOC:
710 if (path != null) {
711 url.append(path);
712 }
713 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700714 default:
715 throw new IllegalStateException("Unknown view type: " + type);
716 }
David Pursehousefa845722016-10-04 16:26:17 +0900717 String baseUrl = escapeName(url.toString());
Dave Borowitz9de65952012-08-13 16:09:45 -0700718 url = new StringBuilder();
719 if (!params.isEmpty()) {
720 url.append('?').append(paramsToString(params));
721 }
722 if (!Strings.isNullOrEmpty(anchor)) {
David Pursehousefa845722016-10-04 16:26:17 +0900723 url.append('#').append(escapeName(anchor));
Dave Borowitz9de65952012-08-13 16:09:45 -0700724 }
Dave Borowitz27058932014-12-03 15:44:46 -0800725 return baseUrl + url;
Dave Borowitz9de65952012-08-13 16:09:45 -0700726 }
727
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800728 /**
Dave Borowitz40255d52016-08-19 16:16:22 -0400729 * @return a list of maps with "text" and "url" keys for all file paths leading up to the path
730 * represented by this view. All URLs allow auto-diving into one-entry subtrees; see also
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700731 * {@link #getBreadcrumbs(List)}.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800732 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700733 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800734 return getBreadcrumbs(null);
735 }
736
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700737 private static final EnumSet<Type> NON_HTML_TYPES = EnumSet.of(Type.DESCRIBE, Type.ARCHIVE);
738
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800739 /**
Dave Borowitz40255d52016-08-19 16:16:22 -0400740 * @param hasSingleTree list of booleans, one per path entry in this view's path excluding the
741 * leaf. True entries indicate the tree at that path only has a single entry that is another
742 * tree.
743 * @return a list of maps with "text" and "url" keys for all file paths leading up to the path
744 * represented by this view. URLs whose corresponding entry in {@code hasSingleTree} is true
745 * will disable auto-diving into one-entry subtrees.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800746 */
747 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200748 checkArgument(!NON_HTML_TYPES.contains(type), "breadcrumbs for %s view not supported", type);
749 checkArgument(
750 type != Type.REFS || Strings.isNullOrEmpty(path),
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800751 "breadcrumbs for REFS view with path not supported");
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200752 checkArgument(
753 hasSingleTree == null || type == Type.PATH, "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700754 String path = this.path;
755 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700756 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this).setRepositoryPrefix(null)));
757 if (repositoryPrefix != null) {
758 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryPrefix));
759 } else if (repositoryName != null) {
760 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryName));
Dave Borowitz9de65952012-08-13 16:09:45 -0700761 }
762 if (type == Type.DIFF) {
763 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
764 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700765 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700766 } else if (type == Type.LOG) {
David Pursehousec53de2b2019-06-01 15:27:25 +0900767 if (!Revision.isNull(revision)) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800768 // TODO(dborowitz): Add something in the navigation area (probably not
769 // a breadcrumb) to allow switching between /+log/ and /+/.
David Pursehousec53de2b2019-06-01 15:27:25 +0900770 if (Revision.isNull(oldRevision)) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700771 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800772 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700773 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800774 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700775 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800776 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700777 }
778 path = Strings.emptyToNull(path);
David Pursehousec53de2b2019-06-01 15:27:25 +0900779 } else if (!Revision.isNull(revision)) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700780 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
781 }
782 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800783 if (type != Type.LOG && type != Type.REFS) {
784 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800785 breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700786 }
787 StringBuilder cur = new StringBuilder();
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800788 List<String> parts = PathUtil.SPLITTER.omitEmptyStrings().splitToList(path);
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200789 checkArgument(
790 hasSingleTree == null
791 || (parts.isEmpty() && hasSingleTree.isEmpty())
792 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800793 "hasSingleTree has wrong number of entries");
794 for (int i = 0; i < parts.size(); i++) {
795 String part = parts.get(i);
796 cur.append(part).append('/');
797 String curPath = cur.toString();
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800798 boolean isLeaf = i == parts.size() - 1;
799 Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800800 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
801 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700802 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800803 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700804 }
805 }
806 return breadcrumbs.build();
807 }
808
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700809 private List<Map<String, String>> hostIndexBreadcrumbs(String name) {
810 List<String> parts = Splitter.on('/').splitToList(name);
811 List<Map<String, String>> r = new ArrayList<>(parts.size());
812 for (int i = 0; i < parts.size(); i++) {
813 String prefix = Joiner.on('/').join(parts.subList(0, i + 1));
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200814 r.add(breadcrumb(parts.get(i), hostIndex().copyFrom(this).setRepositoryPrefix(prefix)));
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700815 }
816 return r;
817 }
818
Dave Borowitz9de65952012-08-13 16:09:45 -0700819 private static Map<String, String> breadcrumb(String text, Builder url) {
820 return ImmutableMap.of("text", text, "url", url.toUrl());
821 }
822
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800823 private Builder copyWithPath(boolean isLeaf) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700824 Builder copy;
825 switch (type) {
826 case DIFF:
827 copy = diff();
828 break;
829 case LOG:
830 copy = log();
831 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800832 case BLAME:
833 copy = isLeaf ? blame() : path();
834 break;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900835 case ARCHIVE:
836 case DESCRIBE:
837 case DOC:
838 case HOST_INDEX:
839 case PATH:
840 case REFS:
841 case REPOSITORY_INDEX:
842 case REVISION:
843 case ROOTED_DOC:
844 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700845 default:
846 copy = path();
847 break;
848 }
849 return copy.copyFrom(this);
850 }
851
852 private static boolean isFirstParent(Revision rev1, Revision rev2) {
David Pursehousec53de2b2019-06-01 15:27:25 +0900853 return Revision.isNull(rev2)
Dave Borowitz9de65952012-08-13 16:09:45 -0700854 || rev2.getName().equals(rev1.getName() + "^")
855 || rev2.getName().equals(rev1.getName() + "~1");
856 }
857
Dave Borowitze8a5e362013-01-14 16:07:26 -0800858 @VisibleForTesting
859 static String paramsToString(ListMultimap<String, String> params) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700860 try {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200861 StringBuilder sb = new StringBuilder();
862 boolean first = true;
863 for (Map.Entry<String, String> e : params.entries()) {
864 if (!first) {
865 sb.append('&');
866 } else {
867 first = false;
868 }
869 sb.append(URLEncoder.encode(e.getKey(), UTF_8.name()));
870 if (!"".equals(e.getValue())) {
871 sb.append('=').append(URLEncoder.encode(e.getValue(), UTF_8.name()));
872 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700873 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200874 return sb.toString();
Dave Borowitz9de65952012-08-13 16:09:45 -0700875 } catch (UnsupportedEncodingException e) {
876 throw new IllegalStateException(e);
877 }
878 }
879}