1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package org.apache.hc.core5.net;
28
29 import java.net.InetAddress;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.net.UnknownHostException;
33 import java.nio.charset.Charset;
34 import java.nio.charset.StandardCharsets;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.BitSet;
38 import java.util.Collections;
39 import java.util.LinkedList;
40 import java.util.List;
41
42 import org.apache.hc.core5.http.HttpHost;
43 import org.apache.hc.core5.http.NameValuePair;
44 import org.apache.hc.core5.http.message.BasicNameValuePair;
45 import org.apache.hc.core5.http.message.ParserCursor;
46 import org.apache.hc.core5.util.Args;
47 import org.apache.hc.core5.util.TextUtils;
48 import org.apache.hc.core5.util.Tokenizer;
49
50
51
52
53
54
55 public class URIBuilder {
56
57
58
59
60
61
62
63 public static URIBuilder localhost() throws UnknownHostException {
64 return new URIBuilder().setHost(InetAddress.getLocalHost());
65 }
66
67
68
69
70 public static URIBuilder loopbackAddress() {
71 return new URIBuilder().setHost(InetAddress.getLoopbackAddress());
72 }
73
74 private String scheme;
75 private String encodedSchemeSpecificPart;
76 private String encodedAuthority;
77 private String userInfo;
78 private String encodedUserInfo;
79 private String host;
80 private int port;
81 private String encodedPath;
82 private boolean pathRootless;
83 private List<String> pathSegments;
84 private String encodedQuery;
85 private List<NameValuePair> queryParams;
86 private String query;
87 private Charset charset;
88 private String fragment;
89 private String encodedFragment;
90
91
92
93
94 public URIBuilder() {
95 super();
96 this.port = -1;
97 }
98
99
100
101
102
103
104
105 public URIBuilder(final String uriString) throws URISyntaxException {
106 this(new URI(uriString), StandardCharsets.UTF_8);
107 }
108
109
110
111
112
113 public URIBuilder(final URI uri) {
114 this(uri, StandardCharsets.UTF_8);
115 }
116
117
118
119
120
121
122
123 public URIBuilder(final String uriString, final Charset charset) throws URISyntaxException {
124 this(new URI(uriString), charset);
125 }
126
127
128
129
130
131
132 public URIBuilder(final URI uri, final Charset charset) {
133 super();
134 digestURI(uri, charset);
135 }
136
137
138
139
140
141
142
143
144 public URIBuilder setAuthority(final NamedEndpoint authority) {
145 setUserInfo(null);
146 setHost(authority.getHostName());
147 setPort(authority.getPort());
148 return this;
149 }
150
151
152
153
154
155
156
157
158 public URIBuilder setAuthority(final URIAuthority authority) {
159 setUserInfo(authority.getUserInfo());
160 setHost(authority.getHostName());
161 setPort(authority.getPort());
162 return this;
163 }
164
165
166
167
168
169
170
171 public URIBuilder setCharset(final Charset charset) {
172 this.charset = charset;
173 return this;
174 }
175
176
177
178
179
180
181
182 public URIAuthority getAuthority() {
183 return new URIAuthority(getUserInfo(), getHost(), getPort());
184 }
185
186
187
188
189
190
191 public Charset getCharset() {
192 return charset;
193 }
194
195 private static final char QUERY_PARAM_SEPARATOR = '&';
196 private static final char PARAM_VALUE_SEPARATOR = '=';
197 private static final char PATH_SEPARATOR = '/';
198
199 private static final BitSet QUERY_PARAM_SEPARATORS = new BitSet(256);
200 private static final BitSet QUERY_VALUE_SEPARATORS = new BitSet(256);
201 private static final BitSet PATH_SEPARATORS = new BitSet(256);
202
203 static {
204 QUERY_PARAM_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
205 QUERY_PARAM_SEPARATORS.set(PARAM_VALUE_SEPARATOR);
206 QUERY_VALUE_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
207 PATH_SEPARATORS.set(PATH_SEPARATOR);
208 }
209
210 static List<NameValuePair> parseQuery(final CharSequence s, final Charset charset, final boolean plusAsBlank) {
211 if (s == null) {
212 return null;
213 }
214 final Tokenizer tokenParser = Tokenizer.INSTANCE;
215 final ParserCursor cursor = new ParserCursor(0, s.length());
216 final List<NameValuePair> list = new ArrayList<>();
217 while (!cursor.atEnd()) {
218 final String name = tokenParser.parseToken(s, cursor, QUERY_PARAM_SEPARATORS);
219 String value = null;
220 if (!cursor.atEnd()) {
221 final int delim = s.charAt(cursor.getPos());
222 cursor.updatePos(cursor.getPos() + 1);
223 if (delim == PARAM_VALUE_SEPARATOR) {
224 value = tokenParser.parseToken(s, cursor, QUERY_VALUE_SEPARATORS);
225 if (!cursor.atEnd()) {
226 cursor.updatePos(cursor.getPos() + 1);
227 }
228 }
229 }
230 if (!name.isEmpty()) {
231 list.add(new BasicNameValuePair(
232 PercentCodec.decode(name, charset, plusAsBlank),
233 PercentCodec.decode(value, charset, plusAsBlank)));
234 }
235 }
236 return list;
237 }
238
239 static List<String> splitPath(final CharSequence s) {
240 if (s == null) {
241 return null;
242 }
243 final ParserCursor cursor = new ParserCursor(0, s.length());
244
245 if (cursor.atEnd()) {
246 return new ArrayList<>(0);
247 }
248 if (PATH_SEPARATORS.get(s.charAt(cursor.getPos()))) {
249 cursor.updatePos(cursor.getPos() + 1);
250 }
251 final List<String> list = new ArrayList<>();
252 final StringBuilder buf = new StringBuilder();
253 for (;;) {
254 if (cursor.atEnd()) {
255 list.add(buf.toString());
256 break;
257 }
258 final char current = s.charAt(cursor.getPos());
259 if (PATH_SEPARATORS.get(current)) {
260 list.add(buf.toString());
261 buf.setLength(0);
262 } else {
263 buf.append(current);
264 }
265 cursor.updatePos(cursor.getPos() + 1);
266 }
267 return list;
268 }
269
270 static List<String> parsePath(final CharSequence s, final Charset charset) {
271 if (s == null) {
272 return null;
273 }
274 final List<String> segments = splitPath(s);
275 final List<String> list = new ArrayList<>(segments.size());
276 for (final String segment: segments) {
277 list.add(PercentCodec.decode(segment, charset));
278 }
279 return list;
280 }
281
282 static void formatPath(final StringBuilder buf, final Iterable<String> segments, final boolean rootless, final Charset charset) {
283 int i = 0;
284 for (final String segment : segments) {
285 if (i > 0 || !rootless) {
286 buf.append(PATH_SEPARATOR);
287 }
288 PercentCodec.encode(buf, segment, charset);
289 i++;
290 }
291 }
292
293 static void formatQuery(final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset,
294 final boolean blankAsPlus) {
295 int i = 0;
296 for (final NameValuePair parameter : params) {
297 if (i > 0) {
298 buf.append(QUERY_PARAM_SEPARATOR);
299 }
300 PercentCodec.encode(buf, parameter.getName(), charset, blankAsPlus);
301 if (parameter.getValue() != null) {
302 buf.append(PARAM_VALUE_SEPARATOR);
303 PercentCodec.encode(buf, parameter.getValue(), charset, blankAsPlus);
304 }
305 i++;
306 }
307 }
308
309
310
311
312 public URI build() throws URISyntaxException {
313 return new URI(buildString());
314 }
315
316 private String buildString() {
317 final StringBuilder sb = new StringBuilder();
318 if (this.scheme != null) {
319 sb.append(this.scheme).append(':');
320 }
321 if (this.encodedSchemeSpecificPart != null) {
322 sb.append(this.encodedSchemeSpecificPart);
323 } else {
324 final boolean authoritySpecified;
325 if (this.encodedAuthority != null) {
326 sb.append("//").append(this.encodedAuthority);
327 authoritySpecified = true;
328 } else if (this.host != null) {
329 sb.append("//");
330 if (this.encodedUserInfo != null) {
331 sb.append(this.encodedUserInfo).append("@");
332 } else if (this.userInfo != null) {
333 final int idx = this.userInfo.indexOf(':');
334 if (idx != -1) {
335 PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset);
336 sb.append(':');
337 PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset);
338 } else {
339 PercentCodec.encode(sb, this.userInfo, this.charset);
340 }
341 sb.append("@");
342 }
343 if (InetAddressUtils.isIPv6Address(this.host)) {
344 sb.append("[").append(this.host).append("]");
345 } else {
346 sb.append(PercentCodec.encode(this.host, this.charset));
347 }
348 if (this.port >= 0) {
349 sb.append(":").append(this.port);
350 }
351 authoritySpecified = true;
352 } else {
353 authoritySpecified = false;
354 }
355 if (this.encodedPath != null) {
356 if (authoritySpecified && !TextUtils.isEmpty(this.encodedPath) && !this.encodedPath.startsWith("/")) {
357 sb.append('/');
358 }
359 sb.append(this.encodedPath);
360 } else if (this.pathSegments != null) {
361 formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset);
362 }
363 if (this.encodedQuery != null) {
364 sb.append("?").append(this.encodedQuery);
365 } else if (this.queryParams != null && !this.queryParams.isEmpty()) {
366 sb.append("?");
367 formatQuery(sb, this.queryParams, this.charset, false);
368 } else if (this.query != null) {
369 sb.append("?");
370 PercentCodec.encode(sb, this.query, this.charset, PercentCodec.URIC, false);
371 }
372 }
373 if (this.encodedFragment != null) {
374 sb.append("#").append(this.encodedFragment);
375 } else if (this.fragment != null) {
376 sb.append("#");
377 PercentCodec.encode(sb, this.fragment, this.charset);
378 }
379 return sb.toString();
380 }
381
382 private void digestURI(final URI uri, final Charset charset) {
383 this.scheme = uri.getScheme();
384 this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
385 this.encodedAuthority = uri.getRawAuthority();
386 final String uriHost = uri.getHost();
387
388
389 this.host = uriHost != null && InetAddressUtils.isIPv6URLBracketedAddress(uriHost)
390 ? uriHost.substring(1, uriHost.length() - 1)
391 : uriHost;
392 this.port = uri.getPort();
393 this.encodedUserInfo = uri.getRawUserInfo();
394 this.userInfo = uri.getUserInfo();
395 if (this.encodedAuthority != null && this.host == null) {
396 try {
397 final URIAuthority uriAuthority = URIAuthority.parse(this.encodedAuthority);
398 this.encodedUserInfo = uriAuthority.getUserInfo();
399 this.userInfo = PercentCodec.decode(uriAuthority.getUserInfo(), charset);
400 this.host = PercentCodec.decode(uriAuthority.getHostName(), charset);
401 this.port = uriAuthority.getPort();
402 } catch (final URISyntaxException ignore) {
403
404 }
405 }
406 this.encodedPath = uri.getRawPath();
407 this.pathSegments = parsePath(uri.getRawPath(), charset);
408 this.pathRootless = uri.getRawPath() == null || !uri.getRawPath().startsWith("/");
409 this.encodedQuery = uri.getRawQuery();
410 this.queryParams = parseQuery(uri.getRawQuery(), charset, false);
411 this.encodedFragment = uri.getRawFragment();
412 this.fragment = uri.getFragment();
413 this.charset = charset;
414 }
415
416
417
418
419
420
421 public URIBuilder setScheme(final String scheme) {
422 this.scheme = !TextUtils.isBlank(scheme) ? scheme : null;
423 return this;
424 }
425
426
427
428
429
430
431
432
433 public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart) {
434 this.encodedSchemeSpecificPart = schemeSpecificPart;
435 return this;
436 }
437
438
439
440
441
442
443
444
445
446 public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final NameValuePair... nvps) {
447 return setSchemeSpecificPart(schemeSpecificPart, nvps != null ? Arrays.asList(nvps) : null);
448 }
449
450
451
452
453
454
455
456
457
458 public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final List <NameValuePair> nvps) {
459 this.encodedSchemeSpecificPart = null;
460 if (!TextUtils.isBlank(schemeSpecificPart)) {
461 final StringBuilder sb = new StringBuilder(schemeSpecificPart);
462 if (nvps != null && !nvps.isEmpty()) {
463 sb.append("?");
464 formatQuery(sb, nvps, this.charset, false);
465 }
466 this.encodedSchemeSpecificPart = sb.toString();
467 }
468 return this;
469 }
470
471
472
473
474
475
476
477 public URIBuilder setUserInfo(final String userInfo) {
478 this.userInfo = !TextUtils.isBlank(userInfo) ? userInfo : null;
479 this.encodedSchemeSpecificPart = null;
480 this.encodedAuthority = null;
481 this.encodedUserInfo = null;
482 return this;
483 }
484
485
486
487
488
489
490
491
492
493
494 @Deprecated
495 public URIBuilder setUserInfo(final String username, final String password) {
496 return setUserInfo(username + ':' + password);
497 }
498
499
500
501
502
503
504 public URIBuilder setHost(final InetAddress host) {
505 this.host = host != null ? host.getHostAddress() : null;
506 this.encodedSchemeSpecificPart = null;
507 this.encodedAuthority = null;
508 return this;
509 }
510
511
512
513
514
515
516
517
518 public URIBuilder setHost(final String host) {
519 this.host = host;
520 this.encodedSchemeSpecificPart = null;
521 this.encodedAuthority = null;
522 return this;
523 }
524
525
526
527
528
529
530
531 public URIBuilder setHttpHost(final HttpHost httpHost) {
532 setScheme(httpHost.getSchemeName());
533 setHost(httpHost.getHostName());
534 setPort(httpHost.getPort());
535 return this;
536 }
537
538
539
540
541
542
543 public URIBuilder setPort(final int port) {
544 this.port = port < 0 ? -1 : port;
545 this.encodedSchemeSpecificPart = null;
546 this.encodedAuthority = null;
547 return this;
548 }
549
550
551
552
553
554
555 public URIBuilder setPath(final String path) {
556 setPathSegments(path != null ? splitPath(path) : null);
557 this.pathRootless = path != null && !path.startsWith("/");
558 return this;
559 }
560
561
562
563
564
565
566 public URIBuilder appendPath(final String path) {
567 if (path != null) {
568 appendPathSegments(splitPath(path));
569 }
570 return this;
571 }
572
573
574
575
576
577
578 public URIBuilder setPathSegments(final String... pathSegments) {
579 return setPathSegments(Arrays.asList(pathSegments));
580 }
581
582
583
584
585
586
587 public URIBuilder appendPathSegments(final String... pathSegments) {
588 return appendPathSegments(Arrays.asList(pathSegments));
589 }
590
591
592
593
594
595
596
597
598
599 public URIBuilder setPathSegmentsRootless(final String... pathSegments) {
600 return setPathSegmentsRootless(Arrays.asList(pathSegments));
601 }
602
603
604
605
606
607
608 public URIBuilder setPathSegments(final List<String> pathSegments) {
609 this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
610 this.encodedSchemeSpecificPart = null;
611 this.encodedPath = null;
612 this.pathRootless = false;
613 return this;
614 }
615
616
617
618
619
620
621 public URIBuilder appendPathSegments(final List<String> pathSegments) {
622 if (pathSegments != null && !pathSegments.isEmpty()) {
623 if (this.pathSegments == null) {
624 this.pathSegments = new ArrayList<>();
625 }
626 this.pathSegments.addAll(pathSegments);
627 this.encodedSchemeSpecificPart = null;
628 this.encodedPath = null;
629 }
630 return this;
631 }
632
633
634
635
636
637
638
639
640
641 public URIBuilder setPathSegmentsRootless(final List<String> pathSegments) {
642 this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
643 this.encodedSchemeSpecificPart = null;
644 this.encodedPath = null;
645 this.pathRootless = true;
646 return this;
647 }
648
649
650
651
652
653
654 public URIBuilder removeQuery() {
655 this.queryParams = null;
656 this.query = null;
657 this.encodedQuery = null;
658 this.encodedSchemeSpecificPart = null;
659 return this;
660 }
661
662
663
664
665
666
667
668
669
670
671
672 public URIBuilder setParameters(final List <NameValuePair> nameValuePairs) {
673 if (this.queryParams == null) {
674 this.queryParams = new ArrayList<>();
675 } else {
676 this.queryParams.clear();
677 }
678 if (nameValuePairs != null) {
679 this.queryParams.addAll(nameValuePairs);
680 }
681 this.encodedQuery = null;
682 this.encodedSchemeSpecificPart = null;
683 this.query = null;
684 return this;
685 }
686
687
688
689
690
691
692
693
694
695
696
697 public URIBuilder addParameters(final List<NameValuePair> nameValuePairs) {
698 if (this.queryParams == null) {
699 this.queryParams = new ArrayList<>();
700 }
701 if (nameValuePairs != null) {
702 this.queryParams.addAll(nameValuePairs);
703 }
704 this.encodedQuery = null;
705 this.encodedSchemeSpecificPart = null;
706 this.query = null;
707 return this;
708 }
709
710
711
712
713
714
715
716
717
718
719
720 public URIBuilder setParameters(final NameValuePair... nameValuePairs) {
721 if (this.queryParams == null) {
722 this.queryParams = new ArrayList<>();
723 } else {
724 this.queryParams.clear();
725 }
726 if (nameValuePairs != null) {
727 Collections.addAll(this.queryParams, nameValuePairs);
728 }
729 this.encodedQuery = null;
730 this.encodedSchemeSpecificPart = null;
731 this.query = null;
732 return this;
733 }
734
735
736
737
738
739
740
741
742
743
744
745 public URIBuilder addParameter(final String param, final String value) {
746 return addParameter(new BasicNameValuePair(param, value));
747 }
748
749
750
751
752
753
754
755
756
757
758
759
760 public URIBuilder addParameter(final NameValuePair nameValuePair) {
761 if (this.queryParams == null) {
762 this.queryParams = new ArrayList<>();
763 }
764 if (nameValuePair != null) {
765 this.queryParams.add(nameValuePair);
766 }
767 this.encodedQuery = null;
768 this.encodedSchemeSpecificPart = null;
769 this.query = null;
770 return this;
771 }
772
773
774
775
776
777
778
779
780
781
782
783
784 public URIBuilder removeParameter(final String param) {
785 Args.notNull(param, "param");
786 if (this.queryParams != null && !this.queryParams.isEmpty()) {
787 this.queryParams.removeIf(nvp -> nvp.getName().equals(param));
788 }
789 this.encodedQuery = null;
790 this.encodedSchemeSpecificPart = null;
791 this.query = null;
792 return this;
793 }
794
795
796
797
798
799
800
801
802
803
804
805 public URIBuilder setParameter(final String param, final String value) {
806 if (this.queryParams == null) {
807 this.queryParams = new ArrayList<>();
808 }
809 if (!this.queryParams.isEmpty()) {
810 this.queryParams.removeIf(nvp -> nvp.getName().equals(param));
811 }
812 this.queryParams.add(new BasicNameValuePair(param, value));
813 this.encodedQuery = null;
814 this.encodedSchemeSpecificPart = null;
815 this.query = null;
816 return this;
817 }
818
819
820
821
822
823
824 public URIBuilder clearParameters() {
825 this.queryParams = null;
826 this.encodedQuery = null;
827 this.encodedSchemeSpecificPart = null;
828 return this;
829 }
830
831
832
833
834
835
836
837
838
839
840
841 public URIBuilder setCustomQuery(final String query) {
842 this.query = !TextUtils.isBlank(query) ? query : null;
843 this.encodedQuery = null;
844 this.encodedSchemeSpecificPart = null;
845 this.queryParams = null;
846 return this;
847 }
848
849
850
851
852
853
854
855 public URIBuilder setFragment(final String fragment) {
856 this.fragment = !TextUtils.isBlank(fragment) ? fragment : null;
857 this.encodedFragment = null;
858 return this;
859 }
860
861
862
863
864
865
866 public boolean isAbsolute() {
867 return this.scheme != null;
868 }
869
870
871
872
873
874
875 public boolean isOpaque() {
876 return this.pathSegments == null && this.encodedPath == null;
877 }
878
879
880
881
882
883
884 public String getScheme() {
885 return this.scheme;
886 }
887
888
889
890
891
892
893
894 public String getSchemeSpecificPart() {
895 return this.encodedSchemeSpecificPart;
896 }
897
898
899
900
901
902
903 public String getUserInfo() {
904 return this.userInfo;
905 }
906
907
908
909
910
911
912
913 public String getHost() {
914 return this.host;
915 }
916
917
918
919
920
921
922 public int getPort() {
923 return this.port;
924 }
925
926
927
928
929
930
931 public boolean isPathEmpty() {
932 return (this.pathSegments == null || this.pathSegments.isEmpty()) &&
933 (this.encodedPath == null || this.encodedPath.isEmpty());
934 }
935
936
937
938
939
940
941 public List<String> getPathSegments() {
942 return this.pathSegments != null ? new ArrayList<>(this.pathSegments) : new ArrayList<>();
943 }
944
945
946
947
948
949
950 public String getPath() {
951 if (this.pathSegments == null) {
952 return null;
953 }
954 final StringBuilder result = new StringBuilder();
955 for (final String segment : this.pathSegments) {
956 result.append('/').append(segment);
957 }
958 return result.toString();
959 }
960
961
962
963
964
965
966 public boolean isQueryEmpty() {
967 return (this.queryParams == null || this.queryParams.isEmpty()) && this.encodedQuery == null;
968 }
969
970
971
972
973
974
975 public List<NameValuePair> getQueryParams() {
976 return this.queryParams != null ? new ArrayList<>(this.queryParams) : new ArrayList<>();
977 }
978
979
980
981
982
983
984
985
986 public NameValuePair getFirstQueryParam(final String name) {
987 return queryParams.stream().filter(e -> name.equals(e.getName())).findFirst().orElse(null);
988 }
989
990
991
992
993
994
995 public String getFragment() {
996 return this.fragment;
997 }
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010 public URIBuilder normalizeSyntax() {
1011 final String scheme = this.scheme;
1012 if (scheme != null) {
1013 this.scheme = TextUtils.toLowerCase(scheme);
1014 }
1015
1016 if (this.pathRootless) {
1017 return this;
1018 }
1019
1020
1021 this.encodedSchemeSpecificPart = null;
1022 this.encodedAuthority = null;
1023 this.encodedUserInfo = null;
1024 this.encodedPath = null;
1025 this.encodedQuery = null;
1026 this.encodedFragment = null;
1027
1028 final String host = this.host;
1029 if (host != null) {
1030 this.host = TextUtils.toLowerCase(host);
1031 }
1032
1033 if (this.pathSegments != null) {
1034 final List<String> inputSegments = this.pathSegments;
1035 if (!inputSegments.isEmpty()) {
1036 final LinkedList<String> outputSegments = new LinkedList<>();
1037 for (final String inputSegment : inputSegments) {
1038 if (!inputSegment.isEmpty() && !".".equals(inputSegment)) {
1039 if ("..".equals(inputSegment)) {
1040 if (!outputSegments.isEmpty()) {
1041 outputSegments.removeLast();
1042 }
1043 } else {
1044 outputSegments.addLast(inputSegment);
1045 }
1046 }
1047 }
1048 if (!inputSegments.isEmpty()) {
1049 final String lastSegment = inputSegments.get(inputSegments.size() - 1);
1050 if (lastSegment.isEmpty()) {
1051 outputSegments.addLast("");
1052 }
1053 }
1054 this.pathSegments = outputSegments;
1055 } else {
1056 this.pathSegments = Collections.singletonList("");
1057 }
1058 }
1059
1060 return this;
1061 }
1062
1063
1064
1065
1066
1067
1068 @Override
1069 public String toString() {
1070 return buildString();
1071 }
1072
1073 }