41
41
42
42
import org .apache .hc .core5 .annotation .Contract ;
43
43
import org .apache .hc .core5 .annotation .ThreadingBehavior ;
44
+ import org .apache .hc .core5 .http .message .BasicHeaderValueFormatter ;
44
45
import org .apache .hc .core5 .http .message .BasicNameValuePair ;
45
46
import org .apache .hc .core5 .http .message .MessageSupport ;
46
47
import org .apache .hc .core5 .http .message .ParserCursor ;
@@ -67,105 +68,125 @@ public final class ContentType implements Serializable {
67
68
*/
68
69
private static final String CHARSET = "charset" ;
69
70
71
+ /**
72
+ * Flag indicating whether the charset is implicit.
73
+ * <p>
74
+ * When {@code implicitCharset} is {@code true}, the charset will not be explicitly
75
+ * included in the string representation of this {@link ContentType} (i.e., in the {@code toString} method),
76
+ * unless it is required for the given MIME type.
77
+ * If {@code implicitCharset} is {@code false}, the charset will always be included in the string representation,
78
+ * unless the MIME type explicitly disallows charset parameters (e.g., certain binary or multipart types).
79
+ * </p>
80
+ * <p>
81
+ * This flag is essential for proper handling of content types where the charset is either implied by the specification
82
+ * (e.g., JSON is always UTF-8) or where including a charset is not meaningful (e.g., binary types like
83
+ * {@code application/octet-stream}).
84
+ * </p>
85
+ *
86
+ * @since 5.5
87
+ */
88
+ private final boolean implicitCharset ;
89
+
90
+
70
91
// constants
71
92
public static final ContentType APPLICATION_ATOM_XML = create (
72
- "application/atom+xml" , StandardCharsets .UTF_8 );
93
+ "application/atom+xml" , StandardCharsets .UTF_8 , false );
73
94
public static final ContentType APPLICATION_FORM_URLENCODED = create (
74
- "application/x-www-form-urlencoded" , StandardCharsets .ISO_8859_1 );
95
+ "application/x-www-form-urlencoded" , StandardCharsets .ISO_8859_1 , true );
75
96
public static final ContentType APPLICATION_JSON = create (
76
- "application/json" , StandardCharsets .UTF_8 );
97
+ "application/json" , StandardCharsets .UTF_8 , true );
77
98
78
99
/**
79
100
* Public constant media type for {@code application/x-ndjson}.
80
101
* @since 5.1
81
102
*/
82
103
public static final ContentType APPLICATION_NDJSON = create (
83
- "application/x-ndjson" , StandardCharsets .UTF_8 );
104
+ "application/x-ndjson" , StandardCharsets .UTF_8 , true );
84
105
85
106
public static final ContentType APPLICATION_OCTET_STREAM = create (
86
- "application/octet-stream" , (Charset ) null );
107
+ "application/octet-stream" , (Charset ) null , true );
87
108
/**
88
109
* Public constant media type for {@code application/pdf}.
89
110
* @since 5.1
90
111
*/
91
112
public static final ContentType APPLICATION_PDF = create (
92
- "application/pdf" , StandardCharsets . UTF_8 );
113
+ "application/pdf" , ( Charset ) null , true );
93
114
94
115
public static final ContentType APPLICATION_SOAP_XML = create (
95
- "application/soap+xml" , StandardCharsets .UTF_8 );
116
+ "application/soap+xml" , StandardCharsets .UTF_8 , false );
96
117
public static final ContentType APPLICATION_SVG_XML = create (
97
- "application/svg+xml" , StandardCharsets .UTF_8 );
118
+ "application/svg+xml" , StandardCharsets .UTF_8 , false );
98
119
public static final ContentType APPLICATION_XHTML_XML = create (
99
- "application/xhtml+xml" , StandardCharsets .UTF_8 );
120
+ "application/xhtml+xml" , StandardCharsets .UTF_8 , false );
100
121
public static final ContentType APPLICATION_XML = create (
101
- "application/xml" , StandardCharsets .UTF_8 );
122
+ "application/xml" , StandardCharsets .UTF_8 , false );
102
123
/**
103
124
* Public constant media type for {@code application/problem+json}.
104
125
* @see <a href="https://tools.ietf.org/html/rfc7807#section-6.1">Problem Details for HTTP APIs, 6.1. application/problem+json</a>
105
126
* @since 5.1
106
127
*/
107
128
public static final ContentType APPLICATION_PROBLEM_JSON = create (
108
- "application/problem+json" , StandardCharsets .UTF_8 );
129
+ "application/problem+json" , StandardCharsets .UTF_8 , true );
109
130
/**
110
131
* Public constant media type for {@code application/problem+xml}.
111
132
* @see <a href="https://tools.ietf.org/html/rfc7807#section-6.2">Problem Details for HTTP APIs, 6.2. application/problem+xml</a>
112
133
* @since 5.1
113
134
*/
114
135
public static final ContentType APPLICATION_PROBLEM_XML = create (
115
- "application/problem+xml" , StandardCharsets .UTF_8 );
136
+ "application/problem+xml" , StandardCharsets .UTF_8 , false );
116
137
117
138
/**
118
139
* Public constant media type for {@code application/rss+xml}.
119
140
* @since 5.1
120
141
*/
121
142
public static final ContentType APPLICATION_RSS_XML = create (
122
- "application/rss+xml" , StandardCharsets .UTF_8 );
143
+ "application/rss+xml" , StandardCharsets .UTF_8 , false );
123
144
124
145
public static final ContentType IMAGE_BMP = create (
125
- "image/bmp" );
146
+ "image/bmp" , ( Charset ) null , true );
126
147
public static final ContentType IMAGE_GIF = create (
127
- "image/gif" );
148
+ "image/gif" , ( Charset ) null , true );
128
149
public static final ContentType IMAGE_JPEG = create (
129
- "image/jpeg" );
150
+ "image/jpeg" , ( Charset ) null , true );
130
151
public static final ContentType IMAGE_PNG = create (
131
- "image/png" );
152
+ "image/png" , ( Charset ) null , true );
132
153
public static final ContentType IMAGE_SVG = create (
133
- "image/svg+xml" );
154
+ "image/svg+xml" , ( Charset ) null , false );
134
155
public static final ContentType IMAGE_TIFF = create (
135
- "image/tiff" );
156
+ "image/tiff" , ( Charset ) null , true );
136
157
public static final ContentType IMAGE_WEBP = create (
137
- "image/webp" );
158
+ "image/webp" , ( Charset ) null , true );
138
159
public static final ContentType MULTIPART_FORM_DATA = create (
139
- "multipart/form-data" , StandardCharsets .ISO_8859_1 );
160
+ "multipart/form-data" , StandardCharsets .ISO_8859_1 , true );
140
161
141
162
/**
142
163
* Public constant media type for {@code multipart/mixed}.
143
164
* @since 5.1
144
165
*/
145
166
public static final ContentType MULTIPART_MIXED = create (
146
- "multipart/mixed" , StandardCharsets .ISO_8859_1 );
167
+ "multipart/mixed" , StandardCharsets .ISO_8859_1 , true );
147
168
148
169
/**
149
170
* Public constant media type for {@code multipart/related}.
150
171
* @since 5.1
151
172
*/
152
173
public static final ContentType MULTIPART_RELATED = create (
153
- "multipart/related" , StandardCharsets .ISO_8859_1 );
174
+ "multipart/related" , StandardCharsets .ISO_8859_1 , true );
154
175
155
176
public static final ContentType TEXT_HTML = create (
156
- "text/html" , StandardCharsets .UTF_8 );
177
+ "text/html" , StandardCharsets .UTF_8 , true );
157
178
158
179
/**
159
180
* Public constant media type for {@code text/markdown}.
160
181
* @since 5.1
161
182
*/
162
183
public static final ContentType TEXT_MARKDOWN = create (
163
- "text/markdown" , StandardCharsets .UTF_8 );
184
+ "text/markdown" , StandardCharsets .UTF_8 , false );
164
185
165
186
public static final ContentType TEXT_PLAIN = create (
166
- "text/plain" , StandardCharsets .UTF_8 );
187
+ "text/plain" , StandardCharsets .UTF_8 , false );
167
188
public static final ContentType TEXT_XML = create (
168
- "text/xml" , StandardCharsets .UTF_8 );
189
+ "text/xml" , StandardCharsets .UTF_8 , false );
169
190
/**
170
191
* Public constant media type for {@code text/event-stream}.
171
192
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
@@ -175,7 +196,7 @@ public final class ContentType implements Serializable {
175
196
"text/event-stream" , StandardCharsets .UTF_8 );
176
197
177
198
public static final ContentType WILDCARD = create (
178
- "*/*" , (Charset ) null );
199
+ "*/*" , (Charset ) null , true );
179
200
180
201
/**
181
202
* An empty immutable {@code NameValuePair} array.
@@ -225,18 +246,42 @@ public final class ContentType implements Serializable {
225
246
ContentType (
226
247
final String mimeType ,
227
248
final Charset charset ) {
228
- this .mimeType = mimeType ;
229
- this .charset = charset ;
230
- this .params = null ;
249
+ this (mimeType ,charset ,null , false );
231
250
}
232
251
233
252
ContentType (
234
253
final String mimeType ,
235
254
final Charset charset ,
236
255
final NameValuePair [] params ) {
256
+
257
+ this (mimeType ,charset ,params , false );
258
+ }
259
+
260
+ /**
261
+ * Constructs a new instance of {@link ContentType} with the given MIME type, charset, parameters,
262
+ * and an implicit charset flag.
263
+ * <p>
264
+ * If {@code implicitCharset} is set to {@code true}, the charset will not be explicitly
265
+ * included in the string representation of this content type (i.e., the {@code toString} method)
266
+ * unless it is required for the given MIME type.
267
+ * If {@code implicitCharset} is {@code false}, the charset will always be included in the
268
+ * string representation unless the MIME type is one of those that should not include a charset.
269
+ * </p>
270
+ *
271
+ * @param mimeType the MIME type. It must not be {@code null} or empty and must not contain
272
+ * reserved characters such as {@code <">, <;>, <,>}.
273
+ * @param charset the character set for this content type. This can be {@code null}.
274
+ * @param params optional parameters for this content type. If {@code null}, no additional
275
+ * parameters will be included.
276
+ * @param implicitCharset whether the charset is implicit. If {@code true}, the charset is not
277
+ * included in the {@code toString} output unless required.
278
+ * @since 5.5
279
+ */
280
+ ContentType (final String mimeType , final Charset charset , final NameValuePair [] params , final boolean implicitCharset ) {
237
281
this .mimeType = mimeType ;
238
282
this .charset = charset ;
239
- this .params = params ;
283
+ this .implicitCharset = implicitCharset ;
284
+ this .params = params != null ? params .clone () : null ;
240
285
}
241
286
242
287
public String getMimeType () {
@@ -288,8 +333,21 @@ public String toString() {
288
333
buf .append (this .mimeType );
289
334
if (this .params != null ) {
290
335
buf .append ("; " );
291
- MessageSupport .formatParameters (buf , this .params );
292
- } else if (this .charset != null ) {
336
+ boolean first = true ;
337
+ for (int i = 0 ; i < params .length ; i ++) {
338
+ final NameValuePair param = params [i ];
339
+ if (!first ) {
340
+ buf .append ("; " );
341
+ }
342
+ if (param .getName ().equalsIgnoreCase ("charset" ) && implicitCharset ) {
343
+ continue ;
344
+ }
345
+ BasicHeaderValueFormatter .INSTANCE .formatNameValuePair (buf , param , false );
346
+ first = false ;
347
+ }
348
+
349
+ } else if (this .charset != null && !implicitCharset ) {
350
+ // Append charset only if it's not one of the types that shouldn't have charset
293
351
buf .append ("; charset=" );
294
352
buf .append (this .charset .name ());
295
353
}
@@ -306,6 +364,58 @@ private static boolean valid(final String s) {
306
364
return true ;
307
365
}
308
366
367
+
368
+ /**
369
+ * Creates a new instance of {@link ContentType} with the given MIME type, charset,
370
+ * and an implicit charset flag.
371
+ * <p>
372
+ * This method allows specifying whether the charset should be implicit or explicit.
373
+ * If {@code implicitCharset} is set to {@code true}, the charset will not be explicitly
374
+ * included in the string representation of this content type (i.e., the {@code toString} method),
375
+ * unless it is required for the given MIME type. If {@code implicitCharset} is {@code false},
376
+ * the charset will always be included unless the MIME type does not allow a charset.
377
+ * </p>
378
+ *
379
+ * @param mimeType the MIME type. It must not be {@code null} or empty and must not contain
380
+ * reserved characters such as {@code <">, <;>, <,>}.
381
+ * @param charset the character set for this content type. This can be {@code null}.
382
+ * @param implicitCharset whether the charset is implicit. If {@code true}, the charset is
383
+ * not included in the {@code toString} output unless required.
384
+ * @return a new instance of {@link ContentType}.
385
+ * @throws IllegalArgumentException if the MIME type is invalid or contains reserved characters.
386
+ * @since 5.5
387
+ */
388
+ public static ContentType create (final String mimeType , final Charset charset , final boolean implicitCharset ) {
389
+ final String normalizedMimeType = TextUtils .toLowerCase (Args .notBlank (mimeType , "MIME type" ));
390
+ Args .check (valid (normalizedMimeType ), "MIME type may not contain reserved characters" );
391
+ return new ContentType (normalizedMimeType , charset , null , implicitCharset );
392
+ }
393
+
394
+ /**
395
+ * Creates a new instance of {@link ContentType} with the given MIME type, parameters,
396
+ * and an implicit charset flag.
397
+ * <p>
398
+ * This method allows specifying additional parameters for the content type and whether
399
+ * the charset should be implicit or explicit. If {@code implicitCharset} is {@code true},
400
+ * the charset will not be included in the string representation unless required.
401
+ * </p>
402
+ *
403
+ * @param mimeType the MIME type. It must not be {@code null} or empty and must not contain
404
+ * reserved characters such as {@code <">, <;>, <,>}.
405
+ * @param implicitCharset whether the charset is implicit. If {@code true}, the charset is
406
+ * not included in the {@code toString} output unless required.
407
+ * @param params optional parameters for the content type. Can be {@code null}.
408
+ * @return a new instance of {@link ContentType}.
409
+ * @throws IllegalArgumentException if the MIME type is invalid or contains reserved characters.
410
+ * @throws UnsupportedCharsetException if the charset provided in the parameters is not supported.
411
+ * @since 5.5
412
+ */
413
+ public static ContentType create (final String mimeType , final boolean implicitCharset , final NameValuePair ... params ) throws UnsupportedCharsetException {
414
+ final String type = TextUtils .toLowerCase (Args .notBlank (mimeType , "MIME type" ));
415
+ Args .check (valid (type ), "MIME type may not contain reserved characters" );
416
+ return create (mimeType , params != null ? params .clone () : null , implicitCharset );
417
+ }
418
+
309
419
/**
310
420
* Creates a new instance of {@link ContentType}.
311
421
*
@@ -315,9 +425,7 @@ private static boolean valid(final String s) {
315
425
* @return content type
316
426
*/
317
427
public static ContentType create (final String mimeType , final Charset charset ) {
318
- final String normalizedMimeType = TextUtils .toLowerCase (Args .notBlank (mimeType , "MIME type" ));
319
- Args .check (valid (normalizedMimeType ), "MIME type may not contain reserved characters" );
320
- return new ContentType (normalizedMimeType , charset );
428
+ return create (mimeType , charset , false );
321
429
}
322
430
323
431
/**
@@ -356,6 +464,10 @@ private static ContentType create(final HeaderElement helem, final boolean stric
356
464
}
357
465
358
466
private static ContentType create (final String mimeType , final NameValuePair [] params , final boolean strict ) {
467
+ return create (mimeType , params != null ? params .clone () : null , strict , false );
468
+ }
469
+
470
+ private static ContentType create (final String mimeType , final NameValuePair [] params , final boolean strict , final boolean implicitCharset ) {
359
471
Charset charset = null ;
360
472
if (params != null ) {
361
473
for (final NameValuePair param : params ) {
@@ -374,7 +486,7 @@ private static ContentType create(final String mimeType, final NameValuePair[] p
374
486
}
375
487
}
376
488
}
377
- return new ContentType (mimeType , charset , params != null && params .length > 0 ? params : null );
489
+ return new ContentType (mimeType , charset , params != null && params .length > 0 ? params : null , implicitCharset );
378
490
}
379
491
380
492
/**
@@ -517,11 +629,11 @@ public ContentType withParameters(
517
629
for (final Map .Entry <String , String > entry : paramMap .entrySet ()) {
518
630
newParams .add (new BasicNameValuePair (entry .getKey (), entry .getValue ()));
519
631
}
520
- return create (this .getMimeType (), newParams .toArray (EMPTY_NAME_VALUE_PAIR_ARRAY ), true );
632
+ return create (this .getMimeType (), newParams .toArray (EMPTY_NAME_VALUE_PAIR_ARRAY ), true , this . implicitCharset );
521
633
}
522
634
523
635
public boolean isSameMimeType (final ContentType contentType ) {
524
636
return contentType != null && mimeType .equalsIgnoreCase (contentType .getMimeType ());
525
637
}
526
638
527
- }
639
+ }
0 commit comments