blob: 0a76b7dce5920892bea67589763ce38264dbef1c [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;
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;
35
Dave Borowitz80334b22013-01-11 14:19:11 -080036import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070037import org.eclipse.jgit.revwalk.RevObject;
38
39import java.io.UnsupportedEncodingException;
40import java.net.URLEncoder;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070041import java.util.ArrayList;
Dave Borowitz27058932014-12-03 15:44:46 -080042import java.util.Arrays;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070043import java.util.EnumSet;
Dave Borowitz9de65952012-08-13 16:09:45 -070044import java.util.List;
45import java.util.Map;
46
47import javax.servlet.http.HttpServletRequest;
48
49/**
50 * Information about a view in Gitiles.
51 * <p>
52 * Views are uniquely identified by a type, and dispatched to servlet types by
53 * {@link GitilesServlet}. This class contains the list of all types, as
54 * well as some methods containing basic information parsed from the URL.
55 * Construction happens in {@link ViewFilter}.
56 */
57public class GitilesView {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070058 private static final String DEFAULT_ARCHIVE_EXTENSION = ".tar.gz";
59
Dave Borowitz9de65952012-08-13 16:09:45 -070060 /** All the possible view types supported in the application. */
61 public static enum Type {
62 HOST_INDEX,
63 REPOSITORY_INDEX,
Dave Borowitz209d0aa2012-12-28 14:28:53 -080064 REFS,
Dave Borowitz9de65952012-08-13 16:09:45 -070065 REVISION,
66 PATH,
Shawn Pearce353ba2f2015-02-12 10:22:37 -080067 SHOW,
Dave Borowitz9de65952012-08-13 16:09:45 -070068 DIFF,
Dave Borowitzba9c1182013-03-13 14:16:43 -070069 LOG,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070070 DESCRIBE,
Dave Borowitz68c7a9b2014-01-28 12:13:21 -080071 ARCHIVE,
Shawn Pearce374f1842015-02-10 15:36:54 -080072 BLAME,
Shawn Pearce68311c72015-06-09 17:01:34 -070073 DOC,
74 ROOTED_DOC;
Dave Borowitz9de65952012-08-13 16:09:45 -070075 }
76
Dave Borowitz6221d982013-01-10 10:39:20 -080077 /** Exception thrown when building a view that is invalid. */
78 public static class InvalidViewException extends IllegalStateException {
79 private static final long serialVersionUID = 1L;
80
81 public InvalidViewException(String msg) {
82 super(msg);
83 }
84 }
85
Dave Borowitz9de65952012-08-13 16:09:45 -070086 /** Builder for views. */
87 public static class Builder {
Shawn Pearce68311c72015-06-09 17:01:34 -070088 private Type type;
Dave Borowitz9de65952012-08-13 16:09:45 -070089 private final ListMultimap<String, String> params = LinkedListMultimap.create();
90
91 private String hostName;
92 private String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070093 private String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -070094 private String repositoryName;
95 private Revision revision = Revision.NULL;
96 private Revision oldRevision = Revision.NULL;
97 private String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070098 private String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -070099 private String anchor;
100
101 private Builder(Type type) {
102 this.type = type;
103 }
104
105 public Builder copyFrom(GitilesView other) {
Shawn Pearce68311c72015-06-09 17:01:34 -0700106 if (type == Type.DOC && other.type == Type.ROOTED_DOC) {
107 type = Type.ROOTED_DOC;
108 }
109
Dave Borowitz9de65952012-08-13 16:09:45 -0700110 hostName = other.hostName;
111 servletPath = other.servletPath;
112 switch (type) {
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700113 case HOST_INDEX:
114 repositoryPrefix = other.repositoryPrefix;
115 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700116 case LOG:
117 case DIFF:
118 oldRevision = other.oldRevision;
119 // Fallthrough.
120 case PATH:
Shawn Pearce374f1842015-02-10 15:36:54 -0800121 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700122 case ROOTED_DOC:
Dave Borowitz5051e672013-11-11 11:09:40 -0800123 case ARCHIVE:
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800124 case BLAME:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800125 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700126 path = other.path;
127 // Fallthrough.
128 case REVISION:
129 revision = other.revision;
130 // Fallthrough.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700131 case DESCRIBE:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800132 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700133 case REPOSITORY_INDEX:
134 repositoryName = other.repositoryName;
Chad Horohoead23f142012-11-12 09:45:39 -0800135 // Fallthrough.
136 default:
137 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700138 }
Dave Borowitz58a96f22014-05-06 14:29:24 -0700139 if (other.type == type) {
140 // Only copy params for matching type.
141 params.putAll(other.params);
142 if (type == Type.ARCHIVE) {
143 extension = other.extension;
144 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700145 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700146 return this;
147 }
148
149 public Builder copyFrom(HttpServletRequest req) {
150 return copyFrom(ViewFilter.getView(req));
151 }
152
153 public Builder setHostName(String hostName) {
154 this.hostName = checkNotNull(hostName);
155 return this;
156 }
157
158 public String getHostName() {
159 return hostName;
160 }
161
162 public Builder setServletPath(String servletPath) {
163 this.servletPath = checkNotNull(servletPath);
164 return this;
165 }
166
167 public String getServletPath() {
168 return servletPath;
169 }
170
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700171 public Builder setRepositoryPrefix(String prefix) {
172 switch (type) {
173 case HOST_INDEX:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200174 this.repositoryPrefix =
175 prefix != null ? Strings.emptyToNull(maybeTrimLeadingAndTrailingSlash(prefix)) : null;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700176 return this;
177 default:
178 throw new IllegalStateException(
179 String.format("cannot set repository prefix on %s view", type));
180 }
181 }
182
Dave Borowitz9de65952012-08-13 16:09:45 -0700183 public Builder setRepositoryName(String repositoryName) {
184 switch (type) {
185 case HOST_INDEX:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200186 throw new IllegalStateException(
187 String.format("cannot set repository name on %s view", type));
Dave Borowitz9de65952012-08-13 16:09:45 -0700188 default:
189 this.repositoryName = checkNotNull(repositoryName);
190 return this;
191 }
192 }
193
194 public String getRepositoryName() {
195 return repositoryName;
196 }
197
198 public Builder setRevision(Revision revision) {
199 switch (type) {
200 case HOST_INDEX:
201 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800202 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700203 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700204 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
205 default:
206 this.revision = checkNotNull(revision);
207 return this;
208 }
209 }
210
211 public Builder setRevision(String name) {
212 return setRevision(Revision.named(name));
213 }
214
215 public Builder setRevision(RevObject obj) {
216 return setRevision(Revision.peeled(obj.name(), obj));
217 }
218
219 public Builder setRevision(String name, RevObject obj) {
220 return setRevision(Revision.peeled(name, obj));
221 }
222
223 public Revision getRevision() {
224 return revision;
225 }
226
227 public Builder setOldRevision(Revision revision) {
228 switch (type) {
229 case DIFF:
230 case LOG:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700231 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700232 default:
Dave Borowitzc410f962014-09-23 10:49:26 -0700233 revision = firstNonNull(revision, Revision.NULL);
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700234 checkState(revision == Revision.NULL, "cannot set old revision on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700235 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700236 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600237 this.oldRevision = revision;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700238 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700239 }
240
241 public Builder setOldRevision(RevObject obj) {
242 return setOldRevision(Revision.peeled(obj.name(), obj));
243 }
244
245 public Builder setOldRevision(String name, RevObject obj) {
246 return setOldRevision(Revision.peeled(name, obj));
247 }
248
249 public Revision getOldRevision() {
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700250 return oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700251 }
252
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700253 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700254 switch (type) {
255 case PATH:
256 case DIFF:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800257 case SHOW:
Dave Borowitz1488fed2013-06-26 11:11:40 -0600258 checkState(path != null, "cannot set null path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700259 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800260 case BLAME:
Dave Borowitz5051e672013-11-11 11:09:40 -0800261 case ARCHIVE:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700262 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800263 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700264 case LOG:
Shawn Pearce374f1842015-02-10 15:36:54 -0800265 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700266 case ROOTED_DOC:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700267 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700268 default:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700269 checkState(path == null, "cannot set path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700270 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700271 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600272 this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700273 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700274 }
275
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700276 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700277 return path;
278 }
279
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700280 public Builder setExtension(String extension) {
281 switch (type) {
282 default:
283 checkState(extension == null, "cannot set path on %s view", type);
284 // Fallthrough;
285 case ARCHIVE:
286 this.extension = extension;
287 break;
288 }
289 return this;
290 }
291
292 public String getExtension() {
293 return extension;
294 }
295
Dave Borowitz9de65952012-08-13 16:09:45 -0700296 public Builder putParam(String key, String value) {
297 params.put(key, value);
298 return this;
299 }
300
301 public Builder replaceParam(String key, String value) {
302 params.replaceValues(key, ImmutableList.of(value));
303 return this;
304 }
305
306 public Builder putAllParams(Map<String, String[]> params) {
307 for (Map.Entry<String, String[]> e : params.entrySet()) {
Dave Borowitz27058932014-12-03 15:44:46 -0800308 this.params.putAll(e.getKey(), Arrays.asList(e.getValue()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700309 }
310 return this;
311 }
312
313 public ListMultimap<String, String> getParams() {
314 return params;
315 }
316
317 public Builder setAnchor(String anchor) {
318 this.anchor = anchor;
319 return this;
320 }
321
322 public String getAnchor() {
323 return anchor;
324 }
325
326 public GitilesView build() {
327 switch (type) {
328 case HOST_INDEX:
329 checkHostIndex();
330 break;
331 case REPOSITORY_INDEX:
332 checkRepositoryIndex();
333 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800334 case REFS:
335 checkRefs();
336 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700337 case DESCRIBE:
338 checkDescribe();
339 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700340 case REVISION:
341 checkRevision();
342 break;
343 case PATH:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800344 case SHOW:
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700345 case DOC:
Dave Borowitz9de65952012-08-13 16:09:45 -0700346 checkPath();
347 break;
348 case DIFF:
349 checkDiff();
350 break;
351 case LOG:
352 checkLog();
353 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700354 case ARCHIVE:
355 checkArchive();
356 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800357 case BLAME:
358 checkBlame();
359 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700360 case ROOTED_DOC:
361 checkRootedDoc();
Shawn Pearce374f1842015-02-10 15:36:54 -0800362 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700363 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200364 return new GitilesView(
365 type,
366 hostName,
367 servletPath,
368 repositoryPrefix,
369 repositoryName,
370 revision,
371 oldRevision,
372 path,
373 extension,
374 params,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700375 anchor);
Dave Borowitz9de65952012-08-13 16:09:45 -0700376 }
377
378 public String toUrl() {
379 return build().toUrl();
380 }
381
Dave Borowitz6221d982013-01-10 10:39:20 -0800382 private void checkView(boolean expr, String msg, Object... args) {
383 if (!expr) {
384 throw new InvalidViewException(String.format(msg, args));
385 }
386 }
387
Dave Borowitz9de65952012-08-13 16:09:45 -0700388 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800389 checkView(hostName != null, "missing hostName on %s view", type);
390 checkView(servletPath != null, "missing hostName on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700391 }
392
393 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800394 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700395 checkHostIndex();
396 }
397
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800398 private void checkRefs() {
399 checkRepositoryIndex();
400 }
401
Dave Borowitzba9c1182013-03-13 14:16:43 -0700402 private void checkDescribe() {
403 checkRepositoryIndex();
404 }
405
Dave Borowitz9de65952012-08-13 16:09:45 -0700406 private void checkRevision() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800407 checkView(revision != Revision.NULL, "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700408 checkRepositoryIndex();
409 }
410
411 private void checkDiff() {
412 checkPath();
413 }
414
415 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800416 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700417 }
418
419 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800420 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700421 checkRevision();
422 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700423
424 private void checkArchive() {
425 checkRevision();
426 }
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800427
428 private void checkBlame() {
429 checkPath();
430 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800431
Shawn Pearce68311c72015-06-09 17:01:34 -0700432 private void checkRootedDoc() {
433 checkView(hostName != null, "missing hostName on %s view", type);
434 checkView(servletPath != null, "missing hostName on %s view", type);
435 checkView(revision != Revision.NULL, "missing revision on %s view", type);
436 checkView(path != null, "missing path on %s view", type);
437 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700438 }
439
440 public static Builder hostIndex() {
441 return new Builder(Type.HOST_INDEX);
442 }
443
444 public static Builder repositoryIndex() {
445 return new Builder(Type.REPOSITORY_INDEX);
446 }
447
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800448 public static Builder refs() {
449 return new Builder(Type.REFS);
450 }
451
Dave Borowitzba9c1182013-03-13 14:16:43 -0700452 public static Builder describe() {
453 return new Builder(Type.DESCRIBE);
454 }
455
Dave Borowitz9de65952012-08-13 16:09:45 -0700456 public static Builder revision() {
457 return new Builder(Type.REVISION);
458 }
459
460 public static Builder path() {
461 return new Builder(Type.PATH);
462 }
463
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800464 public static Builder show() {
465 return new Builder(Type.SHOW);
466 }
467
Dave Borowitz9de65952012-08-13 16:09:45 -0700468 public static Builder diff() {
469 return new Builder(Type.DIFF);
470 }
471
472 public static Builder log() {
473 return new Builder(Type.LOG);
474 }
475
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700476 public static Builder archive() {
477 return new Builder(Type.ARCHIVE);
478 }
479
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800480 public static Builder blame() {
481 return new Builder(Type.BLAME);
482 }
483
Shawn Pearce374f1842015-02-10 15:36:54 -0800484 public static Builder doc() {
485 return new Builder(Type.DOC);
486 }
487
Shawn Pearce68311c72015-06-09 17:01:34 -0700488 public static Builder rootedDoc() {
489 return new Builder(Type.ROOTED_DOC);
490 }
491
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800492 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700493 if (str.startsWith("/")) {
494 str = str.substring(1);
495 }
496 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
497 }
498
499 private final Type type;
500 private final String hostName;
501 private final String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700502 private final String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700503 private final String repositoryName;
504 private final Revision revision;
505 private final Revision oldRevision;
506 private final String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700507 private final String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700508 private final ListMultimap<String, String> params;
509 private final String anchor;
510
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200511 private GitilesView(
512 Type type,
Dave Borowitz9de65952012-08-13 16:09:45 -0700513 String hostName,
514 String servletPath,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700515 String repositoryPrefix,
Dave Borowitz9de65952012-08-13 16:09:45 -0700516 String repositoryName,
517 Revision revision,
518 Revision oldRevision,
519 String path,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700520 String extension,
Dave Borowitz9de65952012-08-13 16:09:45 -0700521 ListMultimap<String, String> params,
522 String anchor) {
523 this.type = type;
524 this.hostName = hostName;
525 this.servletPath = servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700526 this.repositoryPrefix = repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700527 this.repositoryName = repositoryName;
Dave Borowitzc410f962014-09-23 10:49:26 -0700528 this.revision = firstNonNull(revision, Revision.NULL);
529 this.oldRevision = firstNonNull(oldRevision, Revision.NULL);
Dave Borowitz9de65952012-08-13 16:09:45 -0700530 this.path = path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700531 this.extension = extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700532 this.params = Multimaps.unmodifiableListMultimap(params);
533 this.anchor = anchor;
534 }
535
Dave Borowitze02bd422014-05-01 11:44:39 -0700536 public Builder copyFrom(GitilesView other) {
537 return new Builder(other.type).copyFrom(this);
538 }
539
540 public Builder toBuilder() {
541 return copyFrom(this);
542 }
543
Dave Borowitz9de65952012-08-13 16:09:45 -0700544 public String getHostName() {
545 return hostName;
546 }
547
548 public String getServletPath() {
549 return servletPath;
550 }
551
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700552 public String getRepositoryPrefix() {
553 return repositoryPrefix;
554 }
555
Dave Borowitz9de65952012-08-13 16:09:45 -0700556 public String getRepositoryName() {
557 return repositoryName;
558 }
559
560 public Revision getRevision() {
561 return revision;
562 }
563
564 public Revision getOldRevision() {
565 return oldRevision;
566 }
567
568 public String getRevisionRange() {
569 if (oldRevision == Revision.NULL) {
570 switch (type) {
571 case LOG:
572 case DIFF:
573 // For types that require two revisions, NULL indicates the empty
574 // tree/commit.
575 return revision.getName() + "^!";
576 default:
577 // For everything else NULL indicates it is not a range, just a single
578 // revision.
579 return null;
580 }
581 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
582 return revision.getName() + "^!";
583 } else {
584 return oldRevision.getName() + ".." + revision.getName();
585 }
586 }
587
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700588 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700589 return path;
590 }
591
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700592 public String getExtension() {
593 return extension;
594 }
595
Dave Borowitz9de65952012-08-13 16:09:45 -0700596 public ListMultimap<String, String> getParameters() {
597 return params;
598 }
599
600 public String getAnchor() {
601 return anchor;
602 }
603
604 public Type getType() {
605 return type;
606 }
607
Dave Borowitz5530a162013-06-19 15:14:47 -0700608 @Override
609 public String toString() {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200610 ToStringHelper b =
611 toStringHelper(type.toString())
612 .omitNullValues()
613 .add("host", hostName)
614 .add("servlet", servletPath)
615 .add("prefix", repositoryPrefix)
616 .add("repo", repositoryName)
617 .add("rev", revision)
618 .add("old", oldRevision)
619 .add("path", path)
620 .add("extension", extension);
Dave Borowitz5530a162013-06-19 15:14:47 -0700621 if (!params.isEmpty()) {
622 b.add("params", params);
623 }
624 b.add("anchor", anchor);
625 return b.toString();
626 }
627
Dave Borowitz9de65952012-08-13 16:09:45 -0700628 /** @return an escaped, relative URL representing this view. */
629 public String toUrl() {
630 StringBuilder url = new StringBuilder(servletPath).append('/');
631 ListMultimap<String, String> params = this.params;
632 switch (type) {
633 case HOST_INDEX:
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700634 if (repositoryPrefix != null) {
635 url.append(repositoryPrefix).append('/');
636 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700637 params = LinkedListMultimap.create();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700638 if (repositoryPrefix == null && !this.params.containsKey("format")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700639 params.put("format", FormatType.HTML.toString());
640 }
641 params.putAll(this.params);
642 break;
643 case REPOSITORY_INDEX:
644 url.append(repositoryName).append('/');
645 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800646 case REFS:
647 url.append(repositoryName).append("/+refs");
648 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700649 case DESCRIBE:
650 url.append(repositoryName).append("/+describe");
651 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700652 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800653 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700654 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700655 case ARCHIVE:
Dave Borowitz5051e672013-11-11 11:09:40 -0800656 url.append(repositoryName).append("/+archive/").append(revision.getName());
657 if (path != null) {
658 url.append('/').append(path);
659 }
Dave Borowitzc410f962014-09-23 10:49:26 -0700660 url.append(firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700661 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700662 case PATH:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200663 url.append(repositoryName)
664 .append("/+/")
665 .append(revision.getName())
666 .append('/')
Dave Borowitz9de65952012-08-13 16:09:45 -0700667 .append(path);
668 break;
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800669 case SHOW:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200670 url.append(repositoryName)
671 .append("/+show/")
672 .append(revision.getName())
673 .append('/')
674 .append(path);
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800675 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700676 case DIFF:
677 url.append(repositoryName).append("/+/");
678 if (isFirstParent(revision, oldRevision)) {
679 url.append(revision.getName()).append("^!");
680 } else {
681 url.append(oldRevision.getName()).append("..").append(revision.getName());
682 }
683 url.append('/').append(path);
684 break;
685 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800686 url.append(repositoryName).append("/+log");
687 if (revision != Revision.NULL) {
688 url.append('/');
689 if (oldRevision != Revision.NULL) {
690 url.append(oldRevision.getName()).append("..");
691 }
692 url.append(revision.getName());
693 if (path != null) {
694 url.append('/').append(path);
695 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700696 }
697 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800698 case BLAME:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200699 url.append(repositoryName)
700 .append("/+blame/")
701 .append(revision.getName())
702 .append('/')
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800703 .append(path);
704 break;
Shawn Pearce374f1842015-02-10 15:36:54 -0800705 case DOC:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800706 url.append(repositoryName);
707 if (path != null && path.endsWith(".md")) {
708 url.append("/+/");
709 } else {
710 url.append("/+doc/");
711 }
712 url.append(revision.getName());
Shawn Pearce374f1842015-02-10 15:36:54 -0800713 if (path != null) {
714 url.append('/').append(path);
715 }
716 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700717 case ROOTED_DOC:
718 if (path != null) {
719 url.append(path);
720 }
721 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700722 default:
723 throw new IllegalStateException("Unknown view type: " + type);
724 }
725 String baseUrl = NAME_ESCAPER.apply(url.toString());
726 url = new StringBuilder();
727 if (!params.isEmpty()) {
728 url.append('?').append(paramsToString(params));
729 }
730 if (!Strings.isNullOrEmpty(anchor)) {
731 url.append('#').append(NAME_ESCAPER.apply(anchor));
732 }
Dave Borowitz27058932014-12-03 15:44:46 -0800733 return baseUrl + url;
Dave Borowitz9de65952012-08-13 16:09:45 -0700734 }
735
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800736 /**
737 * @return a list of maps with "text" and "url" keys for all file paths
738 * leading up to the path represented by this view. All URLs allow
739 * auto-diving into one-entry subtrees; see also
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700740 * {@link #getBreadcrumbs(List)}.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800741 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700742 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800743 return getBreadcrumbs(null);
744 }
745
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700746 private static final EnumSet<Type> NON_HTML_TYPES = EnumSet.of(Type.DESCRIBE, Type.ARCHIVE);
747
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800748 /**
749 * @param hasSingleTree list of booleans, one per path entry in this view's
750 * path excluding the leaf. True entries indicate the tree at that path
751 * only has a single entry that is another tree.
752 * @return a list of maps with "text" and "url" keys for all file paths
753 * leading up to the path represented by this view. URLs whose
754 * corresponding entry in {@code hasSingleTree} is true will disable
755 * auto-diving into one-entry subtrees.
756 */
757 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200758 checkArgument(!NON_HTML_TYPES.contains(type), "breadcrumbs for %s view not supported", type);
759 checkArgument(
760 type != Type.REFS || Strings.isNullOrEmpty(path),
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800761 "breadcrumbs for REFS view with path not supported");
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200762 checkArgument(
763 hasSingleTree == null || type == Type.PATH, "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700764 String path = this.path;
765 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700766 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this).setRepositoryPrefix(null)));
767 if (repositoryPrefix != null) {
768 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryPrefix));
769 } else if (repositoryName != null) {
770 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryName));
Dave Borowitz9de65952012-08-13 16:09:45 -0700771 }
772 if (type == Type.DIFF) {
773 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
774 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700775 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700776 } else if (type == Type.LOG) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800777 if (revision != Revision.NULL) {
778 // TODO(dborowitz): Add something in the navigation area (probably not
779 // a breadcrumb) to allow switching between /+log/ and /+/.
780 if (oldRevision == Revision.NULL) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700781 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800782 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700783 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800784 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700785 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800786 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700787 }
788 path = Strings.emptyToNull(path);
789 } else if (revision != Revision.NULL) {
790 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
791 }
792 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800793 if (type != Type.LOG && type != Type.REFS) {
794 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800795 breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700796 }
797 StringBuilder cur = new StringBuilder();
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800798 List<String> parts = PathUtil.SPLITTER.omitEmptyStrings().splitToList(path);
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200799 checkArgument(
800 hasSingleTree == null
801 || (parts.isEmpty() && hasSingleTree.isEmpty())
802 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800803 "hasSingleTree has wrong number of entries");
804 for (int i = 0; i < parts.size(); i++) {
805 String part = parts.get(i);
806 cur.append(part).append('/');
807 String curPath = cur.toString();
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800808 boolean isLeaf = i == parts.size() - 1;
809 Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800810 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
811 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700812 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800813 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700814 }
815 }
816 return breadcrumbs.build();
817 }
818
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700819 private List<Map<String, String>> hostIndexBreadcrumbs(String name) {
820 List<String> parts = Splitter.on('/').splitToList(name);
821 List<Map<String, String>> r = new ArrayList<>(parts.size());
822 for (int i = 0; i < parts.size(); i++) {
823 String prefix = Joiner.on('/').join(parts.subList(0, i + 1));
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200824 r.add(breadcrumb(parts.get(i), hostIndex().copyFrom(this).setRepositoryPrefix(prefix)));
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700825 }
826 return r;
827 }
828
Dave Borowitz9de65952012-08-13 16:09:45 -0700829 private static Map<String, String> breadcrumb(String text, Builder url) {
830 return ImmutableMap.of("text", text, "url", url.toUrl());
831 }
832
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800833 private Builder copyWithPath(boolean isLeaf) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700834 Builder copy;
835 switch (type) {
836 case DIFF:
837 copy = diff();
838 break;
839 case LOG:
840 copy = log();
841 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800842 case BLAME:
843 copy = isLeaf ? blame() : path();
844 break;
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) {
853 return rev2 == Revision.NULL
854 || 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}