34
34
import java .nio .charset .StandardCharsets ;
35
35
import java .util .ArrayList ;
36
36
import java .util .Arrays ;
37
+ import java .util .BitSet ;
37
38
import java .util .Collections ;
38
39
import java .util .LinkedList ;
39
40
import java .util .List ;
@@ -87,9 +88,36 @@ public static URIBuilder loopbackAddress() {
87
88
private Charset charset ;
88
89
private String fragment ;
89
90
private String encodedFragment ;
91
+ private EncodingPolicy encodingPolicy = EncodingPolicy .ALL_RESERVED ;
90
92
91
93
private boolean plusAsBlank ;
92
94
95
+ /**
96
+ * Defines the encoding policy for URI components in {@link URIBuilder}.
97
+ * This enum controls how characters are percent-encoded when constructing a URI,
98
+ * allowing flexibility between strict encoding and RFC 3986-compliant behavior.
99
+ *
100
+ * @since 5.4
101
+ */
102
+ public enum EncodingPolicy {
103
+ /**
104
+ * Encodes all reserved characters, allowing only unreserved characters
105
+ * (ALPHA, DIGIT, "-", ".", "_", "~") to remain unencoded. This is a strict
106
+ * policy suitable for conservative URI production where maximum encoding
107
+ * is desired.
108
+ */
109
+ ALL_RESERVED ,
110
+
111
+ /**
112
+ * Follows RFC 3986 component-specific encoding rules. For example, query and
113
+ * fragment components allow unreserved characters, sub-delimiters ("!", "$",
114
+ * "&", "'", "(", ")", "*", "+", ",", ";", "="), and additional characters
115
+ * (":", "@", "/", "?") to remain unencoded, as defined by {@code PercentCodec.FRAGMENT}.
116
+ * This policy ensures compliance with RFC 3986 while maintaining interoperability.
117
+ */
118
+ RFC_3986
119
+ }
120
+
93
121
/**
94
122
* Constructs an empty instance.
95
123
*/
@@ -175,6 +203,22 @@ public URIBuilder setCharset(final Charset charset) {
175
203
return this ;
176
204
}
177
205
206
+ /**
207
+ * Sets the encoding policy for this {@link URIBuilder}.
208
+ * The encoding policy determines how URI components (e.g., query, fragment) are
209
+ * percent-encoded when building the URI string. If not set, the default policy
210
+ * is {@link EncodingPolicy#RFC_3986}.
211
+ *
212
+ * @param encodingPolicy the encoding policy to apply, or {@code null} to reset
213
+ * to the default ({@link EncodingPolicy#ALL_RESERVED})
214
+ * @return this {@link URIBuilder} instance for method chaining
215
+ * @since 5.4
216
+ */
217
+ public URIBuilder setEncodingPolicy (final EncodingPolicy encodingPolicy ) {
218
+ this .encodingPolicy = encodingPolicy ;
219
+ return this ;
220
+ }
221
+
178
222
/**
179
223
* Gets the authority.
180
224
*
@@ -300,33 +344,46 @@ static List<String> parsePath(final CharSequence s, final Charset charset) {
300
344
return list ;
301
345
}
302
346
303
- static void formatPath (final StringBuilder buf , final Iterable <String > segments , final boolean rootless , final Charset charset ) {
347
+ static void formatPath (final StringBuilder buf , final Iterable <String > segments , final boolean rootless ,
348
+ final Charset charset , final BitSet safechars ) {
304
349
int i = 0 ;
305
350
for (final String segment : segments ) {
306
351
if (i > 0 || !rootless ) {
307
352
buf .append (PATH_SEPARATOR );
308
353
}
309
- PercentCodec .encode (buf , segment , charset , PercentCodec . PATH_SEGMENT , false );
354
+ PercentCodec .encode (buf , segment , charset , safechars , false );
310
355
i ++;
311
356
}
312
357
}
313
358
314
- static void formatQuery (final StringBuilder buf , final Iterable <? extends NameValuePair > params , final Charset charset ,
315
- final boolean blankAsPlus ) {
359
+ static void formatPath (final StringBuilder buf , final Iterable <String > segments , final boolean rootless ,
360
+ final Charset charset ) {
361
+ formatPath (buf , segments , rootless , charset , PercentCodec .UNRESERVED );
362
+ }
363
+
364
+
365
+ static void formatQuery (final StringBuilder buf , final Iterable <? extends NameValuePair > params ,
366
+ final Charset charset , final BitSet safechars , final boolean blankAsPlus ) {
316
367
int i = 0 ;
317
368
for (final NameValuePair parameter : params ) {
318
369
if (i > 0 ) {
319
370
buf .append (QUERY_PARAM_SEPARATOR );
320
371
}
321
- PercentCodec .encode (buf , parameter .getName (), charset , blankAsPlus );
372
+ PercentCodec .encode (buf , parameter .getName (), charset , safechars , blankAsPlus );
322
373
if (parameter .getValue () != null ) {
323
374
buf .append (PARAM_VALUE_SEPARATOR );
324
- PercentCodec .encode (buf , parameter .getValue (), charset , blankAsPlus );
375
+ PercentCodec .encode (buf , parameter .getValue (), charset , safechars , blankAsPlus );
325
376
}
326
377
i ++;
327
378
}
328
379
}
329
380
381
+ static void formatQuery (final StringBuilder buf , final Iterable <? extends NameValuePair > params ,
382
+ final Charset charset , final boolean blankAsPlus ) {
383
+ formatQuery (buf , params , charset , PercentCodec .UNRESERVED , blankAsPlus );
384
+ }
385
+
386
+
330
387
/**
331
388
* Builds a {@link URI} instance.
332
389
*/
@@ -356,18 +413,22 @@ private String buildString() {
356
413
} else if (this .userInfo != null ) {
357
414
final int idx = this .userInfo .indexOf (':' );
358
415
if (idx != -1 ) {
359
- PercentCodec .encode (sb , this .userInfo .substring (0 , idx ), this .charset , PercentCodec .USERINFO , false );
416
+ PercentCodec .encode (sb , this .userInfo .substring (0 , idx ), this .charset ,
417
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .UNRESERVED : PercentCodec .USERINFO , false );
360
418
sb .append (':' );
361
- PercentCodec .encode (sb , this .userInfo .substring (idx + 1 ), this .charset , PercentCodec .USERINFO , false );
419
+ PercentCodec .encode (sb , this .userInfo .substring (idx + 1 ), this .charset ,
420
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .UNRESERVED : PercentCodec .USERINFO , false );
362
421
} else {
363
- PercentCodec .encode (sb , this .userInfo , this .charset , PercentCodec .USERINFO , false );
422
+ PercentCodec .encode (sb , this .userInfo , this .charset ,
423
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .UNRESERVED : PercentCodec .USERINFO , false );
364
424
}
365
425
sb .append ("@" );
366
426
}
367
427
if (InetAddressUtils .isIPv6 (this .host )) {
368
428
sb .append ("[" ).append (this .host ).append ("]" );
369
429
} else {
370
- PercentCodec .encode (sb , this .host , this .charset , PercentCodec .REG_NAME , false );
430
+ PercentCodec .encode (sb , this .host , this .charset ,
431
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .UNRESERVED : PercentCodec .REG_NAME , false );
371
432
}
372
433
if (this .port >= 0 ) {
373
434
sb .append (":" ).append (this .port );
@@ -382,23 +443,27 @@ private String buildString() {
382
443
}
383
444
sb .append (this .encodedPath );
384
445
} else if (this .pathSegments != null ) {
385
- formatPath (sb , this .pathSegments , !authoritySpecified && this .pathRootless , this .charset );
446
+ formatPath (sb , this .pathSegments , !authoritySpecified && this .pathRootless , this .charset ,
447
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .UNRESERVED : PercentCodec .PATH_SEGMENT );
386
448
}
387
449
if (this .encodedQuery != null ) {
388
450
sb .append ("?" ).append (this .encodedQuery );
389
451
} else if (this .queryParams != null && !this .queryParams .isEmpty ()) {
390
452
sb .append ("?" );
391
- formatQuery (sb , this .queryParams , this .charset , false );
453
+ formatQuery (sb , this .queryParams , this .charset ,
454
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .UNRESERVED : PercentCodec .QUERY , false );
392
455
} else if (this .query != null ) {
393
456
sb .append ("?" );
394
- PercentCodec .encode (sb , this .query , this .charset , PercentCodec .QUERY , false );
457
+ PercentCodec .encode (sb , this .query , this .charset ,
458
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .URIC : PercentCodec .QUERY , false );
395
459
}
396
460
}
397
461
if (this .encodedFragment != null ) {
398
462
sb .append ("#" ).append (this .encodedFragment );
399
463
} else if (this .fragment != null ) {
400
464
sb .append ("#" );
401
- PercentCodec .encode (sb , this .fragment , this .charset , PercentCodec .FRAGMENT , false );
465
+ PercentCodec .encode (sb , this .fragment , this .charset ,
466
+ encodingPolicy == EncodingPolicy .ALL_RESERVED ? PercentCodec .URIC : PercentCodec .FRAGMENT , false );
402
467
}
403
468
return sb .toString ();
404
469
}
0 commit comments