blob: a6df07a97d15880b04e60f170fb5545d2539824a [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;
Dave Borowitz9de65952012-08-13 16:09:45 -070022import static com.google.gitiles.GitilesUrls.NAME_ESCAPER;
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;
Dave Borowitzc410f962014-09-23 10:49:26 -070026import com.google.common.base.MoreObjects.ToStringHelper;
Dave Borowitz9de65952012-08-13 16:09:45 -070027import com.google.common.base.Strings;
28import com.google.common.collect.ImmutableList;
29import com.google.common.collect.ImmutableMap;
30import com.google.common.collect.LinkedListMultimap;
31import com.google.common.collect.ListMultimap;
32import com.google.common.collect.Multimaps;
33
Dave Borowitz80334b22013-01-11 14:19:11 -080034import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070035import org.eclipse.jgit.revwalk.RevObject;
36
37import java.io.UnsupportedEncodingException;
38import java.net.URLEncoder;
Dave Borowitz27058932014-12-03 15:44:46 -080039import java.util.Arrays;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070040import java.util.EnumSet;
Dave Borowitz9de65952012-08-13 16:09:45 -070041import java.util.List;
42import java.util.Map;
43
44import javax.servlet.http.HttpServletRequest;
45
46/**
47 * Information about a view in Gitiles.
48 * <p>
49 * Views are uniquely identified by a type, and dispatched to servlet types by
50 * {@link GitilesServlet}. This class contains the list of all types, as
51 * well as some methods containing basic information parsed from the URL.
52 * Construction happens in {@link ViewFilter}.
53 */
54public class GitilesView {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070055 private static final String DEFAULT_ARCHIVE_EXTENSION = ".tar.gz";
56
Dave Borowitz9de65952012-08-13 16:09:45 -070057 /** All the possible view types supported in the application. */
58 public static enum Type {
59 HOST_INDEX,
60 REPOSITORY_INDEX,
Dave Borowitz209d0aa2012-12-28 14:28:53 -080061 REFS,
Dave Borowitz9de65952012-08-13 16:09:45 -070062 REVISION,
63 PATH,
Shawn Pearce353ba2f2015-02-12 10:22:37 -080064 SHOW,
Dave Borowitz9de65952012-08-13 16:09:45 -070065 DIFF,
Dave Borowitzba9c1182013-03-13 14:16:43 -070066 LOG,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070067 DESCRIBE,
Dave Borowitz68c7a9b2014-01-28 12:13:21 -080068 ARCHIVE,
Shawn Pearce374f1842015-02-10 15:36:54 -080069 BLAME,
70 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 {
84 private final Type type;
85 private final ListMultimap<String, String> params = LinkedListMultimap.create();
86
87 private String hostName;
88 private String servletPath;
89 private String repositoryName;
90 private Revision revision = Revision.NULL;
91 private Revision oldRevision = Revision.NULL;
92 private String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070093 private String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -070094 private String anchor;
95
96 private Builder(Type type) {
97 this.type = type;
98 }
99
100 public Builder copyFrom(GitilesView other) {
101 hostName = other.hostName;
102 servletPath = other.servletPath;
103 switch (type) {
104 case LOG:
105 case DIFF:
106 oldRevision = other.oldRevision;
107 // Fallthrough.
108 case PATH:
Shawn Pearce374f1842015-02-10 15:36:54 -0800109 case DOC:
Dave Borowitz5051e672013-11-11 11:09:40 -0800110 case ARCHIVE:
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800111 case BLAME:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800112 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700113 path = other.path;
114 // Fallthrough.
115 case REVISION:
116 revision = other.revision;
117 // Fallthrough.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700118 case DESCRIBE:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800119 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700120 case REPOSITORY_INDEX:
121 repositoryName = other.repositoryName;
Chad Horohoead23f142012-11-12 09:45:39 -0800122 // Fallthrough.
123 default:
124 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700125 }
Dave Borowitz58a96f22014-05-06 14:29:24 -0700126 if (other.type == type) {
127 // Only copy params for matching type.
128 params.putAll(other.params);
129 if (type == Type.ARCHIVE) {
130 extension = other.extension;
131 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700132 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700133 return this;
134 }
135
136 public Builder copyFrom(HttpServletRequest req) {
137 return copyFrom(ViewFilter.getView(req));
138 }
139
140 public Builder setHostName(String hostName) {
141 this.hostName = checkNotNull(hostName);
142 return this;
143 }
144
145 public String getHostName() {
146 return hostName;
147 }
148
149 public Builder setServletPath(String servletPath) {
150 this.servletPath = checkNotNull(servletPath);
151 return this;
152 }
153
154 public String getServletPath() {
155 return servletPath;
156 }
157
158 public Builder setRepositoryName(String repositoryName) {
159 switch (type) {
160 case HOST_INDEX:
161 throw new IllegalStateException(String.format(
162 "cannot set repository name on %s view", type));
163 default:
164 this.repositoryName = checkNotNull(repositoryName);
165 return this;
166 }
167 }
168
169 public String getRepositoryName() {
170 return repositoryName;
171 }
172
173 public Builder setRevision(Revision revision) {
174 switch (type) {
175 case HOST_INDEX:
176 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800177 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700178 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700179 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
180 default:
181 this.revision = checkNotNull(revision);
182 return this;
183 }
184 }
185
186 public Builder setRevision(String name) {
187 return setRevision(Revision.named(name));
188 }
189
190 public Builder setRevision(RevObject obj) {
191 return setRevision(Revision.peeled(obj.name(), obj));
192 }
193
194 public Builder setRevision(String name, RevObject obj) {
195 return setRevision(Revision.peeled(name, obj));
196 }
197
198 public Revision getRevision() {
199 return revision;
200 }
201
202 public Builder setOldRevision(Revision revision) {
203 switch (type) {
204 case DIFF:
205 case LOG:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700206 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700207 default:
Dave Borowitzc410f962014-09-23 10:49:26 -0700208 revision = firstNonNull(revision, Revision.NULL);
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700209 checkState(revision == Revision.NULL, "cannot set old revision on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700210 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700211 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600212 this.oldRevision = revision;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700213 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700214 }
215
216 public Builder setOldRevision(RevObject obj) {
217 return setOldRevision(Revision.peeled(obj.name(), obj));
218 }
219
220 public Builder setOldRevision(String name, RevObject obj) {
221 return setOldRevision(Revision.peeled(name, obj));
222 }
223
224 public Revision getOldRevision() {
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700225 return oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700226 }
227
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700228 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700229 switch (type) {
230 case PATH:
231 case DIFF:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800232 case SHOW:
Dave Borowitz1488fed2013-06-26 11:11:40 -0600233 checkState(path != null, "cannot set null path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700234 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800235 case BLAME:
Dave Borowitz5051e672013-11-11 11:09:40 -0800236 case ARCHIVE:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700237 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800238 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700239 case LOG:
Shawn Pearce374f1842015-02-10 15:36:54 -0800240 case DOC:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700241 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700242 default:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700243 checkState(path == null, "cannot set path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700244 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700245 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600246 this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700247 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700248 }
249
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700250 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700251 return path;
252 }
253
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700254 public Builder setExtension(String extension) {
255 switch (type) {
256 default:
257 checkState(extension == null, "cannot set path on %s view", type);
258 // Fallthrough;
259 case ARCHIVE:
260 this.extension = extension;
261 break;
262 }
263 return this;
264 }
265
266 public String getExtension() {
267 return extension;
268 }
269
Dave Borowitz9de65952012-08-13 16:09:45 -0700270 public Builder putParam(String key, String value) {
271 params.put(key, value);
272 return this;
273 }
274
275 public Builder replaceParam(String key, String value) {
276 params.replaceValues(key, ImmutableList.of(value));
277 return this;
278 }
279
280 public Builder putAllParams(Map<String, String[]> params) {
281 for (Map.Entry<String, String[]> e : params.entrySet()) {
Dave Borowitz27058932014-12-03 15:44:46 -0800282 this.params.putAll(e.getKey(), Arrays.asList(e.getValue()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700283 }
284 return this;
285 }
286
287 public ListMultimap<String, String> getParams() {
288 return params;
289 }
290
291 public Builder setAnchor(String anchor) {
292 this.anchor = anchor;
293 return this;
294 }
295
296 public String getAnchor() {
297 return anchor;
298 }
299
300 public GitilesView build() {
301 switch (type) {
302 case HOST_INDEX:
303 checkHostIndex();
304 break;
305 case REPOSITORY_INDEX:
306 checkRepositoryIndex();
307 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800308 case REFS:
309 checkRefs();
310 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700311 case DESCRIBE:
312 checkDescribe();
313 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700314 case REVISION:
315 checkRevision();
316 break;
317 case PATH:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800318 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700319 checkPath();
320 break;
321 case DIFF:
322 checkDiff();
323 break;
324 case LOG:
325 checkLog();
326 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700327 case ARCHIVE:
328 checkArchive();
329 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800330 case BLAME:
331 checkBlame();
332 break;
Shawn Pearce374f1842015-02-10 15:36:54 -0800333 case DOC:
334 checkDoc();
335 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700336 }
337 return new GitilesView(type, hostName, servletPath, repositoryName, revision,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700338 oldRevision, path, extension, params, anchor);
Dave Borowitz9de65952012-08-13 16:09:45 -0700339 }
340
341 public String toUrl() {
342 return build().toUrl();
343 }
344
Dave Borowitz6221d982013-01-10 10:39:20 -0800345 private void checkView(boolean expr, String msg, Object... args) {
346 if (!expr) {
347 throw new InvalidViewException(String.format(msg, args));
348 }
349 }
350
Dave Borowitz9de65952012-08-13 16:09:45 -0700351 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800352 checkView(hostName != null, "missing hostName on %s view", type);
353 checkView(servletPath != null, "missing hostName on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700354 }
355
356 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800357 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700358 checkHostIndex();
359 }
360
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800361 private void checkRefs() {
362 checkRepositoryIndex();
363 }
364
Dave Borowitzba9c1182013-03-13 14:16:43 -0700365 private void checkDescribe() {
366 checkRepositoryIndex();
367 }
368
Dave Borowitz9de65952012-08-13 16:09:45 -0700369 private void checkRevision() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800370 checkView(revision != Revision.NULL, "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700371 checkRepositoryIndex();
372 }
373
374 private void checkDiff() {
375 checkPath();
376 }
377
378 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800379 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700380 }
381
382 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800383 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700384 checkRevision();
385 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700386
387 private void checkArchive() {
388 checkRevision();
389 }
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800390
391 private void checkBlame() {
392 checkPath();
393 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800394
395 private void checkDoc() {
396 checkRevision();
397 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700398 }
399
400 public static Builder hostIndex() {
401 return new Builder(Type.HOST_INDEX);
402 }
403
404 public static Builder repositoryIndex() {
405 return new Builder(Type.REPOSITORY_INDEX);
406 }
407
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800408 public static Builder refs() {
409 return new Builder(Type.REFS);
410 }
411
Dave Borowitzba9c1182013-03-13 14:16:43 -0700412 public static Builder describe() {
413 return new Builder(Type.DESCRIBE);
414 }
415
Dave Borowitz9de65952012-08-13 16:09:45 -0700416 public static Builder revision() {
417 return new Builder(Type.REVISION);
418 }
419
420 public static Builder path() {
421 return new Builder(Type.PATH);
422 }
423
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800424 public static Builder show() {
425 return new Builder(Type.SHOW);
426 }
427
Dave Borowitz9de65952012-08-13 16:09:45 -0700428 public static Builder diff() {
429 return new Builder(Type.DIFF);
430 }
431
432 public static Builder log() {
433 return new Builder(Type.LOG);
434 }
435
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700436 public static Builder archive() {
437 return new Builder(Type.ARCHIVE);
438 }
439
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800440 public static Builder blame() {
441 return new Builder(Type.BLAME);
442 }
443
Shawn Pearce374f1842015-02-10 15:36:54 -0800444 public static Builder doc() {
445 return new Builder(Type.DOC);
446 }
447
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800448 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700449 if (str.startsWith("/")) {
450 str = str.substring(1);
451 }
452 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
453 }
454
455 private final Type type;
456 private final String hostName;
457 private final String servletPath;
458 private final String repositoryName;
459 private final Revision revision;
460 private final Revision oldRevision;
461 private final String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700462 private final String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700463 private final ListMultimap<String, String> params;
464 private final String anchor;
465
466 private GitilesView(Type type,
467 String hostName,
468 String servletPath,
469 String repositoryName,
470 Revision revision,
471 Revision oldRevision,
472 String path,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700473 String extension,
Dave Borowitz9de65952012-08-13 16:09:45 -0700474 ListMultimap<String, String> params,
475 String anchor) {
476 this.type = type;
477 this.hostName = hostName;
478 this.servletPath = servletPath;
479 this.repositoryName = repositoryName;
Dave Borowitzc410f962014-09-23 10:49:26 -0700480 this.revision = firstNonNull(revision, Revision.NULL);
481 this.oldRevision = firstNonNull(oldRevision, Revision.NULL);
Dave Borowitz9de65952012-08-13 16:09:45 -0700482 this.path = path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700483 this.extension = extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700484 this.params = Multimaps.unmodifiableListMultimap(params);
485 this.anchor = anchor;
486 }
487
Dave Borowitze02bd422014-05-01 11:44:39 -0700488 public Builder copyFrom(GitilesView other) {
489 return new Builder(other.type).copyFrom(this);
490 }
491
492 public Builder toBuilder() {
493 return copyFrom(this);
494 }
495
Dave Borowitz9de65952012-08-13 16:09:45 -0700496 public String getHostName() {
497 return hostName;
498 }
499
500 public String getServletPath() {
501 return servletPath;
502 }
503
504 public String getRepositoryName() {
505 return repositoryName;
506 }
507
508 public Revision getRevision() {
509 return revision;
510 }
511
512 public Revision getOldRevision() {
513 return oldRevision;
514 }
515
516 public String getRevisionRange() {
517 if (oldRevision == Revision.NULL) {
518 switch (type) {
519 case LOG:
520 case DIFF:
521 // For types that require two revisions, NULL indicates the empty
522 // tree/commit.
523 return revision.getName() + "^!";
524 default:
525 // For everything else NULL indicates it is not a range, just a single
526 // revision.
527 return null;
528 }
529 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
530 return revision.getName() + "^!";
531 } else {
532 return oldRevision.getName() + ".." + revision.getName();
533 }
534 }
535
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700536 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700537 return path;
538 }
539
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700540 public String getExtension() {
541 return extension;
542 }
543
Dave Borowitz9de65952012-08-13 16:09:45 -0700544 public ListMultimap<String, String> getParameters() {
545 return params;
546 }
547
548 public String getAnchor() {
549 return anchor;
550 }
551
552 public Type getType() {
553 return type;
554 }
555
Dave Borowitz5530a162013-06-19 15:14:47 -0700556 @Override
557 public String toString() {
Dave Borowitzc410f962014-09-23 10:49:26 -0700558 ToStringHelper b = toStringHelper(type.toString())
Dave Borowitz5530a162013-06-19 15:14:47 -0700559 .omitNullValues()
560 .add("host", hostName)
561 .add("servlet", servletPath)
562 .add("repo", repositoryName)
563 .add("rev", revision)
564 .add("old", oldRevision)
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700565 .add("path", path)
566 .add("extension", extension);
Dave Borowitz5530a162013-06-19 15:14:47 -0700567 if (!params.isEmpty()) {
568 b.add("params", params);
569 }
570 b.add("anchor", anchor);
571 return b.toString();
572 }
573
Dave Borowitz9de65952012-08-13 16:09:45 -0700574 /** @return an escaped, relative URL representing this view. */
575 public String toUrl() {
576 StringBuilder url = new StringBuilder(servletPath).append('/');
577 ListMultimap<String, String> params = this.params;
578 switch (type) {
579 case HOST_INDEX:
580 params = LinkedListMultimap.create();
581 if (!this.params.containsKey("format")) {
582 params.put("format", FormatType.HTML.toString());
583 }
584 params.putAll(this.params);
585 break;
586 case REPOSITORY_INDEX:
587 url.append(repositoryName).append('/');
588 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800589 case REFS:
590 url.append(repositoryName).append("/+refs");
591 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700592 case DESCRIBE:
593 url.append(repositoryName).append("/+describe");
594 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700595 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800596 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700597 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700598 case ARCHIVE:
Dave Borowitz5051e672013-11-11 11:09:40 -0800599 url.append(repositoryName).append("/+archive/").append(revision.getName());
600 if (path != null) {
601 url.append('/').append(path);
602 }
Dave Borowitzc410f962014-09-23 10:49:26 -0700603 url.append(firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700604 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700605 case PATH:
606 url.append(repositoryName).append("/+/").append(revision.getName()).append('/')
607 .append(path);
608 break;
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800609 case SHOW:
610 url.append(repositoryName).append("/+show/").append(revision.getName())
611 .append('/').append(path);
612 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700613 case DIFF:
614 url.append(repositoryName).append("/+/");
615 if (isFirstParent(revision, oldRevision)) {
616 url.append(revision.getName()).append("^!");
617 } else {
618 url.append(oldRevision.getName()).append("..").append(revision.getName());
619 }
620 url.append('/').append(path);
621 break;
622 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800623 url.append(repositoryName).append("/+log");
624 if (revision != Revision.NULL) {
625 url.append('/');
626 if (oldRevision != Revision.NULL) {
627 url.append(oldRevision.getName()).append("..");
628 }
629 url.append(revision.getName());
630 if (path != null) {
631 url.append('/').append(path);
632 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700633 }
634 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800635 case BLAME:
636 url.append(repositoryName).append("/+blame/").append(revision.getName()).append('/')
637 .append(path);
638 break;
Shawn Pearce374f1842015-02-10 15:36:54 -0800639 case DOC:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800640 url.append(repositoryName);
641 if (path != null && path.endsWith(".md")) {
642 url.append("/+/");
643 } else {
644 url.append("/+doc/");
645 }
646 url.append(revision.getName());
Shawn Pearce374f1842015-02-10 15:36:54 -0800647 if (path != null) {
648 url.append('/').append(path);
649 }
650 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700651 default:
652 throw new IllegalStateException("Unknown view type: " + type);
653 }
654 String baseUrl = NAME_ESCAPER.apply(url.toString());
655 url = new StringBuilder();
656 if (!params.isEmpty()) {
657 url.append('?').append(paramsToString(params));
658 }
659 if (!Strings.isNullOrEmpty(anchor)) {
660 url.append('#').append(NAME_ESCAPER.apply(anchor));
661 }
Dave Borowitz27058932014-12-03 15:44:46 -0800662 return baseUrl + url;
Dave Borowitz9de65952012-08-13 16:09:45 -0700663 }
664
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800665 /**
666 * @return a list of maps with "text" and "url" keys for all file paths
667 * leading up to the path represented by this view. All URLs allow
668 * auto-diving into one-entry subtrees; see also
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700669 * {@link #getBreadcrumbs(List)}.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800670 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700671 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800672 return getBreadcrumbs(null);
673 }
674
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700675 private static final EnumSet<Type> NON_HTML_TYPES = EnumSet.of(Type.DESCRIBE, Type.ARCHIVE);
676
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800677 /**
678 * @param hasSingleTree list of booleans, one per path entry in this view's
679 * path excluding the leaf. True entries indicate the tree at that path
680 * only has a single entry that is another tree.
681 * @return a list of maps with "text" and "url" keys for all file paths
682 * leading up to the path represented by this view. URLs whose
683 * corresponding entry in {@code hasSingleTree} is true will disable
684 * auto-diving into one-entry subtrees.
685 */
686 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700687 checkArgument(!NON_HTML_TYPES.contains(type),
688 "breadcrumbs for %s view not supported", type);
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800689 checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path),
690 "breadcrumbs for REFS view with path not supported");
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800691 checkArgument(hasSingleTree == null || type == Type.PATH,
692 "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700693 String path = this.path;
694 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
695 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this)));
696 if (repositoryName != null) {
697 breadcrumbs.add(breadcrumb(repositoryName, repositoryIndex().copyFrom(this)));
698 }
699 if (type == Type.DIFF) {
700 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
701 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700702 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700703 } else if (type == Type.LOG) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800704 if (revision != Revision.NULL) {
705 // TODO(dborowitz): Add something in the navigation area (probably not
706 // a breadcrumb) to allow switching between /+log/ and /+/.
707 if (oldRevision == Revision.NULL) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700708 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800709 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700710 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800711 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700712 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800713 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700714 }
715 path = Strings.emptyToNull(path);
716 } else if (revision != Revision.NULL) {
717 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
718 }
719 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800720 if (type != Type.LOG && type != Type.REFS) {
721 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800722 breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700723 }
724 StringBuilder cur = new StringBuilder();
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800725 List<String> parts = PathUtil.SPLITTER.omitEmptyStrings().splitToList(path);
Dave Borowitz44a15842013-01-07 09:39:05 -0800726 checkArgument(hasSingleTree == null
727 || (parts.isEmpty() && hasSingleTree.isEmpty())
728 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800729 "hasSingleTree has wrong number of entries");
730 for (int i = 0; i < parts.size(); i++) {
731 String part = parts.get(i);
732 cur.append(part).append('/');
733 String curPath = cur.toString();
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800734 boolean isLeaf = i == parts.size() - 1;
735 Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800736 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
737 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700738 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800739 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700740 }
741 }
742 return breadcrumbs.build();
743 }
744
745 private static Map<String, String> breadcrumb(String text, Builder url) {
746 return ImmutableMap.of("text", text, "url", url.toUrl());
747 }
748
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800749 private Builder copyWithPath(boolean isLeaf) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700750 Builder copy;
751 switch (type) {
752 case DIFF:
753 copy = diff();
754 break;
755 case LOG:
756 copy = log();
757 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800758 case BLAME:
759 copy = isLeaf ? blame() : path();
760 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700761 default:
762 copy = path();
763 break;
764 }
765 return copy.copyFrom(this);
766 }
767
768 private static boolean isFirstParent(Revision rev1, Revision rev2) {
769 return rev2 == Revision.NULL
770 || rev2.getName().equals(rev1.getName() + "^")
771 || rev2.getName().equals(rev1.getName() + "~1");
772 }
773
Dave Borowitze8a5e362013-01-14 16:07:26 -0800774 @VisibleForTesting
775 static String paramsToString(ListMultimap<String, String> params) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700776 try {
777 StringBuilder sb = new StringBuilder();
778 boolean first = true;
779 for (Map.Entry<String, String> e : params.entries()) {
780 if (!first) {
781 sb.append('&');
782 } else {
783 first = false;
784 }
David Pletcherd7bdaf32014-08-27 14:50:32 -0700785 sb.append(URLEncoder.encode(e.getKey(), UTF_8.name()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700786 if (!"".equals(e.getValue())) {
787 sb.append('=')
David Pletcherd7bdaf32014-08-27 14:50:32 -0700788 .append(URLEncoder.encode(e.getValue(), UTF_8.name()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700789 }
790 }
791 return sb.toString();
792 } catch (UnsupportedEncodingException e) {
793 throw new IllegalStateException(e);
794 }
795 }
796}