blob: 885074dc9f0995bda9eca5e337856c584674f3a6 [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
Dave Borowitz4e8ffd82012-12-26 16:01:06 -080017import static com.google.common.base.Preconditions.checkArgument;
Dave Borowitz9de65952012-08-13 16:09:45 -070018import static com.google.common.base.Preconditions.checkNotNull;
Dave Borowitz9de65952012-08-13 16:09:45 -070019import static com.google.gitiles.GitilesUrls.NAME_ESCAPER;
20
Dave Borowitze8a5e362013-01-14 16:07:26 -080021import com.google.common.annotations.VisibleForTesting;
Dave Borowitz9de65952012-08-13 16:09:45 -070022import com.google.common.base.Charsets;
23import com.google.common.base.Objects;
24import com.google.common.base.Strings;
25import com.google.common.collect.ImmutableList;
26import com.google.common.collect.ImmutableMap;
27import com.google.common.collect.LinkedListMultimap;
28import com.google.common.collect.ListMultimap;
29import com.google.common.collect.Multimaps;
30
Dave Borowitz80334b22013-01-11 14:19:11 -080031import org.eclipse.jgit.lib.Constants;
Dave Borowitz9de65952012-08-13 16:09:45 -070032import org.eclipse.jgit.revwalk.RevObject;
33
34import java.io.UnsupportedEncodingException;
35import java.net.URLEncoder;
36import java.util.List;
37import java.util.Map;
38
39import javax.servlet.http.HttpServletRequest;
40
41/**
42 * Information about a view in Gitiles.
43 * <p>
44 * Views are uniquely identified by a type, and dispatched to servlet types by
45 * {@link GitilesServlet}. This class contains the list of all types, as
46 * well as some methods containing basic information parsed from the URL.
47 * Construction happens in {@link ViewFilter}.
48 */
49public class GitilesView {
50 /** All the possible view types supported in the application. */
51 public static enum Type {
52 HOST_INDEX,
53 REPOSITORY_INDEX,
Dave Borowitz209d0aa2012-12-28 14:28:53 -080054 REFS,
Dave Borowitz9de65952012-08-13 16:09:45 -070055 REVISION,
56 PATH,
57 DIFF,
Dave Borowitzba9c1182013-03-13 14:16:43 -070058 LOG,
59 DESCRIBE;
Dave Borowitz9de65952012-08-13 16:09:45 -070060 }
61
Dave Borowitz6221d982013-01-10 10:39:20 -080062 /** Exception thrown when building a view that is invalid. */
63 public static class InvalidViewException extends IllegalStateException {
64 private static final long serialVersionUID = 1L;
65
66 public InvalidViewException(String msg) {
67 super(msg);
68 }
69 }
70
Dave Borowitz9de65952012-08-13 16:09:45 -070071 /** Builder for views. */
72 public static class Builder {
73 private final Type type;
74 private final ListMultimap<String, String> params = LinkedListMultimap.create();
75
76 private String hostName;
77 private String servletPath;
78 private String repositoryName;
79 private Revision revision = Revision.NULL;
80 private Revision oldRevision = Revision.NULL;
81 private String path;
82 private String anchor;
83
84 private Builder(Type type) {
85 this.type = type;
86 }
87
88 public Builder copyFrom(GitilesView other) {
89 hostName = other.hostName;
90 servletPath = other.servletPath;
91 switch (type) {
92 case LOG:
93 case DIFF:
94 oldRevision = other.oldRevision;
95 // Fallthrough.
96 case PATH:
97 path = other.path;
98 // Fallthrough.
99 case REVISION:
100 revision = other.revision;
101 // Fallthrough.
Dave Borowitzba9c1182013-03-13 14:16:43 -0700102 case DESCRIBE:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800103 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700104 case REPOSITORY_INDEX:
105 repositoryName = other.repositoryName;
Chad Horohoead23f142012-11-12 09:45:39 -0800106 // Fallthrough.
107 default:
108 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700109 }
110 // Don't copy params.
111 return this;
112 }
113
114 public Builder copyFrom(HttpServletRequest req) {
115 return copyFrom(ViewFilter.getView(req));
116 }
117
118 public Builder setHostName(String hostName) {
119 this.hostName = checkNotNull(hostName);
120 return this;
121 }
122
123 public String getHostName() {
124 return hostName;
125 }
126
127 public Builder setServletPath(String servletPath) {
128 this.servletPath = checkNotNull(servletPath);
129 return this;
130 }
131
132 public String getServletPath() {
133 return servletPath;
134 }
135
136 public Builder setRepositoryName(String repositoryName) {
137 switch (type) {
138 case HOST_INDEX:
139 throw new IllegalStateException(String.format(
140 "cannot set repository name on %s view", type));
141 default:
142 this.repositoryName = checkNotNull(repositoryName);
143 return this;
144 }
145 }
146
147 public String getRepositoryName() {
148 return repositoryName;
149 }
150
151 public Builder setRevision(Revision revision) {
152 switch (type) {
153 case HOST_INDEX:
154 case REPOSITORY_INDEX:
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800155 case REFS:
Dave Borowitzba9c1182013-03-13 14:16:43 -0700156 case DESCRIBE:
Dave Borowitz9de65952012-08-13 16:09:45 -0700157 throw new IllegalStateException(String.format("cannot set revision on %s view", type));
158 default:
159 this.revision = checkNotNull(revision);
160 return this;
161 }
162 }
163
164 public Builder setRevision(String name) {
165 return setRevision(Revision.named(name));
166 }
167
168 public Builder setRevision(RevObject obj) {
169 return setRevision(Revision.peeled(obj.name(), obj));
170 }
171
172 public Builder setRevision(String name, RevObject obj) {
173 return setRevision(Revision.peeled(name, obj));
174 }
175
176 public Revision getRevision() {
177 return revision;
178 }
179
180 public Builder setOldRevision(Revision revision) {
181 switch (type) {
182 case DIFF:
183 case LOG:
184 this.oldRevision = checkNotNull(revision);
185 return this;
186 default:
187 throw new IllegalStateException(
188 String.format("cannot set old revision on %s view", type));
189 }
190 }
191
192 public Builder setOldRevision(RevObject obj) {
193 return setOldRevision(Revision.peeled(obj.name(), obj));
194 }
195
196 public Builder setOldRevision(String name, RevObject obj) {
197 return setOldRevision(Revision.peeled(name, obj));
198 }
199
200 public Revision getOldRevision() {
201 return revision;
202 }
203
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700204 public Builder setPathPart(String path) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700205 switch (type) {
206 case PATH:
207 case DIFF:
208 this.path = maybeTrimLeadingAndTrailingSlash(checkNotNull(path));
209 return this;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700210 case DESCRIBE:
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800211 case REFS:
Dave Borowitz9de65952012-08-13 16:09:45 -0700212 case LOG:
213 this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
214 return this;
215 default:
216 throw new IllegalStateException(String.format("cannot set path on %s view", type));
217 }
218 }
219
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700220 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700221 return path;
222 }
223
224 public Builder putParam(String key, String value) {
225 params.put(key, value);
226 return this;
227 }
228
229 public Builder replaceParam(String key, String value) {
230 params.replaceValues(key, ImmutableList.of(value));
231 return this;
232 }
233
234 public Builder putAllParams(Map<String, String[]> params) {
235 for (Map.Entry<String, String[]> e : params.entrySet()) {
236 for (String v : e.getValue()) {
237 this.params.put(e.getKey(), v);
238 }
239 }
240 return this;
241 }
242
243 public ListMultimap<String, String> getParams() {
244 return params;
245 }
246
247 public Builder setAnchor(String anchor) {
248 this.anchor = anchor;
249 return this;
250 }
251
252 public String getAnchor() {
253 return anchor;
254 }
255
256 public GitilesView build() {
257 switch (type) {
258 case HOST_INDEX:
259 checkHostIndex();
260 break;
261 case REPOSITORY_INDEX:
262 checkRepositoryIndex();
263 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800264 case REFS:
265 checkRefs();
266 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700267 case DESCRIBE:
268 checkDescribe();
269 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700270 case REVISION:
271 checkRevision();
272 break;
273 case PATH:
274 checkPath();
275 break;
276 case DIFF:
277 checkDiff();
278 break;
279 case LOG:
280 checkLog();
281 break;
282 }
283 return new GitilesView(type, hostName, servletPath, repositoryName, revision,
284 oldRevision, path, params, anchor);
285 }
286
287 public String toUrl() {
288 return build().toUrl();
289 }
290
Dave Borowitz6221d982013-01-10 10:39:20 -0800291 private void checkView(boolean expr, String msg, Object... args) {
292 if (!expr) {
293 throw new InvalidViewException(String.format(msg, args));
294 }
295 }
296
Dave Borowitz9de65952012-08-13 16:09:45 -0700297 private void checkHostIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800298 checkView(hostName != null, "missing hostName on %s view", type);
299 checkView(servletPath != null, "missing hostName on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700300 }
301
302 private void checkRepositoryIndex() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800303 checkView(repositoryName != null, "missing repository name on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700304 checkHostIndex();
305 }
306
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800307 private void checkRefs() {
308 checkRepositoryIndex();
309 }
310
Dave Borowitzba9c1182013-03-13 14:16:43 -0700311 private void checkDescribe() {
312 checkRepositoryIndex();
313 }
314
Dave Borowitz9de65952012-08-13 16:09:45 -0700315 private void checkRevision() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800316 checkView(revision != Revision.NULL, "missing revision on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700317 checkRepositoryIndex();
318 }
319
320 private void checkDiff() {
321 checkPath();
322 }
323
324 private void checkLog() {
Dave Borowitz80334b22013-01-11 14:19:11 -0800325 checkRepositoryIndex();
Dave Borowitz9de65952012-08-13 16:09:45 -0700326 }
327
328 private void checkPath() {
Dave Borowitz6221d982013-01-10 10:39:20 -0800329 checkView(path != null, "missing path on %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700330 checkRevision();
331 }
332 }
333
334 public static Builder hostIndex() {
335 return new Builder(Type.HOST_INDEX);
336 }
337
338 public static Builder repositoryIndex() {
339 return new Builder(Type.REPOSITORY_INDEX);
340 }
341
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800342 public static Builder refs() {
343 return new Builder(Type.REFS);
344 }
345
Dave Borowitzba9c1182013-03-13 14:16:43 -0700346 public static Builder describe() {
347 return new Builder(Type.DESCRIBE);
348 }
349
Dave Borowitz9de65952012-08-13 16:09:45 -0700350 public static Builder revision() {
351 return new Builder(Type.REVISION);
352 }
353
354 public static Builder path() {
355 return new Builder(Type.PATH);
356 }
357
358 public static Builder diff() {
359 return new Builder(Type.DIFF);
360 }
361
362 public static Builder log() {
363 return new Builder(Type.LOG);
364 }
365
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800366 static String maybeTrimLeadingAndTrailingSlash(String str) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700367 if (str.startsWith("/")) {
368 str = str.substring(1);
369 }
370 return !str.isEmpty() && str.endsWith("/") ? str.substring(0, str.length() - 1) : str;
371 }
372
373 private final Type type;
374 private final String hostName;
375 private final String servletPath;
376 private final String repositoryName;
377 private final Revision revision;
378 private final Revision oldRevision;
379 private final String path;
380 private final ListMultimap<String, String> params;
381 private final String anchor;
382
383 private GitilesView(Type type,
384 String hostName,
385 String servletPath,
386 String repositoryName,
387 Revision revision,
388 Revision oldRevision,
389 String path,
390 ListMultimap<String, String> params,
391 String anchor) {
392 this.type = type;
393 this.hostName = hostName;
394 this.servletPath = servletPath;
395 this.repositoryName = repositoryName;
396 this.revision = Objects.firstNonNull(revision, Revision.NULL);
397 this.oldRevision = Objects.firstNonNull(oldRevision, Revision.NULL);
398 this.path = path;
399 this.params = Multimaps.unmodifiableListMultimap(params);
400 this.anchor = anchor;
401 }
402
403 public String getHostName() {
404 return hostName;
405 }
406
407 public String getServletPath() {
408 return servletPath;
409 }
410
411 public String getRepositoryName() {
412 return repositoryName;
413 }
414
415 public Revision getRevision() {
416 return revision;
417 }
418
419 public Revision getOldRevision() {
420 return oldRevision;
421 }
422
423 public String getRevisionRange() {
424 if (oldRevision == Revision.NULL) {
425 switch (type) {
426 case LOG:
427 case DIFF:
428 // For types that require two revisions, NULL indicates the empty
429 // tree/commit.
430 return revision.getName() + "^!";
431 default:
432 // For everything else NULL indicates it is not a range, just a single
433 // revision.
434 return null;
435 }
436 } else if (type == Type.DIFF && isFirstParent(revision, oldRevision)) {
437 return revision.getName() + "^!";
438 } else {
439 return oldRevision.getName() + ".." + revision.getName();
440 }
441 }
442
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700443 public String getPathPart() {
Dave Borowitz9de65952012-08-13 16:09:45 -0700444 return path;
445 }
446
447 public ListMultimap<String, String> getParameters() {
448 return params;
449 }
450
451 public String getAnchor() {
452 return anchor;
453 }
454
455 public Type getType() {
456 return type;
457 }
458
459 /** @return an escaped, relative URL representing this view. */
460 public String toUrl() {
461 StringBuilder url = new StringBuilder(servletPath).append('/');
462 ListMultimap<String, String> params = this.params;
463 switch (type) {
464 case HOST_INDEX:
465 params = LinkedListMultimap.create();
466 if (!this.params.containsKey("format")) {
467 params.put("format", FormatType.HTML.toString());
468 }
469 params.putAll(this.params);
470 break;
471 case REPOSITORY_INDEX:
472 url.append(repositoryName).append('/');
473 break;
Dave Borowitz209d0aa2012-12-28 14:28:53 -0800474 case REFS:
475 url.append(repositoryName).append("/+refs");
476 break;
Dave Borowitzba9c1182013-03-13 14:16:43 -0700477 case DESCRIBE:
478 url.append(repositoryName).append("/+describe");
479 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700480 case REVISION:
Dave Borowitzd3e6dd72012-12-20 15:48:24 -0800481 url.append(repositoryName).append("/+/").append(revision.getName());
Dave Borowitz9de65952012-08-13 16:09:45 -0700482 break;
483 case PATH:
484 url.append(repositoryName).append("/+/").append(revision.getName()).append('/')
485 .append(path);
486 break;
487 case DIFF:
488 url.append(repositoryName).append("/+/");
489 if (isFirstParent(revision, oldRevision)) {
490 url.append(revision.getName()).append("^!");
491 } else {
492 url.append(oldRevision.getName()).append("..").append(revision.getName());
493 }
494 url.append('/').append(path);
495 break;
496 case LOG:
Dave Borowitz80334b22013-01-11 14:19:11 -0800497 url.append(repositoryName).append("/+log");
498 if (revision != Revision.NULL) {
499 url.append('/');
500 if (oldRevision != Revision.NULL) {
501 url.append(oldRevision.getName()).append("..");
502 }
503 url.append(revision.getName());
504 if (path != null) {
505 url.append('/').append(path);
506 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700507 }
508 break;
509 default:
510 throw new IllegalStateException("Unknown view type: " + type);
511 }
512 String baseUrl = NAME_ESCAPER.apply(url.toString());
513 url = new StringBuilder();
514 if (!params.isEmpty()) {
515 url.append('?').append(paramsToString(params));
516 }
517 if (!Strings.isNullOrEmpty(anchor)) {
518 url.append('#').append(NAME_ESCAPER.apply(anchor));
519 }
520 return baseUrl + url.toString();
521 }
522
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800523 /**
524 * @return a list of maps with "text" and "url" keys for all file paths
525 * leading up to the path represented by this view. All URLs allow
526 * auto-diving into one-entry subtrees; see also
527 * {@link #getBreadcrumbs(List<Boolean>)}.
528 */
Dave Borowitz9de65952012-08-13 16:09:45 -0700529 public List<Map<String, String>> getBreadcrumbs() {
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800530 return getBreadcrumbs(null);
531 }
532
533 /**
534 * @param hasSingleTree list of booleans, one per path entry in this view's
535 * path excluding the leaf. True entries indicate the tree at that path
536 * only has a single entry that is another tree.
537 * @return a list of maps with "text" and "url" keys for all file paths
538 * leading up to the path represented by this view. URLs whose
539 * corresponding entry in {@code hasSingleTree} is true will disable
540 * auto-diving into one-entry subtrees.
541 */
542 public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
Dave Borowitzba9c1182013-03-13 14:16:43 -0700543 checkArgument(type != Type.DESCRIBE,
544 "breadcrumbs for DESCRIBE view not supported");
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800545 checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path),
546 "breadcrumbs for REFS view with path not supported");
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800547 checkArgument(hasSingleTree == null || type == Type.PATH,
548 "hasSingleTree must be null for %s view", type);
Dave Borowitz9de65952012-08-13 16:09:45 -0700549 String path = this.path;
550 ImmutableList.Builder<Map<String, String>> breadcrumbs = ImmutableList.builder();
551 breadcrumbs.add(breadcrumb(hostName, hostIndex().copyFrom(this)));
552 if (repositoryName != null) {
553 breadcrumbs.add(breadcrumb(repositoryName, repositoryIndex().copyFrom(this)));
554 }
555 if (type == Type.DIFF) {
556 // TODO(dborowitz): Tweak the breadcrumbs template to allow us to render
557 // separate links in "old..new".
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700558 breadcrumbs.add(breadcrumb(getRevisionRange(), diff().copyFrom(this).setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700559 } else if (type == Type.LOG) {
Dave Borowitz80334b22013-01-11 14:19:11 -0800560 if (revision != Revision.NULL) {
561 // TODO(dborowitz): Add something in the navigation area (probably not
562 // a breadcrumb) to allow switching between /+log/ and /+/.
563 if (oldRevision == Revision.NULL) {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700564 breadcrumbs.add(breadcrumb(revision.getName(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800565 } else {
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700566 breadcrumbs.add(breadcrumb(getRevisionRange(), log().copyFrom(this).setPathPart(null)));
Dave Borowitz80334b22013-01-11 14:19:11 -0800567 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700568 } else {
Dave Borowitz80334b22013-01-11 14:19:11 -0800569 breadcrumbs.add(breadcrumb(Constants.HEAD, log().copyFrom(this)));
Dave Borowitz9de65952012-08-13 16:09:45 -0700570 }
571 path = Strings.emptyToNull(path);
572 } else if (revision != Revision.NULL) {
573 breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
574 }
575 if (path != null) {
Dave Borowitzd0b7e182013-01-11 15:55:09 -0800576 if (type != Type.LOG && type != Type.REFS) {
577 // The "." breadcrumb would be no different for LOG or REFS.
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700578 breadcrumbs.add(breadcrumb(".", copyWithPath().setPathPart("")));
Dave Borowitz9de65952012-08-13 16:09:45 -0700579 }
580 StringBuilder cur = new StringBuilder();
Dave Borowitzbcd753d2013-02-08 11:10:19 -0800581 List<String> parts = ImmutableList.copyOf(Paths.SPLITTER.omitEmptyStrings().split(path));
Dave Borowitz44a15842013-01-07 09:39:05 -0800582 checkArgument(hasSingleTree == null
583 || (parts.isEmpty() && hasSingleTree.isEmpty())
584 || hasSingleTree.size() == parts.size() - 1,
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800585 "hasSingleTree has wrong number of entries");
586 for (int i = 0; i < parts.size(); i++) {
587 String part = parts.get(i);
588 cur.append(part).append('/');
589 String curPath = cur.toString();
Dave Borowitzdd3c3d92013-03-11 16:38:41 -0700590 Builder builder = copyWithPath().setPathPart(curPath);
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800591 if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
592 builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
Dave Borowitz9de65952012-08-13 16:09:45 -0700593 }
Dave Borowitz4e8ffd82012-12-26 16:01:06 -0800594 breadcrumbs.add(breadcrumb(part, builder));
Dave Borowitz9de65952012-08-13 16:09:45 -0700595 }
596 }
597 return breadcrumbs.build();
598 }
599
600 private static Map<String, String> breadcrumb(String text, Builder url) {
601 return ImmutableMap.of("text", text, "url", url.toUrl());
602 }
603
604 private Builder copyWithPath() {
605 Builder copy;
606 switch (type) {
607 case DIFF:
608 copy = diff();
609 break;
610 case LOG:
611 copy = log();
612 break;
613 default:
614 copy = path();
615 break;
616 }
617 return copy.copyFrom(this);
618 }
619
620 private static boolean isFirstParent(Revision rev1, Revision rev2) {
621 return rev2 == Revision.NULL
622 || rev2.getName().equals(rev1.getName() + "^")
623 || rev2.getName().equals(rev1.getName() + "~1");
624 }
625
Dave Borowitze8a5e362013-01-14 16:07:26 -0800626 @VisibleForTesting
627 static String paramsToString(ListMultimap<String, String> params) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700628 try {
629 StringBuilder sb = new StringBuilder();
630 boolean first = true;
631 for (Map.Entry<String, String> e : params.entries()) {
632 if (!first) {
633 sb.append('&');
634 } else {
635 first = false;
636 }
637 sb.append(URLEncoder.encode(e.getKey(), Charsets.UTF_8.name()));
638 if (!"".equals(e.getValue())) {
639 sb.append('=')
640 .append(URLEncoder.encode(e.getValue(), Charsets.UTF_8.name()));
641 }
642 }
643 return sb.toString();
644 } catch (UnsupportedEncodingException e) {
645 throw new IllegalStateException(e);
646 }
647 }
648}