blob: d3bf7cfc2fd49ec7eb9af458b815923d7c2e5bdb [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 Borowitz4e8ffd82012-12-26 16:01:06 -080017import static com.google.common.base.Preconditions.checkArgument;
Dave Borowitz9de65952012-08-13 16:09:45 -070018import static com.google.common.base.Preconditions.checkNotNull;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070019import static com.google.common.base.Preconditions.checkState;
Dave Borowitz9de65952012-08-13 16:09:45 -070020import static com.google.gitiles.GitilesUrls.NAME_ESCAPER;
21
Dave Borowitze8a5e362013-01-14 16:07:26 -080022import com.google.common.annotations.VisibleForTesting;
Dave Borowitz9de65952012-08-13 16:09:45 -070023import com.google.common.base.Charsets;
24import com.google.common.base.Objects;
Dave Borowitz5530a162013-06-19 15:14:47 -070025import com.google.common.base.Objects.ToStringHelper;
Dave Borowitz9de65952012-08-13 16:09:45 -070026import com.google.common.base.Strings;
27import com.google.common.collect.ImmutableList;
28import com.google.common.collect.ImmutableMap;
29import com.google.common.collect.LinkedListMultimap;
30import com.google.common.collect.ListMultimap;
31import com.google.common.collect.Multimaps;
32
Dave Borowitz80334b22013-01-11 14:19:11 -080033import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070034import org.eclipse.jgit.revwalk.RevObject;
35
36import java.io.UnsupportedEncodingException;
37import java.net.URLEncoder;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070038import java.util.EnumSet;
Dave Borowitz9de65952012-08-13 16:09:45 -070039import java.util.List;
40import java.util.Map;
41
42import javax.servlet.http.HttpServletRequest;
43
44/**
45 * Information about a view in Gitiles.
46 * <p>
47 * Views are uniquely identified by a type, and dispatched to servlet types by
48 * {@link GitilesServlet}. This class contains the list of all types, as
49 * well as some methods containing basic information parsed from the URL.
50 * Construction happens in {@link ViewFilter}.
51 */
52public class GitilesView {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070053 private static final String DEFAULT_ARCHIVE_EXTENSION = ".tar.gz";
54
Dave Borowitz9de65952012-08-13 16:09:45 -070055 /** All the possible view types supported in the application. */
56 public static enum Type {
57 HOST_INDEX,
58 REPOSITORY_INDEX,
Dave Borowitz209d0aa2012-12-28 14:28:53 -080059 REFS,
Dave Borowitz9de65952012-08-13 16:09:45 -070060 REVISION,
61 PATH,
62 DIFF,
Dave Borowitzba9c1182013-03-13 14:16:43 -070063 LOG,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070064 DESCRIBE,
Dave Borowitz68c7a9b2014-01-28 12:13:21 -080065 ARCHIVE,
66 BLAME;
Dave Borowitz9de65952012-08-13 16:09:45 -070067 }
68
Dave Borowitz6221d982013-01-10 10:39:20 -080069 /** Exception thrown when building a view that is invalid. */
70 public static class InvalidViewException extends IllegalStateException {
71 private static final long serialVersionUID = 1L;
72
73 public InvalidViewException(String msg) {
74 super(msg);
75 }
76 }
77
Dave Borowitz9de65952012-08-13 16:09:45 -070078 /** Builder for views. */
79 public static class Builder {
80 private final Type type;
81 private final ListMultimap<String, String> params = LinkedListMultimap.create();
82
83 private String hostName;
84 private String servletPath;
85 private String repositoryName;
86 private Revision revision = Revision.NULL;
87 private Revision oldRevision = Revision.NULL;
88 private String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070089 private String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -070090 private String anchor;
91
92 private Builder(Type type) {
93 this.type = type;
94 }
95
96 public Builder copyFrom(GitilesView other) {
97 hostName = other.hostName;
98 servletPath = other.servletPath;
99 switch (type) {
100 case LOG:
101 case DIFF:
102 oldRevision = other.oldRevision;
103 // Fallthrough.
104 case PATH:
Dave Borowitz5051e672013-11-11 11:09:40 -0800105 case ARCHIVE:
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800106 case BLAME:
Dave Borowitz9de65952012-08-13 16:09:45 -0700107 path = other.path;
108 // Fallthrough.
109 case REVISION:
110 revision = other.revision;
111 // Fallthrough.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700112 case DESCRIBE:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800113 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700114 case REPOSITORY_INDEX:
115 repositoryName = other.repositoryName;
Chad Horohoead23f142012-11-12 09:45:39 -0800116 // Fallthrough.
117 default:
118 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700119 }
Dave Borowitzc8a15682013-07-02 14:33:08 -0700120 if (type == Type.ARCHIVE && other.type == Type.ARCHIVE) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700121 extension = other.extension;
122 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700123 // Don't copy params.
124 return this;
125 }
126
127 public Builder copyFrom(HttpServletRequest req) {
128 return copyFrom(ViewFilter.getView(req));
129 }
130
131 public Builder setHostName(String hostName) {
132 this.hostName = checkNotNull(hostName);
133 return this;
134 }
135
136 public String getHostName() {
137 return hostName;
138 }
139
140 public Builder setServletPath(String servletPath) {
141 this.servletPath = checkNotNull(servletPath);
142 return this;
143 }
144
145 public String getServletPath() {
146 return servletPath;
147 }
148
149 public Builder setRepositoryName(String repositoryName) {
150 switch (type) {
151 case HOST_INDEX:
152 throw new IllegalStateException(String.format(
153 "cannot set repository name on %s view", type));
154 default:
155 this.repositoryName = checkNotNull(repositoryName);
156 return this;
157 }
158 }
159
160 public String getRepositoryName() {
161 return repositoryName;
162 }
163
164 public Builder setRevision(Revision revision) {
165 switch (type) {
166 case HOST_INDEX:
167 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800168 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700169 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700170 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
171 default:
172 this.revision = checkNotNull(revision);
173 return this;
174 }
175 }
176
177 public Builder setRevision(String name) {
178 return setRevision(Revision.named(name));
179 }
180
181 public Builder setRevision(RevObject obj) {
182 return setRevision(Revision.peeled(obj.name(), obj));
183 }
184
185 public Builder setRevision(String name, RevObject obj) {
186 return setRevision(Revision.peeled(name, obj));
187 }
188
189 public Revision getRevision() {
190 return revision;
191 }
192
193 public Builder setOldRevision(Revision revision) {
194 switch (type) {
195 case DIFF:
196 case LOG:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700197 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700198 default:
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700199 revision = Objects.firstNonNull(revision, Revision.NULL);
200 checkState(revision == Revision.NULL, "cannot set old revision on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700201 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700202 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600203 this.oldRevision = revision;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700204 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700205 }
206
207 public Builder setOldRevision(RevObject obj) {
208 return setOldRevision(Revision.peeled(obj.name(), obj));
209 }
210
211 public Builder setOldRevision(String name, RevObject obj) {
212 return setOldRevision(Revision.peeled(name, obj));
213 }
214
215 public Revision getOldRevision() {
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700216 return oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700217 }
218
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700219 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700220 switch (type) {
221 case PATH:
222 case DIFF:
Dave Borowitz1488fed2013-06-26 11:11:40 -0600223 checkState(path != null, "cannot set null path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700224 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800225 case BLAME:
Dave Borowitz5051e672013-11-11 11:09:40 -0800226 case ARCHIVE:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700227 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800228 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700229 case LOG:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700230 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700231 default:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700232 checkState(path == null, "cannot set path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700233 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700234 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600235 this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700236 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700237 }
238
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700239 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700240 return path;
241 }
242
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700243 public Builder setExtension(String extension) {
244 switch (type) {
245 default:
246 checkState(extension == null, "cannot set path on %s view", type);
247 // Fallthrough;
248 case ARCHIVE:
249 this.extension = extension;
250 break;
251 }
252 return this;
253 }
254
255 public String getExtension() {
256 return extension;
257 }
258
Dave Borowitz9de65952012-08-13 16:09:45 -0700259 public Builder putParam(String key, String value) {
260 params.put(key, value);
261 return this;
262 }
263
264 public Builder replaceParam(String key, String value) {
265 params.replaceValues(key, ImmutableList.of(value));
266 return this;
267 }
268
269 public Builder putAllParams(Map<String, String[]> params) {
270 for (Map.Entry<String, String[]> e : params.entrySet()) {
271 for (String v : e.getValue()) {
272 this.params.put(e.getKey(), v);
273 }
274 }
275 return this;
276 }
277
278 public ListMultimap<String, String> getParams() {
279 return params;
280 }
281
282 public Builder setAnchor(String anchor) {
283 this.anchor = anchor;
284 return this;
285 }
286
287 public String getAnchor() {
288 return anchor;
289 }
290
291 public GitilesView build() {
292 switch (type) {
293 case HOST_INDEX:
294 checkHostIndex();
295 break;
296 case REPOSITORY_INDEX:
297 checkRepositoryIndex();
298 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800299 case REFS:
300 checkRefs();
301 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700302 case DESCRIBE:
303 checkDescribe();
304 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700305 case REVISION:
306 checkRevision();
307 break;
308 case PATH:
309 checkPath();
310 break;
311 case DIFF:
312 checkDiff();
313 break;
314 case LOG:
315 checkLog();
316 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700317 case ARCHIVE:
318 checkArchive();
319 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800320 case BLAME:
321 checkBlame();
322 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700323 }
324 return new GitilesView(type, hostName, servletPath, repositoryName, revision,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700325 oldRevision, path, extension, params, anchor);
Dave Borowitz9de65952012-08-13 16:09:45 -0700326 }
327
328 public String toUrl() {
329 return build().toUrl();
330 }
331
Dave Borowitz6221d982013-01-10 10:39:20 -0800332 private void checkView(boolean expr, String msg, Object... args) {
333 if (!expr) {
334 throw new InvalidViewException(String.format(msg, args));
335 }
336 }
337
Dave Borowitz9de65952012-08-13 16:09:45 -0700338 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800339 checkView(hostName != null, "missing hostName on %s view", type);
340 checkView(servletPath != null, "missing hostName on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700341 }
342
343 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800344 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700345 checkHostIndex();
346 }
347
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800348 private void checkRefs() {
349 checkRepositoryIndex();
350 }
351
Dave Borowitzba9c1182013-03-13 14:16:43 -0700352 private void checkDescribe() {
353 checkRepositoryIndex();
354 }
355
Dave Borowitz9de65952012-08-13 16:09:45 -0700356 private void checkRevision() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800357 checkView(revision != Revision.NULL, "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700358 checkRepositoryIndex();
359 }
360
361 private void checkDiff() {
362 checkPath();
363 }
364
365 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800366 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700367 }
368
369 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800370 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700371 checkRevision();
372 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700373
374 private void checkArchive() {
375 checkRevision();
376 }
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800377
378 private void checkBlame() {
379 checkPath();
380 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700381 }
382
383 public static Builder hostIndex() {
384 return new Builder(Type.HOST_INDEX);
385 }
386
387 public static Builder repositoryIndex() {
388 return new Builder(Type.REPOSITORY_INDEX);
389 }
390
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800391 public static Builder refs() {
392 return new Builder(Type.REFS);
393 }
394
Dave Borowitzba9c1182013-03-13 14:16:43 -0700395 public static Builder describe() {
396 return new Builder(Type.DESCRIBE);
397 }
398
Dave Borowitz9de65952012-08-13 16:09:45 -0700399 public static Builder revision() {
400 return new Builder(Type.REVISION);
401 }
402
403 public static Builder path() {
404 return new Builder(Type.PATH);
405 }
406
407 public static Builder diff() {
408 return new Builder(Type.DIFF);
409 }
410
411 public static Builder log() {
412 return new Builder(Type.LOG);
413 }
414
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700415 public static Builder archive() {
416 return new Builder(Type.ARCHIVE);
417 }
418
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800419 public static Builder blame() {
420 return new Builder(Type.BLAME);
421 }
422
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800423 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700424 if (str.startsWith("/")) {
425 str = str.substring(1);
426 }
427 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
428 }
429
430 private final Type type;
431 private final String hostName;
432 private final String servletPath;
433 private final String repositoryName;
434 private final Revision revision;
435 private final Revision oldRevision;
436 private final String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700437 private final String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700438 private final ListMultimap<String, String> params;
439 private final String anchor;
440
441 private GitilesView(Type type,
442 String hostName,
443 String servletPath,
444 String repositoryName,
445 Revision revision,
446 Revision oldRevision,
447 String path,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700448 String extension,
Dave Borowitz9de65952012-08-13 16:09:45 -0700449 ListMultimap<String, String> params,
450 String anchor) {
451 this.type = type;
452 this.hostName = hostName;
453 this.servletPath = servletPath;
454 this.repositoryName = repositoryName;
455 this.revision = Objects.firstNonNull(revision, Revision.NULL);
456 this.oldRevision = Objects.firstNonNull(oldRevision, Revision.NULL);
457 this.path = path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700458 this.extension = extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700459 this.params = Multimaps.unmodifiableListMultimap(params);
460 this.anchor = anchor;
461 }
462
463 public String getHostName() {
464 return hostName;
465 }
466
467 public String getServletPath() {
468 return servletPath;
469 }
470
471 public String getRepositoryName() {
472 return repositoryName;
473 }
474
475 public Revision getRevision() {
476 return revision;
477 }
478
479 public Revision getOldRevision() {
480 return oldRevision;
481 }
482
483 public String getRevisionRange() {
484 if (oldRevision == Revision.NULL) {
485 switch (type) {
486 case LOG:
487 case DIFF:
488 // For types that require two revisions, NULL indicates the empty
489 // tree/commit.
490 return revision.getName() + "^!";
491 default:
492 // For everything else NULL indicates it is not a range, just a single
493 // revision.
494 return null;
495 }
496 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
497 return revision.getName() + "^!";
498 } else {
499 return oldRevision.getName() + ".." + revision.getName();
500 }
501 }
502
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700503 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700504 return path;
505 }
506
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700507 public String getExtension() {
508 return extension;
509 }
510
Dave Borowitz9de65952012-08-13 16:09:45 -0700511 public ListMultimap<String, String> getParameters() {
512 return params;
513 }
514
515 public String getAnchor() {
516 return anchor;
517 }
518
519 public Type getType() {
520 return type;
521 }
522
Dave Borowitz5530a162013-06-19 15:14:47 -0700523 @Override
524 public String toString() {
525 ToStringHelper b = Objects.toStringHelper(type.toString())
526 .omitNullValues()
527 .add("host", hostName)
528 .add("servlet", servletPath)
529 .add("repo", repositoryName)
530 .add("rev", revision)
531 .add("old", oldRevision)
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700532 .add("path", path)
533 .add("extension", extension);
Dave Borowitz5530a162013-06-19 15:14:47 -0700534 if (!params.isEmpty()) {
535 b.add("params", params);
536 }
537 b.add("anchor", anchor);
538 return b.toString();
539 }
540
Dave Borowitz9de65952012-08-13 16:09:45 -0700541 /** @return an escaped, relative URL representing this view. */
542 public String toUrl() {
543 StringBuilder url = new StringBuilder(servletPath).append('/');
544 ListMultimap<String, String> params = this.params;
545 switch (type) {
546 case HOST_INDEX:
547 params = LinkedListMultimap.create();
548 if (!this.params.containsKey("format")) {
549 params.put("format", FormatType.HTML.toString());
550 }
551 params.putAll(this.params);
552 break;
553 case REPOSITORY_INDEX:
554 url.append(repositoryName).append('/');
555 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800556 case REFS:
557 url.append(repositoryName).append("/+refs");
558 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700559 case DESCRIBE:
560 url.append(repositoryName).append("/+describe");
561 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700562 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800563 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700564 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700565 case ARCHIVE:
Dave Borowitz5051e672013-11-11 11:09:40 -0800566 url.append(repositoryName).append("/+archive/").append(revision.getName());
567 if (path != null) {
568 url.append('/').append(path);
569 }
570 url.append(Objects.firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700571 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700572 case PATH:
573 url.append(repositoryName).append("/+/").append(revision.getName()).append('/')
574 .append(path);
575 break;
576 case DIFF:
577 url.append(repositoryName).append("/+/");
578 if (isFirstParent(revision, oldRevision)) {
579 url.append(revision.getName()).append("^!");
580 } else {
581 url.append(oldRevision.getName()).append("..").append(revision.getName());
582 }
583 url.append('/').append(path);
584 break;
585 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800586 url.append(repositoryName).append("/+log");
587 if (revision != Revision.NULL) {
588 url.append('/');
589 if (oldRevision != Revision.NULL) {
590 url.append(oldRevision.getName()).append("..");
591 }
592 url.append(revision.getName());
593 if (path != null) {
594 url.append('/').append(path);
595 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700596 }
597 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800598 case BLAME:
599 url.append(repositoryName).append("/+blame/").append(revision.getName()).append('/')
600 .append(path);
601 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700602 default:
603 throw new IllegalStateException("Unknown view type: " + type);
604 }
605 String baseUrl = NAME_ESCAPER.apply(url.toString());
606 url = new StringBuilder();
607 if (!params.isEmpty()) {
608 url.append('?').append(paramsToString(params));
609 }
610 if (!Strings.isNullOrEmpty(anchor)) {
611 url.append('#').append(NAME_ESCAPER.apply(anchor));
612 }
613 return baseUrl + url.toString();
614 }
615
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800616 /**
617 * @return a list of maps with "text" and "url" keys for all file paths
618 * leading up to the path represented by this view. All URLs allow
619 * auto-diving into one-entry subtrees; see also
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700620 * {@link #getBreadcrumbs(List)}.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800621 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700622 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800623 return getBreadcrumbs(null);
624 }
625
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700626 private static final EnumSet<Type> NON_HTML_TYPES = EnumSet.of(Type.DESCRIBE, Type.ARCHIVE);
627
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800628 /**
629 * @param hasSingleTree list of booleans, one per path entry in this view's
630 * path excluding the leaf. True entries indicate the tree at that path
631 * only has a single entry that is another tree.
632 * @return a list of maps with "text" and "url" keys for all file paths
633 * leading up to the path represented by this view. URLs whose
634 * corresponding entry in {@code hasSingleTree} is true will disable
635 * auto-diving into one-entry subtrees.
636 */
637 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700638 checkArgument(!NON_HTML_TYPES.contains(type),
639 "breadcrumbs for %s view not supported", type);
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800640 checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path),
641 "breadcrumbs for REFS view with path not supported");
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800642 checkArgument(hasSingleTree == null || type == Type.PATH,
643 "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700644 String path = this.path;
645 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
646 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this)));
647 if (repositoryName != null) {
648 breadcrumbs.add(breadcrumb(repositoryName, repositoryIndex().copyFrom(this)));
649 }
650 if (type == Type.DIFF) {
651 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
652 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700653 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700654 } else if (type == Type.LOG) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800655 if (revision != Revision.NULL) {
656 // TODO(dborowitz): Add something in the navigation area (probably not
657 // a breadcrumb) to allow switching between /+log/ and /+/.
658 if (oldRevision == Revision.NULL) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700659 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800660 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700661 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800662 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700663 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800664 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700665 }
666 path = Strings.emptyToNull(path);
667 } else if (revision != Revision.NULL) {
668 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
669 }
670 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800671 if (type != Type.LOG && type != Type.REFS) {
672 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800673 breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700674 }
675 StringBuilder cur = new StringBuilder();
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800676 List<String> parts = ImmutableList.copyOf(Paths.SPLITTER.omitEmptyStrings().split(path));
Dave Borowitz44a15842013-01-07 09:39:05 -0800677 checkArgument(hasSingleTree == null
678 || (parts.isEmpty() && hasSingleTree.isEmpty())
679 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800680 "hasSingleTree has wrong number of entries");
681 for (int i = 0; i < parts.size(); i++) {
682 String part = parts.get(i);
683 cur.append(part).append('/');
684 String curPath = cur.toString();
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800685 boolean isLeaf = i == parts.size() - 1;
686 Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800687 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
688 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700689 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800690 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700691 }
692 }
693 return breadcrumbs.build();
694 }
695
696 private static Map<String, String> breadcrumb(String text, Builder url) {
697 return ImmutableMap.of("text", text, "url", url.toUrl());
698 }
699
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800700 private Builder copyWithPath(boolean isLeaf) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700701 Builder copy;
702 switch (type) {
703 case DIFF:
704 copy = diff();
705 break;
706 case LOG:
707 copy = log();
708 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800709 case BLAME:
710 copy = isLeaf ? blame() : path();
711 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700712 default:
713 copy = path();
714 break;
715 }
716 return copy.copyFrom(this);
717 }
718
719 private static boolean isFirstParent(Revision rev1, Revision rev2) {
720 return rev2 == Revision.NULL
721 || rev2.getName().equals(rev1.getName() + "^")
722 || rev2.getName().equals(rev1.getName() + "~1");
723 }
724
Dave Borowitze8a5e362013-01-14 16:07:26 -0800725 @VisibleForTesting
726 static String paramsToString(ListMultimap<String, String> params) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700727 try {
728 StringBuilder sb = new StringBuilder();
729 boolean first = true;
730 for (Map.Entry<String, String> e : params.entries()) {
731 if (!first) {
732 sb.append('&');
733 } else {
734 first = false;
735 }
736 sb.append(URLEncoder.encode(e.getKey(), Charsets.UTF_8.name()));
737 if (!"".equals(e.getValue())) {
738 sb.append('=')
739 .append(URLEncoder.encode(e.getValue(), Charsets.UTF_8.name()));
740 }
741 }
742 return sb.toString();
743 } catch (UnsupportedEncodingException e) {
744 throw new IllegalStateException(e);
745 }
746 }
747}