blob: 889c6032bd62bf945c3be161e0cd7a86bd74601b [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;
Dave Borowitz9de65952012-08-13 16:09:45 -070035import java.io.UnsupportedEncodingException;
36import java.net.URLEncoder;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070037import java.util.ArrayList;
Dave Borowitz27058932014-12-03 15:44:46 -080038import java.util.Arrays;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070039import java.util.EnumSet;
Dave Borowitz9de65952012-08-13 16:09:45 -070040import java.util.List;
41import java.util.Map;
Dave Borowitz9de65952012-08-13 16:09:45 -070042import javax.servlet.http.HttpServletRequest;
Dave Borowitz3b744b12016-08-19 16:11:10 -040043import org.eclipse.jgit.lib.Constants;
44import org.eclipse.jgit.revwalk.RevObject;
Dave Borowitz9de65952012-08-13 16:09:45 -070045
46/**
47 * Information about a view in Gitiles.
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. */
David Pursehousee3d3ec82016-06-15 22:10:48 +090058 public enum Type {
Dave Borowitz9de65952012-08-13 16:09:45 -070059 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,
Shawn Pearce68311c72015-06-09 17:01:34 -070070 DOC,
71 ROOTED_DOC;
Dave Borowitz9de65952012-08-13 16:09:45 -070072 }
73
Dave Borowitz6221d982013-01-10 10:39:20 -080074 /** Exception thrown when building a view that is invalid. */
75 public static class InvalidViewException extends IllegalStateException {
76 private static final long serialVersionUID = 1L;
77
78 public InvalidViewException(String msg) {
79 super(msg);
80 }
81 }
82
Dave Borowitz9de65952012-08-13 16:09:45 -070083 /** Builder for views. */
84 public static class Builder {
Shawn Pearce68311c72015-06-09 17:01:34 -070085 private Type type;
Dave Borowitz9de65952012-08-13 16:09:45 -070086 private final ListMultimap<String, String> params = LinkedListMultimap.create();
87
88 private String hostName;
89 private String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -070090 private String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -070091 private String repositoryName;
92 private Revision revision = Revision.NULL;
93 private Revision oldRevision = Revision.NULL;
94 private String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -070095 private String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -070096 private String anchor;
97
98 private Builder(Type type) {
99 this.type = type;
100 }
101
102 public Builder copyFrom(GitilesView other) {
Shawn Pearce68311c72015-06-09 17:01:34 -0700103 if (type == Type.DOC && other.type == Type.ROOTED_DOC) {
104 type = Type.ROOTED_DOC;
105 }
106
Dave Borowitz9de65952012-08-13 16:09:45 -0700107 hostName = other.hostName;
108 servletPath = other.servletPath;
109 switch (type) {
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700110 case HOST_INDEX:
111 repositoryPrefix = other.repositoryPrefix;
112 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700113 case LOG:
114 case DIFF:
115 oldRevision = other.oldRevision;
David Pursehouse0d0cee82016-06-15 21:53:29 +0900116 //$FALL-THROUGH$
Dave Borowitz9de65952012-08-13 16:09:45 -0700117 case PATH:
Shawn Pearce374f1842015-02-10 15:36:54 -0800118 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700119 case ROOTED_DOC:
Dave Borowitz5051e672013-11-11 11:09:40 -0800120 case ARCHIVE:
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800121 case BLAME:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800122 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700123 path = other.path;
David Pursehouse0d0cee82016-06-15 21:53:29 +0900124 //$FALL-THROUGH$
Dave Borowitz9de65952012-08-13 16:09:45 -0700125 case REVISION:
126 revision = other.revision;
David Pursehouse0d0cee82016-06-15 21:53:29 +0900127 //$FALL-THROUGH$
Dave Borowitzba9c1182013-03-13 14:16:43 -0700128 case DESCRIBE:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800129 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700130 case REPOSITORY_INDEX:
131 repositoryName = other.repositoryName;
David Pursehouse0d0cee82016-06-15 21:53:29 +0900132 //$FALL-THROUGH$
Chad Horohoead23f142012-11-12 09:45:39 -0800133 default:
134 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700135 }
Dave Borowitz58a96f22014-05-06 14:29:24 -0700136 if (other.type == type) {
137 // Only copy params for matching type.
138 params.putAll(other.params);
139 if (type == Type.ARCHIVE) {
140 extension = other.extension;
141 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700142 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700143 return this;
144 }
145
146 public Builder copyFrom(HttpServletRequest req) {
147 return copyFrom(ViewFilter.getView(req));
148 }
149
150 public Builder setHostName(String hostName) {
151 this.hostName = checkNotNull(hostName);
152 return this;
153 }
154
155 public String getHostName() {
156 return hostName;
157 }
158
159 public Builder setServletPath(String servletPath) {
160 this.servletPath = checkNotNull(servletPath);
161 return this;
162 }
163
164 public String getServletPath() {
165 return servletPath;
166 }
167
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700168 public Builder setRepositoryPrefix(String prefix) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900169 if (type == Type.HOST_INDEX) {
170 this.repositoryPrefix = prefix != null
171 ? Strings.emptyToNull(maybeTrimLeadingAndTrailingSlash(prefix))
172 : null;
173 return this;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700174 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900175 throw new IllegalStateException(
176 String.format("cannot set repository prefix on %s view", type));
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700177 }
178
Dave Borowitz9de65952012-08-13 16:09:45 -0700179 public Builder setRepositoryName(String repositoryName) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900180 if (type == Type.HOST_INDEX) {
181 throw new IllegalStateException(
182 String.format("cannot set repository name on %s view", type));
Dave Borowitz9de65952012-08-13 16:09:45 -0700183 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900184 this.repositoryName = checkNotNull(repositoryName);
185 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700186 }
187
188 public String getRepositoryName() {
189 return repositoryName;
190 }
191
192 public Builder setRevision(Revision revision) {
193 switch (type) {
194 case HOST_INDEX:
195 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800196 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700197 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700198 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
David Pursehousecb91aaf2016-06-15 22:05:24 +0900199 case ARCHIVE:
200 case BLAME:
201 case DIFF:
202 case DOC:
203 case LOG:
204 case PATH:
205 case REVISION:
206 case ROOTED_DOC:
207 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700208 default:
209 this.revision = checkNotNull(revision);
210 return this;
211 }
212 }
213
214 public Builder setRevision(String name) {
215 return setRevision(Revision.named(name));
216 }
217
218 public Builder setRevision(RevObject obj) {
219 return setRevision(Revision.peeled(obj.name(), obj));
220 }
221
222 public Builder setRevision(String name, RevObject obj) {
223 return setRevision(Revision.peeled(name, obj));
224 }
225
226 public Revision getRevision() {
227 return revision;
228 }
229
230 public Builder setOldRevision(Revision revision) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900231 if (type != Type.DIFF && type != Type.LOG) {
232 revision = firstNonNull(revision, Revision.NULL);
233 checkState(revision == Revision.NULL, "cannot set old revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700234 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600235 this.oldRevision = revision;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700236 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700237 }
238
239 public Builder setOldRevision(RevObject obj) {
240 return setOldRevision(Revision.peeled(obj.name(), obj));
241 }
242
243 public Builder setOldRevision(String name, RevObject obj) {
244 return setOldRevision(Revision.peeled(name, obj));
245 }
246
247 public Revision getOldRevision() {
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700248 return oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700249 }
250
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700251 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700252 switch (type) {
253 case PATH:
254 case DIFF:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800255 case SHOW:
Dave Borowitz1488fed2013-06-26 11:11:40 -0600256 checkState(path != null, "cannot set null path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700257 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800258 case BLAME:
Dave Borowitz5051e672013-11-11 11:09:40 -0800259 case ARCHIVE:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700260 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800261 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700262 case LOG:
Shawn Pearce374f1842015-02-10 15:36:54 -0800263 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700264 case ROOTED_DOC:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700265 break;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900266 case HOST_INDEX:
267 case REPOSITORY_INDEX:
268 case REVISION:
Dave Borowitz9de65952012-08-13 16:09:45 -0700269 default:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700270 checkState(path == null, "cannot set path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700271 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700272 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600273 this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700274 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700275 }
276
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700277 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700278 return path;
279 }
280
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700281 public Builder setExtension(String extension) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900282 if (type != Type.ARCHIVE) {
283 checkState(extension == null, "cannot set extension on %s view", type);
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700284 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900285 this.extension = extension;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700286 return this;
287 }
288
289 public String getExtension() {
290 return extension;
291 }
292
Dave Borowitz9de65952012-08-13 16:09:45 -0700293 public Builder putParam(String key, String value) {
294 params.put(key, value);
295 return this;
296 }
297
298 public Builder replaceParam(String key, String value) {
299 params.replaceValues(key, ImmutableList.of(value));
300 return this;
301 }
302
303 public Builder putAllParams(Map<String, String[]> params) {
304 for (Map.Entry<String, String[]> e : params.entrySet()) {
Dave Borowitz27058932014-12-03 15:44:46 -0800305 this.params.putAll(e.getKey(), Arrays.asList(e.getValue()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700306 }
307 return this;
308 }
309
310 public ListMultimap<String, String> getParams() {
311 return params;
312 }
313
314 public Builder setAnchor(String anchor) {
315 this.anchor = anchor;
316 return this;
317 }
318
319 public String getAnchor() {
320 return anchor;
321 }
322
323 public GitilesView build() {
324 switch (type) {
325 case HOST_INDEX:
326 checkHostIndex();
327 break;
328 case REPOSITORY_INDEX:
329 checkRepositoryIndex();
330 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800331 case REFS:
332 checkRefs();
333 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700334 case DESCRIBE:
335 checkDescribe();
336 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700337 case REVISION:
338 checkRevision();
339 break;
340 case PATH:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800341 case SHOW:
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700342 case DOC:
Dave Borowitz9de65952012-08-13 16:09:45 -0700343 checkPath();
344 break;
345 case DIFF:
346 checkDiff();
347 break;
348 case LOG:
349 checkLog();
350 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700351 case ARCHIVE:
352 checkArchive();
353 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800354 case BLAME:
355 checkBlame();
356 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700357 case ROOTED_DOC:
358 checkRootedDoc();
Shawn Pearce374f1842015-02-10 15:36:54 -0800359 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700360 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200361 return new GitilesView(
362 type,
363 hostName,
364 servletPath,
365 repositoryPrefix,
366 repositoryName,
367 revision,
368 oldRevision,
369 path,
370 extension,
371 params,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700372 anchor);
Dave Borowitz9de65952012-08-13 16:09:45 -0700373 }
374
375 public String toUrl() {
376 return build().toUrl();
377 }
378
Dave Borowitz6221d982013-01-10 10:39:20 -0800379 private void checkView(boolean expr, String msg, Object... args) {
380 if (!expr) {
381 throw new InvalidViewException(String.format(msg, args));
382 }
383 }
384
Dave Borowitz9de65952012-08-13 16:09:45 -0700385 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800386 checkView(hostName != null, "missing hostName on %s view", type);
Shawn Pearce1b8322a2016-05-16 13:14:46 -0600387 checkView(servletPath != null, "missing servletPath on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700388 }
389
390 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800391 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700392 checkHostIndex();
393 }
394
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800395 private void checkRefs() {
396 checkRepositoryIndex();
397 }
398
Dave Borowitzba9c1182013-03-13 14:16:43 -0700399 private void checkDescribe() {
400 checkRepositoryIndex();
401 }
402
Dave Borowitz9de65952012-08-13 16:09:45 -0700403 private void checkRevision() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800404 checkView(revision != Revision.NULL, "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700405 checkRepositoryIndex();
406 }
407
408 private void checkDiff() {
409 checkPath();
410 }
411
412 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800413 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700414 }
415
416 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800417 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700418 checkRevision();
419 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700420
421 private void checkArchive() {
422 checkRevision();
423 }
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800424
425 private void checkBlame() {
426 checkPath();
427 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800428
Shawn Pearce68311c72015-06-09 17:01:34 -0700429 private void checkRootedDoc() {
430 checkView(hostName != null, "missing hostName on %s view", type);
431 checkView(servletPath != null, "missing hostName on %s view", type);
432 checkView(revision != Revision.NULL, "missing revision on %s view", type);
433 checkView(path != null, "missing path on %s view", type);
434 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700435 }
436
437 public static Builder hostIndex() {
438 return new Builder(Type.HOST_INDEX);
439 }
440
441 public static Builder repositoryIndex() {
442 return new Builder(Type.REPOSITORY_INDEX);
443 }
444
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800445 public static Builder refs() {
446 return new Builder(Type.REFS);
447 }
448
Dave Borowitzba9c1182013-03-13 14:16:43 -0700449 public static Builder describe() {
450 return new Builder(Type.DESCRIBE);
451 }
452
Dave Borowitz9de65952012-08-13 16:09:45 -0700453 public static Builder revision() {
454 return new Builder(Type.REVISION);
455 }
456
457 public static Builder path() {
458 return new Builder(Type.PATH);
459 }
460
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800461 public static Builder show() {
462 return new Builder(Type.SHOW);
463 }
464
Dave Borowitz9de65952012-08-13 16:09:45 -0700465 public static Builder diff() {
466 return new Builder(Type.DIFF);
467 }
468
469 public static Builder log() {
470 return new Builder(Type.LOG);
471 }
472
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700473 public static Builder archive() {
474 return new Builder(Type.ARCHIVE);
475 }
476
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800477 public static Builder blame() {
478 return new Builder(Type.BLAME);
479 }
480
Shawn Pearce374f1842015-02-10 15:36:54 -0800481 public static Builder doc() {
482 return new Builder(Type.DOC);
483 }
484
Shawn Pearce68311c72015-06-09 17:01:34 -0700485 public static Builder rootedDoc() {
486 return new Builder(Type.ROOTED_DOC);
487 }
488
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800489 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700490 if (str.startsWith("/")) {
491 str = str.substring(1);
492 }
493 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
494 }
495
496 private final Type type;
497 private final String hostName;
498 private final String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700499 private final String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700500 private final String repositoryName;
501 private final Revision revision;
502 private final Revision oldRevision;
503 private final String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700504 private final String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700505 private final ListMultimap<String, String> params;
506 private final String anchor;
507
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200508 private GitilesView(
509 Type type,
Dave Borowitz9de65952012-08-13 16:09:45 -0700510 String hostName,
511 String servletPath,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700512 String repositoryPrefix,
Dave Borowitz9de65952012-08-13 16:09:45 -0700513 String repositoryName,
514 Revision revision,
515 Revision oldRevision,
516 String path,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700517 String extension,
Dave Borowitz9de65952012-08-13 16:09:45 -0700518 ListMultimap<String, String> params,
519 String anchor) {
520 this.type = type;
521 this.hostName = hostName;
522 this.servletPath = servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700523 this.repositoryPrefix = repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700524 this.repositoryName = repositoryName;
Dave Borowitzc410f962014-09-23 10:49:26 -0700525 this.revision = firstNonNull(revision, Revision.NULL);
526 this.oldRevision = firstNonNull(oldRevision, Revision.NULL);
Dave Borowitz9de65952012-08-13 16:09:45 -0700527 this.path = path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700528 this.extension = extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700529 this.params = Multimaps.unmodifiableListMultimap(params);
530 this.anchor = anchor;
531 }
532
Dave Borowitze02bd422014-05-01 11:44:39 -0700533 public Builder copyFrom(GitilesView other) {
534 return new Builder(other.type).copyFrom(this);
535 }
536
537 public Builder toBuilder() {
538 return copyFrom(this);
539 }
540
Dave Borowitz9de65952012-08-13 16:09:45 -0700541 public String getHostName() {
542 return hostName;
543 }
544
545 public String getServletPath() {
546 return servletPath;
547 }
548
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700549 public String getRepositoryPrefix() {
550 return repositoryPrefix;
551 }
552
Dave Borowitz9de65952012-08-13 16:09:45 -0700553 public String getRepositoryName() {
554 return repositoryName;
555 }
556
557 public Revision getRevision() {
558 return revision;
559 }
560
561 public Revision getOldRevision() {
562 return oldRevision;
563 }
564
565 public String getRevisionRange() {
566 if (oldRevision == Revision.NULL) {
David Pursehousecb91aaf2016-06-15 22:05:24 +0900567 if (type == Type.LOG || type == Type.DIFF) {
568 // For types that require two revisions, NULL indicates the empty
569 // tree/commit.
570 return revision.getName() + "^!";
Dave Borowitz9de65952012-08-13 16:09:45 -0700571 }
David Pursehousecb91aaf2016-06-15 22:05:24 +0900572 // For everything else NULL indicates it is not a range, just a single
573 // revision.
574 return null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700575 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
576 return revision.getName() + "^!";
577 } else {
578 return oldRevision.getName() + ".." + revision.getName();
579 }
580 }
581
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700582 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700583 return path;
584 }
585
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700586 public String getExtension() {
587 return extension;
588 }
589
Dave Borowitz9de65952012-08-13 16:09:45 -0700590 public ListMultimap<String, String> getParameters() {
591 return params;
592 }
593
594 public String getAnchor() {
595 return anchor;
596 }
597
598 public Type getType() {
599 return type;
600 }
601
Dave Borowitz5530a162013-06-19 15:14:47 -0700602 @Override
603 public String toString() {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200604 ToStringHelper b =
605 toStringHelper(type.toString())
606 .omitNullValues()
607 .add("host", hostName)
608 .add("servlet", servletPath)
609 .add("prefix", repositoryPrefix)
610 .add("repo", repositoryName)
611 .add("rev", revision)
612 .add("old", oldRevision)
613 .add("path", path)
614 .add("extension", extension);
Dave Borowitz5530a162013-06-19 15:14:47 -0700615 if (!params.isEmpty()) {
616 b.add("params", params);
617 }
618 b.add("anchor", anchor);
619 return b.toString();
620 }
621
Dave Borowitz9de65952012-08-13 16:09:45 -0700622 /** @return an escaped, relative URL representing this view. */
623 public String toUrl() {
624 StringBuilder url = new StringBuilder(servletPath).append('/');
625 ListMultimap<String, String> params = this.params;
626 switch (type) {
627 case HOST_INDEX:
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700628 if (repositoryPrefix != null) {
629 url.append(repositoryPrefix).append('/');
630 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700631 params = LinkedListMultimap.create();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700632 if (repositoryPrefix == null && !this.params.containsKey("format")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700633 params.put("format", FormatType.HTML.toString());
634 }
635 params.putAll(this.params);
636 break;
637 case REPOSITORY_INDEX:
638 url.append(repositoryName).append('/');
639 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800640 case REFS:
641 url.append(repositoryName).append("/+refs");
642 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700643 case DESCRIBE:
644 url.append(repositoryName).append("/+describe");
645 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700646 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800647 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700648 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700649 case ARCHIVE:
Dave Borowitz5051e672013-11-11 11:09:40 -0800650 url.append(repositoryName).append("/+archive/").append(revision.getName());
651 if (path != null) {
652 url.append('/').append(path);
653 }
Dave Borowitzc410f962014-09-23 10:49:26 -0700654 url.append(firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700655 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700656 case PATH:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200657 url.append(repositoryName)
658 .append("/+/")
659 .append(revision.getName())
660 .append('/')
Dave Borowitz9de65952012-08-13 16:09:45 -0700661 .append(path);
662 break;
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800663 case SHOW:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200664 url.append(repositoryName)
665 .append("/+show/")
666 .append(revision.getName())
667 .append('/')
668 .append(path);
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800669 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700670 case DIFF:
671 url.append(repositoryName).append("/+/");
672 if (isFirstParent(revision, oldRevision)) {
673 url.append(revision.getName()).append("^!");
674 } else {
675 url.append(oldRevision.getName()).append("..").append(revision.getName());
676 }
677 url.append('/').append(path);
678 break;
679 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800680 url.append(repositoryName).append("/+log");
681 if (revision != Revision.NULL) {
682 url.append('/');
683 if (oldRevision != Revision.NULL) {
684 url.append(oldRevision.getName()).append("..");
685 }
686 url.append(revision.getName());
687 if (path != null) {
688 url.append('/').append(path);
689 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700690 }
691 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800692 case BLAME:
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200693 url.append(repositoryName)
694 .append("/+blame/")
695 .append(revision.getName())
696 .append('/')
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800697 .append(path);
698 break;
Shawn Pearce374f1842015-02-10 15:36:54 -0800699 case DOC:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800700 url.append(repositoryName);
701 if (path != null && path.endsWith(".md")) {
702 url.append("/+/");
703 } else {
704 url.append("/+doc/");
705 }
706 url.append(revision.getName());
Shawn Pearce374f1842015-02-10 15:36:54 -0800707 if (path != null) {
708 url.append('/').append(path);
709 }
710 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700711 case ROOTED_DOC:
712 if (path != null) {
713 url.append(path);
714 }
715 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700716 default:
717 throw new IllegalStateException("Unknown view type: " + type);
718 }
719 String baseUrl = NAME_ESCAPER.apply(url.toString());
720 url = new StringBuilder();
721 if (!params.isEmpty()) {
722 url.append('?').append(paramsToString(params));
723 }
724 if (!Strings.isNullOrEmpty(anchor)) {
725 url.append('#').append(NAME_ESCAPER.apply(anchor));
726 }
Dave Borowitz27058932014-12-03 15:44:46 -0800727 return baseUrl + url;
Dave Borowitz9de65952012-08-13 16:09:45 -0700728 }
729
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800730 /**
731 * @return a list of maps with "text" and "url" keys for all file paths
732 * leading up to the path represented by this view. All URLs allow
733 * auto-diving into one-entry subtrees; see also
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700734 * {@link #getBreadcrumbs(List)}.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800735 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700736 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800737 return getBreadcrumbs(null);
738 }
739
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700740 private static final EnumSet<Type> NON_HTML_TYPES = EnumSet.of(Type.DESCRIBE, Type.ARCHIVE);
741
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800742 /**
743 * @param hasSingleTree list of booleans, one per path entry in this view's
744 * path excluding the leaf. True entries indicate the tree at that path
745 * only has a single entry that is another tree.
746 * @return a list of maps with "text" and "url" keys for all file paths
747 * leading up to the path represented by this view. URLs whose
748 * corresponding entry in {@code hasSingleTree} is true will disable
749 * auto-diving into one-entry subtrees.
750 */
751 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200752 checkArgument(!NON_HTML_TYPES.contains(type), "breadcrumbs for %s view not supported", type);
753 checkArgument(
754 type != Type.REFS || Strings.isNullOrEmpty(path),
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800755 "breadcrumbs for REFS view with path not supported");
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200756 checkArgument(
757 hasSingleTree == null || type == Type.PATH, "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700758 String path = this.path;
759 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700760 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this).setRepositoryPrefix(null)));
761 if (repositoryPrefix != null) {
762 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryPrefix));
763 } else if (repositoryName != null) {
764 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryName));
Dave Borowitz9de65952012-08-13 16:09:45 -0700765 }
766 if (type == Type.DIFF) {
767 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
768 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700769 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700770 } else if (type == Type.LOG) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800771 if (revision != Revision.NULL) {
772 // TODO(dborowitz): Add something in the navigation area (probably not
773 // a breadcrumb) to allow switching between /+log/ and /+/.
774 if (oldRevision == Revision.NULL) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700775 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800776 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700777 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800778 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700779 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800780 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700781 }
782 path = Strings.emptyToNull(path);
783 } else if (revision != Revision.NULL) {
784 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
785 }
786 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800787 if (type != Type.LOG && type != Type.REFS) {
788 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800789 breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700790 }
791 StringBuilder cur = new StringBuilder();
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800792 List<String> parts = PathUtil.SPLITTER.omitEmptyStrings().splitToList(path);
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200793 checkArgument(
794 hasSingleTree == null
795 || (parts.isEmpty() && hasSingleTree.isEmpty())
796 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800797 "hasSingleTree has wrong number of entries");
798 for (int i = 0; i < parts.size(); i++) {
799 String part = parts.get(i);
800 cur.append(part).append('/');
801 String curPath = cur.toString();
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800802 boolean isLeaf = i == parts.size() - 1;
803 Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800804 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
805 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700806 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800807 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700808 }
809 }
810 return breadcrumbs.build();
811 }
812
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700813 private List<Map<String, String>> hostIndexBreadcrumbs(String name) {
814 List<String> parts = Splitter.on('/').splitToList(name);
815 List<Map<String, String>> r = new ArrayList<>(parts.size());
816 for (int i = 0; i < parts.size(); i++) {
817 String prefix = Joiner.on('/').join(parts.subList(0, i + 1));
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200818 r.add(breadcrumb(parts.get(i), hostIndex().copyFrom(this).setRepositoryPrefix(prefix)));
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700819 }
820 return r;
821 }
822
Dave Borowitz9de65952012-08-13 16:09:45 -0700823 private static Map<String, String> breadcrumb(String text, Builder url) {
824 return ImmutableMap.of("text", text, "url", url.toUrl());
825 }
826
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800827 private Builder copyWithPath(boolean isLeaf) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700828 Builder copy;
829 switch (type) {
830 case DIFF:
831 copy = diff();
832 break;
833 case LOG:
834 copy = log();
835 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800836 case BLAME:
837 copy = isLeaf ? blame() : path();
838 break;
David Pursehousecb91aaf2016-06-15 22:05:24 +0900839 case ARCHIVE:
840 case DESCRIBE:
841 case DOC:
842 case HOST_INDEX:
843 case PATH:
844 case REFS:
845 case REPOSITORY_INDEX:
846 case REVISION:
847 case ROOTED_DOC:
848 case SHOW:
Dave Borowitz9de65952012-08-13 16:09:45 -0700849 default:
850 copy = path();
851 break;
852 }
853 return copy.copyFrom(this);
854 }
855
856 private static boolean isFirstParent(Revision rev1, Revision rev2) {
857 return rev2 == Revision.NULL
858 || rev2.getName().equals(rev1.getName() + "^")
859 || rev2.getName().equals(rev1.getName() + "~1");
860 }
861
Dave Borowitze8a5e362013-01-14 16:07:26 -0800862 @VisibleForTesting
863 static String paramsToString(ListMultimap<String, String> params) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700864 try {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200865 StringBuilder sb = new StringBuilder();
866 boolean first = true;
867 for (Map.Entry<String, String> e : params.entries()) {
868 if (!first) {
869 sb.append('&');
870 } else {
871 first = false;
872 }
873 sb.append(URLEncoder.encode(e.getKey(), UTF_8.name()));
874 if (!"".equals(e.getValue())) {
875 sb.append('=').append(URLEncoder.encode(e.getValue(), UTF_8.name()));
876 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700877 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200878 return sb.toString();
Dave Borowitz9de65952012-08-13 16:09:45 -0700879 } catch (UnsupportedEncodingException e) {
880 throw new IllegalStateException(e);
881 }
882 }
883}