blob: 0e7dc5915dfbcd9480b627c677b3b7273abfd2f6 [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:
174 this.repositoryPrefix = prefix != null
175 ? Strings.emptyToNull(maybeTrimLeadingAndTrailingSlash(prefix))
176 : null;
177 return this;
178 default:
179 throw new IllegalStateException(
180 String.format("cannot set repository prefix on %s view", type));
181 }
182 }
183
Dave Borowitz9de65952012-08-13 16:09:45 -0700184 public Builder setRepositoryName(String repositoryName) {
185 switch (type) {
186 case HOST_INDEX:
187 throw new IllegalStateException(String.format(
188 "cannot set repository name on %s view", type));
189 default:
190 this.repositoryName = checkNotNull(repositoryName);
191 return this;
192 }
193 }
194
195 public String getRepositoryName() {
196 return repositoryName;
197 }
198
199 public Builder setRevision(Revision revision) {
200 switch (type) {
201 case HOST_INDEX:
202 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800203 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700204 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700205 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
206 default:
207 this.revision = checkNotNull(revision);
208 return this;
209 }
210 }
211
212 public Builder setRevision(String name) {
213 return setRevision(Revision.named(name));
214 }
215
216 public Builder setRevision(RevObject obj) {
217 return setRevision(Revision.peeled(obj.name(), obj));
218 }
219
220 public Builder setRevision(String name, RevObject obj) {
221 return setRevision(Revision.peeled(name, obj));
222 }
223
224 public Revision getRevision() {
225 return revision;
226 }
227
228 public Builder setOldRevision(Revision revision) {
229 switch (type) {
230 case DIFF:
231 case LOG:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700232 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700233 default:
Dave Borowitzc410f962014-09-23 10:49:26 -0700234 revision = firstNonNull(revision, Revision.NULL);
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700235 checkState(revision == Revision.NULL, "cannot set old revision on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700236 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700237 }
Dave Borowitz1488fed2013-06-26 11:11:40 -0600238 this.oldRevision = revision;
Dave Borowitzc222cce2013-06-19 10:47:06 -0700239 return this;
Dave Borowitz9de65952012-08-13 16:09:45 -0700240 }
241
242 public Builder setOldRevision(RevObject obj) {
243 return setOldRevision(Revision.peeled(obj.name(), obj));
244 }
245
246 public Builder setOldRevision(String name, RevObject obj) {
247 return setOldRevision(Revision.peeled(name, obj));
248 }
249
250 public Revision getOldRevision() {
Dave Borowitz5d5619d2014-04-18 17:01:45 -0700251 return oldRevision;
Dave Borowitz9de65952012-08-13 16:09:45 -0700252 }
253
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700254 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700255 switch (type) {
256 case PATH:
257 case DIFF:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800258 case SHOW:
Dave Borowitz1488fed2013-06-26 11:11:40 -0600259 checkState(path != null, "cannot set null path on %s view", type);
Dave Borowitzc222cce2013-06-19 10:47:06 -0700260 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800261 case BLAME:
Dave Borowitz5051e672013-11-11 11:09:40 -0800262 case ARCHIVE:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700263 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800264 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700265 case LOG:
Shawn Pearce374f1842015-02-10 15:36:54 -0800266 case DOC:
Shawn Pearce68311c72015-06-09 17:01:34 -0700267 case ROOTED_DOC:
Dave Borowitzc222cce2013-06-19 10:47:06 -0700268 break;
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) {
282 switch (type) {
283 default:
284 checkState(extension == null, "cannot set path on %s view", type);
285 // Fallthrough;
286 case ARCHIVE:
287 this.extension = extension;
288 break;
289 }
290 return this;
291 }
292
293 public String getExtension() {
294 return extension;
295 }
296
Dave Borowitz9de65952012-08-13 16:09:45 -0700297 public Builder putParam(String key, String value) {
298 params.put(key, value);
299 return this;
300 }
301
302 public Builder replaceParam(String key, String value) {
303 params.replaceValues(key, ImmutableList.of(value));
304 return this;
305 }
306
307 public Builder putAllParams(Map<String, String[]> params) {
308 for (Map.Entry<String, String[]> e : params.entrySet()) {
Dave Borowitz27058932014-12-03 15:44:46 -0800309 this.params.putAll(e.getKey(), Arrays.asList(e.getValue()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700310 }
311 return this;
312 }
313
314 public ListMultimap<String, String> getParams() {
315 return params;
316 }
317
318 public Builder setAnchor(String anchor) {
319 this.anchor = anchor;
320 return this;
321 }
322
323 public String getAnchor() {
324 return anchor;
325 }
326
327 public GitilesView build() {
328 switch (type) {
329 case HOST_INDEX:
330 checkHostIndex();
331 break;
332 case REPOSITORY_INDEX:
333 checkRepositoryIndex();
334 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800335 case REFS:
336 checkRefs();
337 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700338 case DESCRIBE:
339 checkDescribe();
340 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700341 case REVISION:
342 checkRevision();
343 break;
344 case PATH:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800345 case SHOW:
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700346 case DOC:
Dave Borowitz9de65952012-08-13 16:09:45 -0700347 checkPath();
348 break;
349 case DIFF:
350 checkDiff();
351 break;
352 case LOG:
353 checkLog();
354 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700355 case ARCHIVE:
356 checkArchive();
357 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800358 case BLAME:
359 checkBlame();
360 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700361 case ROOTED_DOC:
362 checkRootedDoc();
Shawn Pearce374f1842015-02-10 15:36:54 -0800363 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700364 }
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700365 return new GitilesView(type, hostName, servletPath, repositoryPrefix,
366 repositoryName, revision, oldRevision, path, extension, params,
367 anchor);
Dave Borowitz9de65952012-08-13 16:09:45 -0700368 }
369
370 public String toUrl() {
371 return build().toUrl();
372 }
373
Dave Borowitz6221d982013-01-10 10:39:20 -0800374 private void checkView(boolean expr, String msg, Object... args) {
375 if (!expr) {
376 throw new InvalidViewException(String.format(msg, args));
377 }
378 }
379
Dave Borowitz9de65952012-08-13 16:09:45 -0700380 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800381 checkView(hostName != null, "missing hostName on %s view", type);
382 checkView(servletPath != null, "missing hostName on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700383 }
384
385 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800386 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700387 checkHostIndex();
388 }
389
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800390 private void checkRefs() {
391 checkRepositoryIndex();
392 }
393
Dave Borowitzba9c1182013-03-13 14:16:43 -0700394 private void checkDescribe() {
395 checkRepositoryIndex();
396 }
397
Dave Borowitz9de65952012-08-13 16:09:45 -0700398 private void checkRevision() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800399 checkView(revision != Revision.NULL, "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700400 checkRepositoryIndex();
401 }
402
403 private void checkDiff() {
404 checkPath();
405 }
406
407 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800408 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700409 }
410
411 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800412 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700413 checkRevision();
414 }
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700415
416 private void checkArchive() {
417 checkRevision();
418 }
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800419
420 private void checkBlame() {
421 checkPath();
422 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800423
Shawn Pearce68311c72015-06-09 17:01:34 -0700424 private void checkRootedDoc() {
425 checkView(hostName != null, "missing hostName on %s view", type);
426 checkView(servletPath != null, "missing hostName on %s view", type);
427 checkView(revision != Revision.NULL, "missing revision on %s view", type);
428 checkView(path != null, "missing path on %s view", type);
429 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700430 }
431
432 public static Builder hostIndex() {
433 return new Builder(Type.HOST_INDEX);
434 }
435
436 public static Builder repositoryIndex() {
437 return new Builder(Type.REPOSITORY_INDEX);
438 }
439
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800440 public static Builder refs() {
441 return new Builder(Type.REFS);
442 }
443
Dave Borowitzba9c1182013-03-13 14:16:43 -0700444 public static Builder describe() {
445 return new Builder(Type.DESCRIBE);
446 }
447
Dave Borowitz9de65952012-08-13 16:09:45 -0700448 public static Builder revision() {
449 return new Builder(Type.REVISION);
450 }
451
452 public static Builder path() {
453 return new Builder(Type.PATH);
454 }
455
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800456 public static Builder show() {
457 return new Builder(Type.SHOW);
458 }
459
Dave Borowitz9de65952012-08-13 16:09:45 -0700460 public static Builder diff() {
461 return new Builder(Type.DIFF);
462 }
463
464 public static Builder log() {
465 return new Builder(Type.LOG);
466 }
467
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700468 public static Builder archive() {
469 return new Builder(Type.ARCHIVE);
470 }
471
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800472 public static Builder blame() {
473 return new Builder(Type.BLAME);
474 }
475
Shawn Pearce374f1842015-02-10 15:36:54 -0800476 public static Builder doc() {
477 return new Builder(Type.DOC);
478 }
479
Shawn Pearce68311c72015-06-09 17:01:34 -0700480 public static Builder rootedDoc() {
481 return new Builder(Type.ROOTED_DOC);
482 }
483
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800484 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700485 if (str.startsWith("/")) {
486 str = str.substring(1);
487 }
488 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
489 }
490
491 private final Type type;
492 private final String hostName;
493 private final String servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700494 private final String repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700495 private final String repositoryName;
496 private final Revision revision;
497 private final Revision oldRevision;
498 private final String path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700499 private final String extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700500 private final ListMultimap<String, String> params;
501 private final String anchor;
502
503 private GitilesView(Type type,
504 String hostName,
505 String servletPath,
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700506 String repositoryPrefix,
Dave Borowitz9de65952012-08-13 16:09:45 -0700507 String repositoryName,
508 Revision revision,
509 Revision oldRevision,
510 String path,
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700511 String extension,
Dave Borowitz9de65952012-08-13 16:09:45 -0700512 ListMultimap<String, String> params,
513 String anchor) {
514 this.type = type;
515 this.hostName = hostName;
516 this.servletPath = servletPath;
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700517 this.repositoryPrefix = repositoryPrefix;
Dave Borowitz9de65952012-08-13 16:09:45 -0700518 this.repositoryName = repositoryName;
Dave Borowitzc410f962014-09-23 10:49:26 -0700519 this.revision = firstNonNull(revision, Revision.NULL);
520 this.oldRevision = firstNonNull(oldRevision, Revision.NULL);
Dave Borowitz9de65952012-08-13 16:09:45 -0700521 this.path = path;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700522 this.extension = extension;
Dave Borowitz9de65952012-08-13 16:09:45 -0700523 this.params = Multimaps.unmodifiableListMultimap(params);
524 this.anchor = anchor;
525 }
526
Dave Borowitze02bd422014-05-01 11:44:39 -0700527 public Builder copyFrom(GitilesView other) {
528 return new Builder(other.type).copyFrom(this);
529 }
530
531 public Builder toBuilder() {
532 return copyFrom(this);
533 }
534
Dave Borowitz9de65952012-08-13 16:09:45 -0700535 public String getHostName() {
536 return hostName;
537 }
538
539 public String getServletPath() {
540 return servletPath;
541 }
542
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700543 public String getRepositoryPrefix() {
544 return repositoryPrefix;
545 }
546
Dave Borowitz9de65952012-08-13 16:09:45 -0700547 public String getRepositoryName() {
548 return repositoryName;
549 }
550
551 public Revision getRevision() {
552 return revision;
553 }
554
555 public Revision getOldRevision() {
556 return oldRevision;
557 }
558
559 public String getRevisionRange() {
560 if (oldRevision == Revision.NULL) {
561 switch (type) {
562 case LOG:
563 case DIFF:
564 // For types that require two revisions, NULL indicates the empty
565 // tree/commit.
566 return revision.getName() + "^!";
567 default:
568 // For everything else NULL indicates it is not a range, just a single
569 // revision.
570 return null;
571 }
572 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
573 return revision.getName() + "^!";
574 } else {
575 return oldRevision.getName() + ".." + revision.getName();
576 }
577 }
578
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700579 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700580 return path;
581 }
582
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700583 public String getExtension() {
584 return extension;
585 }
586
Dave Borowitz9de65952012-08-13 16:09:45 -0700587 public ListMultimap<String, String> getParameters() {
588 return params;
589 }
590
591 public String getAnchor() {
592 return anchor;
593 }
594
595 public Type getType() {
596 return type;
597 }
598
Dave Borowitz5530a162013-06-19 15:14:47 -0700599 @Override
600 public String toString() {
Dave Borowitzc410f962014-09-23 10:49:26 -0700601 ToStringHelper b = toStringHelper(type.toString())
Dave Borowitz5530a162013-06-19 15:14:47 -0700602 .omitNullValues()
603 .add("host", hostName)
604 .add("servlet", servletPath)
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700605 .add("prefix", repositoryPrefix)
Dave Borowitz5530a162013-06-19 15:14:47 -0700606 .add("repo", repositoryName)
607 .add("rev", revision)
608 .add("old", oldRevision)
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700609 .add("path", path)
610 .add("extension", extension);
Dave Borowitz5530a162013-06-19 15:14:47 -0700611 if (!params.isEmpty()) {
612 b.add("params", params);
613 }
614 b.add("anchor", anchor);
615 return b.toString();
616 }
617
Dave Borowitz9de65952012-08-13 16:09:45 -0700618 /** @return an escaped, relative URL representing this view. */
619 public String toUrl() {
620 StringBuilder url = new StringBuilder(servletPath).append('/');
621 ListMultimap<String, String> params = this.params;
622 switch (type) {
623 case HOST_INDEX:
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700624 if (repositoryPrefix != null) {
625 url.append(repositoryPrefix).append('/');
626 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700627 params = LinkedListMultimap.create();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700628 if (repositoryPrefix == null && !this.params.containsKey("format")) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700629 params.put("format", FormatType.HTML.toString());
630 }
631 params.putAll(this.params);
632 break;
633 case REPOSITORY_INDEX:
634 url.append(repositoryName).append('/');
635 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800636 case REFS:
637 url.append(repositoryName).append("/+refs");
638 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700639 case DESCRIBE:
640 url.append(repositoryName).append("/+describe");
641 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700642 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800643 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700644 break;
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700645 case ARCHIVE:
Dave Borowitz5051e672013-11-11 11:09:40 -0800646 url.append(repositoryName).append("/+archive/").append(revision.getName());
647 if (path != null) {
648 url.append('/').append(path);
649 }
Dave Borowitzc410f962014-09-23 10:49:26 -0700650 url.append(firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700651 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700652 case PATH:
653 url.append(repositoryName).append("/+/").append(revision.getName()).append('/')
654 .append(path);
655 break;
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800656 case SHOW:
657 url.append(repositoryName).append("/+show/").append(revision.getName())
658 .append('/').append(path);
659 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700660 case DIFF:
661 url.append(repositoryName).append("/+/");
662 if (isFirstParent(revision, oldRevision)) {
663 url.append(revision.getName()).append("^!");
664 } else {
665 url.append(oldRevision.getName()).append("..").append(revision.getName());
666 }
667 url.append('/').append(path);
668 break;
669 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800670 url.append(repositoryName).append("/+log");
671 if (revision != Revision.NULL) {
672 url.append('/');
673 if (oldRevision != Revision.NULL) {
674 url.append(oldRevision.getName()).append("..");
675 }
676 url.append(revision.getName());
677 if (path != null) {
678 url.append('/').append(path);
679 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700680 }
681 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800682 case BLAME:
683 url.append(repositoryName).append("/+blame/").append(revision.getName()).append('/')
684 .append(path);
685 break;
Shawn Pearce374f1842015-02-10 15:36:54 -0800686 case DOC:
Shawn Pearce353ba2f2015-02-12 10:22:37 -0800687 url.append(repositoryName);
688 if (path != null && path.endsWith(".md")) {
689 url.append("/+/");
690 } else {
691 url.append("/+doc/");
692 }
693 url.append(revision.getName());
Shawn Pearce374f1842015-02-10 15:36:54 -0800694 if (path != null) {
695 url.append('/').append(path);
696 }
697 break;
Shawn Pearce68311c72015-06-09 17:01:34 -0700698 case ROOTED_DOC:
699 if (path != null) {
700 url.append(path);
701 }
702 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700703 default:
704 throw new IllegalStateException("Unknown view type: " + type);
705 }
706 String baseUrl = NAME_ESCAPER.apply(url.toString());
707 url = new StringBuilder();
708 if (!params.isEmpty()) {
709 url.append('?').append(paramsToString(params));
710 }
711 if (!Strings.isNullOrEmpty(anchor)) {
712 url.append('#').append(NAME_ESCAPER.apply(anchor));
713 }
Dave Borowitz27058932014-12-03 15:44:46 -0800714 return baseUrl + url;
Dave Borowitz9de65952012-08-13 16:09:45 -0700715 }
716
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800717 /**
718 * @return a list of maps with "text" and "url" keys for all file paths
719 * leading up to the path represented by this view. All URLs allow
720 * auto-diving into one-entry subtrees; see also
Dave Borowitz33d4fda2013-10-22 16:40:20 -0700721 * {@link #getBreadcrumbs(List)}.
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800722 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700723 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800724 return getBreadcrumbs(null);
725 }
726
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700727 private static final EnumSet<Type> NON_HTML_TYPES = EnumSet.of(Type.DESCRIBE, Type.ARCHIVE);
728
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800729 /**
730 * @param hasSingleTree list of booleans, one per path entry in this view's
731 * path excluding the leaf. True entries indicate the tree at that path
732 * only has a single entry that is another tree.
733 * @return a list of maps with "text" and "url" keys for all file paths
734 * leading up to the path represented by this view. URLs whose
735 * corresponding entry in {@code hasSingleTree} is true will disable
736 * auto-diving into one-entry subtrees.
737 */
738 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Dave Borowitz6d9bc5a2013-06-19 09:12:52 -0700739 checkArgument(!NON_HTML_TYPES.contains(type),
740 "breadcrumbs for %s view not supported", type);
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800741 checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path),
742 "breadcrumbs for REFS view with path not supported");
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800743 checkArgument(hasSingleTree == null || type == Type.PATH,
744 "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700745 String path = this.path;
746 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700747 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this).setRepositoryPrefix(null)));
748 if (repositoryPrefix != null) {
749 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryPrefix));
750 } else if (repositoryName != null) {
751 breadcrumbs.addAll(hostIndexBreadcrumbs(repositoryName));
Dave Borowitz9de65952012-08-13 16:09:45 -0700752 }
753 if (type == Type.DIFF) {
754 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
755 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700756 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700757 } else if (type == Type.LOG) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800758 if (revision != Revision.NULL) {
759 // TODO(dborowitz): Add something in the navigation area (probably not
760 // a breadcrumb) to allow switching between /+log/ and /+/.
761 if (oldRevision == Revision.NULL) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700762 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800763 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700764 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800765 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700766 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800767 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700768 }
769 path = Strings.emptyToNull(path);
770 } else if (revision != Revision.NULL) {
771 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
772 }
773 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800774 if (type != Type.LOG && type != Type.REFS) {
775 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800776 breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700777 }
778 StringBuilder cur = new StringBuilder();
Dave Borowitzcfc1c532015-02-18 13:41:19 -0800779 List<String> parts = PathUtil.SPLITTER.omitEmptyStrings().splitToList(path);
Dave Borowitz44a15842013-01-07 09:39:05 -0800780 checkArgument(hasSingleTree == null
781 || (parts.isEmpty() && hasSingleTree.isEmpty())
782 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800783 "hasSingleTree has wrong number of entries");
784 for (int i = 0; i < parts.size(); i++) {
785 String part = parts.get(i);
786 cur.append(part).append('/');
787 String curPath = cur.toString();
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800788 boolean isLeaf = i == parts.size() - 1;
789 Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800790 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
791 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700792 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800793 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700794 }
795 }
796 return breadcrumbs.build();
797 }
798
Shawn Pearcec709c4c2015-08-28 15:30:42 -0700799 private List<Map<String, String>> hostIndexBreadcrumbs(String name) {
800 List<String> parts = Splitter.on('/').splitToList(name);
801 List<Map<String, String>> r = new ArrayList<>(parts.size());
802 for (int i = 0; i < parts.size(); i++) {
803 String prefix = Joiner.on('/').join(parts.subList(0, i + 1));
804 r.add(breadcrumb(
805 parts.get(i),
806 hostIndex().copyFrom(this).setRepositoryPrefix(prefix)));
807 }
808 return r;
809 }
810
Dave Borowitz9de65952012-08-13 16:09:45 -0700811 private static Map<String, String> breadcrumb(String text, Builder url) {
812 return ImmutableMap.of("text", text, "url", url.toUrl());
813 }
814
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800815 private Builder copyWithPath(boolean isLeaf) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700816 Builder copy;
817 switch (type) {
818 case DIFF:
819 copy = diff();
820 break;
821 case LOG:
822 copy = log();
823 break;
Dave Borowitz68c7a9b2014-01-28 12:13:21 -0800824 case BLAME:
825 copy = isLeaf ? blame() : path();
826 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700827 default:
828 copy = path();
829 break;
830 }
831 return copy.copyFrom(this);
832 }
833
834 private static boolean isFirstParent(Revision rev1, Revision rev2) {
835 return rev2 == Revision.NULL
836 || rev2.getName().equals(rev1.getName() + "^")
837 || rev2.getName().equals(rev1.getName() + "~1");
838 }
839
Dave Borowitze8a5e362013-01-14 16:07:26 -0800840 @VisibleForTesting
841 static String paramsToString(ListMultimap<String, String> params) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700842 try {
843 StringBuilder sb = new StringBuilder();
844 boolean first = true;
845 for (Map.Entry<String, String> e : params.entries()) {
846 if (!first) {
847 sb.append('&');
848 } else {
849 first = false;
850 }
David Pletcherd7bdaf32014-08-27 14:50:32 -0700851 sb.append(URLEncoder.encode(e.getKey(), UTF_8.name()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700852 if (!"".equals(e.getValue())) {
853 sb.append('=')
David Pletcherd7bdaf32014-08-27 14:50:32 -0700854 .append(URLEncoder.encode(e.getValue(), UTF_8.name()));
Dave Borowitz9de65952012-08-13 16:09:45 -0700855 }
856 }
857 return sb.toString();
858 } catch (UnsupportedEncodingException e) {
859 throw new IllegalStateException(e);
860 }
861 }
862}