From b0625f395690e99a9be8de4643bb8d00a4e3fc5f Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Fri, 14 Mar 2025 23:25:10 -0400 Subject: [PATCH 01/21] Added MultiPartFormDataRepresentation Added MultiPartFormDataRepresentation to Jetty extension to support generation and parsing. --- changes.md | 10 +- .../MultiPartFormDataRepresentation.java | 213 ++++++++++++++++++ .../restlet/engine/header/ContentType.java | 10 +- 3 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java diff --git a/changes.md b/changes.md index 54c5c0cc15..e505307406 100644 --- a/changes.md +++ b/changes.md @@ -2,11 +2,13 @@ Changes log =========== - 2.6 Release Candidate 1 (??-03-2025) + - Enhancements + - Added MultiPartFormDataRepresentation to Jetty extension to support generation and parsing. - Misc - - Upgrade the thymeleaf library to 3.1.3.RELEASE. - - Upgraded the Slf4j library to 5.12.0. - - Upgraded the GWT libraries to version 2.12.2. - - Upgraded the Jetty library to version 2.0.17. + - Upgraded Thymeleaf library to 3.1.3.RELEASE. + - Upgraded Slf4j library to 5.12.0. + - Upgraded GWT libraries to version 2.12.2. + - Upgraded Jetty library to version 2.0.17. - 2.6 Milestone 2 (02-03-2025) - Enhancements diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java new file mode 100644 index 0000000000..88f93f1ec8 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -0,0 +1,213 @@ +/** + * Copyright 2005-2024 Qlik + *

+ * The contents of this file is subject to the terms of the Apache 2.0 open + * source license available at http://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ + +package org.restlet.ext.jetty; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.eclipse.jetty.http.MultiPart; +import org.eclipse.jetty.http.MultiPart.Part; +import org.eclipse.jetty.http.MultiPartConfig; +import org.eclipse.jetty.http.MultiPartFormData; +import org.eclipse.jetty.http.MultiPartFormData.Parts; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.InputStreamContentSource; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.Promise; +import org.restlet.data.MediaType; +import org.restlet.engine.header.ContentType; +import org.restlet.representation.InputRepresentation; +import org.restlet.representation.Representation; + +/** + * Input representation that can either parse or generate a multipart form data + * representation depending on which constructor is invoked. + * + * @author Jerome Louvel + */ +public class MultiPartFormDataRepresentation extends InputRepresentation { + + /** + * Adds a randomly generated boundary to the media type parameters. + * + * @param mediaType The media type to update. + * @return The updated media type. + */ + public static MediaType addBoundary(MediaType mediaType) { + String boundary = MultiPart.generateBoundary(null, 24); + mediaType.getParameters().add("boundary", boundary); + return mediaType; + } + + /** + * Returns the value of the first mediatype parameter with "boundary" name. + * + * @param mediaType The media type that might contain a "boundary" + * parameter. + * @return The value of the first mediatype parameter with "boundary" name. + */ + public static String getBoundary(MediaType mediaType) { + String result = null; + + if (mediaType != null) { + mediaType.getParameters().getFirstValue("boundary"); + } + + return result; + } + + /** + * The boundary used to separate each part for the parsed or generated form. + */ + private volatile String boundary; + + /** The wrapped multipart form data either parsed or to be generated. */ + private volatile Parts parts; + + /** + * Constructor that wraps multiple parts and generates the content via + * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param parts The source parts to use when generating the representation. + */ + public MultiPartFormDataRepresentation(Parts parts) { + super(null, MediaType.MULTIPART_FORM_DATA); + this.boundary = getMediaType().getParameters() + .getFirstValue("boundary"); + this.parts = parts; + } + + /** + * Constructor that parses the content based on a given configuration into + * {@link #getParts()}. Uses a default {@link MultiPartConfig}. + * + * @param content The multipart entity to parse which should have a media + * type based on {@link MediaType#MULTIPART_FORM_DATA}, with + * a "boundary" parameter. + * @throws IOException + */ + public MultiPartFormDataRepresentation(Representation content) + throws IOException { + this(content, new MultiPartConfig.Builder().build()); + } + + /** + * Constructor that parses the content based on a given configuration into + * {@link #getParts()}. + * + * @param content The multipart entity to parse which should have a media + * type based on {@link MediaType#MULTIPART_FORM_DATA}, with + * a "boundary" parameter. + * @param config The multipart configuration. + * @throws IOException + */ + public MultiPartFormDataRepresentation(Representation content, + MultiPartConfig config) throws IOException { + this(ContentType.writeHeader(content), content.getStream(), config); + } + + /** + * Constructor that parses the content based on a given configuration into + * {@link #getParts()}. + * + * @param contentType The media type that should be based on + * {@link MediaType#MULTIPART_FORM_DATA}, with a + * "boundary" parameter. + * @param content The multipart entity to parse. + * @param config The multipart configuration. + * @throws IOException + */ + public MultiPartFormDataRepresentation(String contentType, + InputStream content, MultiPartConfig config) throws IOException { + super(null, MediaType.MULTIPART_FORM_DATA); + this.boundary = boundary; + + if (content != null) { + Content.Source contentSource = new InputStreamContentSource( + content); + Attributes.Mapped attributes = new Attributes.Mapped(); + + // Convert the request content into parts. + MultiPartFormData.onParts(contentSource, attributes, contentType, + config, new Promise.Invocable<>() { + @Override + public void failed(Throwable failure) { + throw new IllegalStateException( + "Unable to parse the multipart form data representation", + failure); + } + + @Override + public InvocationType getInvocationType() { + return InvocationType.BLOCKING; + } + + @Override + public void succeeded(MultiPartFormData.Parts parts) { + // Store the resulting parts + MultiPartFormDataRepresentation.this.parts = parts; + } + }); + } + } + + /** + * Returns the boundary used to separate each part for the parsed or + * generated form. + * + * @return The boundary used to separate each part for the parsed or + * generated form. + */ + public String getBoundary() { + return boundary; + } + + /** + * Returns the wrapped multipart form data either parsed or to be generated. + * + * @return The wrapped multipart form data either parsed or to be generated. + */ + public Parts getParts() { + return parts; + } + + /** + * Returns an input stream that generates the multipart form data + * serialization for the wrapped {@link #getParts()} object. + * + * @return An input stream that generates the multipart form data. + */ + @Override + public InputStream getStream() throws IOException { + MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource( + getBoundary()); + + for (Part part : this.parts) { + content.addPart(part); + } + + content.close(); + setStream(null); + return Content.Source.asInputStream(content); + } + + /** + * Sets the boundary used to separate each part for the parsed or generated + * form. + * + * @param boundary The boundary used to separate each part for the parsed or + * generated form. + */ + public void setBoundary(String boundary) { + this.boundary = boundary; + } + +} diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java index aa0932e138..6fe0387ab3 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java @@ -11,6 +11,7 @@ import org.restlet.data.CharacterSet; import org.restlet.data.MediaType; +import org.restlet.data.Parameter; import org.restlet.representation.Representation; import java.io.IOException; @@ -56,7 +57,14 @@ public static String writeHeader(MediaType mediaType, CharacterSet characterSet) if ((mediaType.getParameters().getFirstValue("charset") == null) && (characterSet != null)) { result = result + "; charset=" + characterSet.getName(); } - + + for (Parameter param : mediaType.getParameters()) { + if ((param != null) && !param.getName().equals("charset")) { + result = result + "; " + param.getName() + "=" + + param.getValue(); + } + } + return result; } From 966ec59a350798e79d8a8cc8a54b0c13c6f45799 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Sat, 15 Mar 2025 23:10:21 -0400 Subject: [PATCH 02/21] Added support for the "charset" parameter in HTTP BASIC challenges See issue https://github.com/restlet/restlet-framework-java/issues/1455 --- .../engine/security/HttpBasicHelper.java | 202 +++++++++++------- 1 file changed, 128 insertions(+), 74 deletions(-) diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java index 74466ab480..6637751bc9 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java @@ -41,93 +41,147 @@ public HttpBasicHelper() { @Override public void formatRequest(ChallengeWriter cw, ChallengeRequest challenge, Response response, Series

httpHeaders) throws IOException { - if (challenge.getRealm() != null) { - cw.appendQuotedChallengeParameter("realm", challenge.getRealm()); - } else { - getLogger() - .warning("The realm directive is required for all authentication schemes that issue a challenge."); - } + String realm = challenge.getRealm(); + String charset = challenge.getParameters().getFirstValue("charset"); + + if (realm != null) { + cw.appendQuotedChallengeParameter("realm", realm); + } else { + getLogger().warning( + "The realm directive is required for all authentication schemes that issue a challenge."); + } + + if (charset != null) { + if ("UTF-8".equalsIgnoreCase(charset)) { + cw.appendQuotedChallengeParameter("charset", "UTF-8"); + } else { + getLogger().warning( + "The \"charset\" parameter must be \"UTF-8\" per RFC 7617."); + } + } } @Override public void formatResponse(ChallengeWriter cw, ChallengeResponse challenge, Request request, Series
httpHeaders) { - try { - if (challenge == null) { - throw new RuntimeException("No challenge provided, unable to encode credentials"); - } else { - CharArrayWriter credentials = new CharArrayWriter(); - credentials.write(challenge.getIdentifier()); - credentials.write(":"); - credentials.write(challenge.getSecret()); - cw.append(Base64.getEncoder() - .encodeToString(IoUtils.toByteArray(credentials.toCharArray(), "ISO-8859-1"))); - } - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Unsupported encoding, unable to encode credentials"); - } catch (IOException e) { - throw new RuntimeException("Unexpected exception, unable to encode credentials", e); - } + try { + if (challenge == null) { + throw new RuntimeException( + "No challenge provided, unable to encode credentials"); + } else { + String charset = challenge.getParameters() + .getFirstValue("charset"); + + if (charset != null) { + if ("UTF-8".equalsIgnoreCase(charset)) { + charset = "UTF-8"; + } else { + getLogger().warning( + "The \"charset\" parameter must be \"UTF-8\" per RFC 7617. Using \"ISO-8859-1\" instead."); + charset = "ISO-8859-1"; + } + }else { + charset = "ISO-8859-1"; + } + + CharArrayWriter credentials = new CharArrayWriter(); + credentials.write(challenge.getIdentifier()); + credentials.write(":"); + credentials.write(challenge.getSecret()); + cw.append(Base64.getEncoder().encodeToString(IoUtils + .toByteArray(credentials.toCharArray(), charset))); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException( + "Unsupported encoding, unable to encode credentials"); + } catch (IOException e) { + throw new RuntimeException( + "Unexpected exception, unable to encode credentials", e); + } } @Override public void parseRequest(ChallengeRequest challenge, Response response, Series
httpHeaders) { - if (challenge.getRawValue() != null) { - HeaderReader hr = new HeaderReader(challenge.getRawValue()); - - try { - Parameter param = hr.readParameter(); - - while (param != null) { - try { - if ("realm".equals(param.getName())) { - challenge.setRealm(param.getValue()); - } else { - challenge.getParameters().add(param); - } - - if (hr.skipValueSeparator()) { - param = hr.readParameter(); - } else { - param = null; - } - } catch (Exception e) { - Context.getCurrentLogger().log(Level.WARNING, - "Unable to parse the challenge request header parameter", e); - } - } - } catch (Exception e) { - Context.getCurrentLogger().log(Level.WARNING, "Unable to parse the challenge request header parameter", - e); - } - } + if (challenge.getRawValue() != null) { + HeaderReader hr = new HeaderReader( + challenge.getRawValue()); + + try { + Parameter param = hr.readParameter(); + + while (param != null) { + try { + if ("realm".equals(param.getName())) { + challenge.setRealm(param.getValue()); + } else { + challenge.getParameters().add(param); + } + + if (hr.skipValueSeparator()) { + param = hr.readParameter(); + } else { + param = null; + } + } catch (Exception e) { + Context.getCurrentLogger().log(Level.WARNING, + "Unable to parse the challenge request header parameter", + e); + } + } + } catch (Exception e) { + Context.getCurrentLogger().log(Level.WARNING, + "Unable to parse the challenge request header parameter", + e); + } + } } @Override public void parseResponse(ChallengeResponse challenge, Request request, Series
httpHeaders) { - try { - byte[] credentialsEncoded = Base64.getDecoder().decode(challenge.getRawValue()); - - if (credentialsEncoded == null) { - getLogger().info("Cannot decode credentials: " + challenge.getRawValue()); - } - - String credentials = new String(credentialsEncoded, "ISO-8859-1"); - int separator = credentials.indexOf(':'); - - if (separator == -1) { - // Log the blocking - getLogger().info("Invalid credentials given by client with IP: " - + ((request != null) ? request.getClientInfo().getAddress() : "?")); - } else { - challenge.setIdentifier(credentials.substring(0, separator)); - challenge.setSecret(credentials.substring(separator + 1)); - } - } catch (UnsupportedEncodingException e) { - getLogger().log(Level.INFO, "Unsupported HTTP Basic encoding error", e); - } catch (IllegalArgumentException e) { - getLogger().log(Level.INFO, "Unable to decode the HTTP Basic credential", e); - } + try { + String charset = challenge.getParameters() + .getFirstValue("charset"); + + if (charset != null) { + if ("UTF-8".equalsIgnoreCase(charset)) { + charset = "UTF-8"; + } else { + getLogger().warning( + "The \"charset\" parameter must be \"UTF-8\" per RFC 7617. Using \"ISO-8859-1\" instead."); + charset = "ISO-8859-1"; + } + }else { + charset = "ISO-8859-1"; + } + + byte[] credentialsEncoded = Base64.getDecoder() + .decode(challenge.getRawValue()); + + if (credentialsEncoded == null) { + getLogger().info("Cannot decode credentials: " + + challenge.getRawValue()); + } + + String credentials = new String(credentialsEncoded, charset); + int separator = credentials.indexOf(':'); + + if (separator == -1) { + // Log the blocking + getLogger().info("Invalid credentials given by client with IP: " + + ((request != null) + ? request.getClientInfo().getAddress() + : "?")); + } else { + challenge.setIdentifier(credentials.substring(0, separator)); + challenge.setSecret(credentials.substring(separator + 1)); + } + } catch (UnsupportedEncodingException e) { + getLogger().log(Level.INFO, "Unsupported HTTP Basic encoding error", + e); + } catch (IllegalArgumentException e) { + getLogger().log(Level.INFO, + "Unable to decode the HTTP Basic credential", e); + } } } From e019a2b4dd59f6be4ebb8c09dc10e90cc2988f56 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Sat, 15 Mar 2025 23:11:04 -0400 Subject: [PATCH 03/21] Removed unused imports --- .../org/restlet/ext/crypto/CookieAuthenticatorTestCase.java | 1 - .../java/org/restlet/ext/freemarker/FreeMarkerTestCase.java | 1 - .../src/test/java/org/restlet/ext/jackson/JacksonTestCase.java | 1 - .../java/org/restlet/ext/jetty/connectors/SslGetTestCase.java | 1 - .../java/org/restlet/ext/spring/SpringBeanRouterTestCase.java | 1 - .../src/test/java/org/restlet/data/FileClientTestCase.java | 2 -- .../src/test/java/org/restlet/data/RiapConnectorsTestCase.java | 3 --- .../test/java/org/restlet/engine/header/HeaderTestCase.java | 3 --- .../restlet/engine/util/AlphaNumericComparatorTestCase.java | 1 - .../restlet/resource/AbstractAnnotatedResourceTestCase.java | 1 - .../resource/AbstractGenericAnnotatedServerResource.java | 3 --- .../src/test/java/org/restlet/resource/MyException01.java | 2 -- .../src/test/java/org/restlet/resource/MyException02.java | 2 -- .../src/test/java/org/restlet/resource/MyResource02.java | 2 -- .../src/test/java/org/restlet/resource/MyResource04.java | 3 --- .../src/test/java/org/restlet/resource/MyResource05.java | 3 --- .../src/test/java/org/restlet/resource/MyResource06.java | 3 --- .../src/test/java/org/restlet/resource/MyResource07.java | 3 --- .../src/test/java/org/restlet/resource/MyResource08.java | 3 --- .../src/test/java/org/restlet/resource/MyResource12.java | 2 -- .../src/test/java/org/restlet/resource/MyResource17.java | 2 -- .../test/java/org/restlet/routing/AbstractFilterTestCase.java | 1 - .../src/test/java/org/restlet/security/SaasApplication.java | 1 - .../src/test/java/org/restlet/security/SecurityTestCase.java | 1 - 24 files changed, 46 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/CookieAuthenticatorTestCase.java b/org.restlet.java/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/CookieAuthenticatorTestCase.java index 5ea7961cd6..4c74e7e91f 100644 --- a/org.restlet.java/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/CookieAuthenticatorTestCase.java +++ b/org.restlet.java/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/CookieAuthenticatorTestCase.java @@ -9,7 +9,6 @@ package org.restlet.ext.crypto; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.restlet.*; import org.restlet.data.CookieSetting; diff --git a/org.restlet.java/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java b/org.restlet.java/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java index c1aa98fb76..013cc4ed5d 100644 --- a/org.restlet.java/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java +++ b/org.restlet.java/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java @@ -18,7 +18,6 @@ import java.io.FileWriter; import java.nio.file.Files; import java.util.Map; -import java.util.TreeMap; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/org.restlet.java/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java b/org.restlet.java/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java index aea896a2a8..1ec6a46c68 100644 --- a/org.restlet.java/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java +++ b/org.restlet.java/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java @@ -19,7 +19,6 @@ import java.util.Date; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; /** diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java index 0ee1b8d7a7..43d56eab66 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java @@ -16,7 +16,6 @@ import org.restlet.representation.Variant; import org.restlet.resource.ServerResource; import org.restlet.routing.Router; -import org.restlet.util.Series; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/org.restlet.java/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java b/org.restlet.java/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java index b16db8c473..e8566469a4 100644 --- a/org.restlet.java/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java +++ b/org.restlet.java/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java @@ -10,7 +10,6 @@ package org.restlet.ext.spring; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.restlet.Request; diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/data/FileClientTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/data/FileClientTestCase.java index 275d81a463..273d81cf9d 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/data/FileClientTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/data/FileClientTestCase.java @@ -11,10 +11,8 @@ import org.junit.jupiter.api.Test; import org.restlet.engine.Engine; -import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.resource.ClientResource; -import org.restlet.resource.ResourceException; import java.io.File; import java.io.IOException; diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/data/RiapConnectorsTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/data/RiapConnectorsTestCase.java index f29848cfef..4e71bb98dd 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/data/RiapConnectorsTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/data/RiapConnectorsTestCase.java @@ -11,8 +11,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.restlet.*; @@ -24,7 +22,6 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; /** * Unit test case for the RIAP Internal routing protocol. diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/header/HeaderTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/header/HeaderTestCase.java index 7e622ffd32..c116b96517 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/header/HeaderTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/header/HeaderTestCase.java @@ -9,18 +9,15 @@ package org.restlet.engine.header; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.restlet.data.ClientInfo; import org.restlet.data.Encoding; import org.restlet.data.Header; import org.restlet.data.MediaType; import org.restlet.engine.util.DateUtils; -import org.restlet.representation.Representation; import java.io.IOException; import java.util.ArrayList; -import java.util.Base64; import java.util.Date; import java.util.List; diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/util/AlphaNumericComparatorTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/util/AlphaNumericComparatorTestCase.java index f1003fd7dc..e62f890faf 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/util/AlphaNumericComparatorTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/engine/util/AlphaNumericComparatorTestCase.java @@ -14,7 +14,6 @@ import org.restlet.resource.Directory; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractAnnotatedResourceTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractAnnotatedResourceTestCase.java index c4b0a7725f..3badf2983b 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractAnnotatedResourceTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractAnnotatedResourceTestCase.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.BeforeEach; import org.restlet.engine.Engine; import org.restlet.representation.ObjectRepresentation; -import org.restlet.resource.ClientResource; /** * Test the annotated resources, client and server sides. diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractGenericAnnotatedServerResource.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractGenericAnnotatedServerResource.java index b859f17e04..4035a8512c 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractGenericAnnotatedServerResource.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/AbstractGenericAnnotatedServerResource.java @@ -9,9 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Post; -import org.restlet.resource.ServerResource; - public abstract class AbstractGenericAnnotatedServerResource extends ServerResource { @Post diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException01.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException01.java index f9e79d5749..620bf91493 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException01.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException01.java @@ -11,8 +11,6 @@ import java.util.Date; -import org.restlet.resource.Status; - @Status(value = 400, serialize = false) public class MyException01 extends Throwable { diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException02.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException02.java index 8b431b9b3b..5ff4e2055d 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException02.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyException02.java @@ -9,8 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Status; - @Status(value = 400) public class MyException02 extends Throwable { diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource02.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource02.java index edeef7f103..786b164cb4 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource02.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource02.java @@ -12,8 +12,6 @@ import org.restlet.data.MediaType; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; -import org.restlet.resource.Get; -import org.restlet.resource.ServerResource; public class MyResource02 extends ServerResource { diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource04.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource04.java index cadd4de7ec..2c0240b31f 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource04.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource04.java @@ -9,9 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Get; -import org.restlet.resource.ServerResource; - public class MyResource04 extends ServerResource { @Get("xml") diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource05.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource05.java index d87341832b..a036ccef83 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource05.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource05.java @@ -9,9 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Post; -import org.restlet.resource.ServerResource; - public class MyResource05 extends ServerResource { @Post("txt:xml") diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource06.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource06.java index c777fc8f79..206681092a 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource06.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource06.java @@ -11,9 +11,6 @@ import java.io.IOException; -import org.restlet.resource.Post; -import org.restlet.resource.ServerResource; - public class MyResource06 extends ServerResource { @Post("txt:xml") diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource07.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource07.java index c927c7114b..f38a0f867a 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource07.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource07.java @@ -9,9 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Post; -import org.restlet.resource.ServerResource; - public class MyResource07 extends ServerResource { @Post("json:xml") diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource08.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource08.java index 350d0d729d..cb0eb69e41 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource08.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource08.java @@ -9,9 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Post; -import org.restlet.resource.ServerResource; - public class MyResource08 extends ServerResource { @Post("xml|json:xml|json") diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource12.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource12.java index 8d8574a1b4..9ab5705ec1 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource12.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource12.java @@ -10,8 +10,6 @@ package org.restlet.resource; import org.restlet.data.Form; -import org.restlet.resource.Get; -import org.restlet.resource.Put; /** * Sample annotated interface. diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource17.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource17.java index 85381289bb..2294da3ab3 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource17.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/resource/MyResource17.java @@ -9,8 +9,6 @@ package org.restlet.resource; -import org.restlet.resource.Post; - public interface MyResource17 { @Post diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/AbstractFilterTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/AbstractFilterTestCase.java index 8c723b9cbb..e88b04ce5c 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/AbstractFilterTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/AbstractFilterTestCase.java @@ -9,7 +9,6 @@ package org.restlet.routing; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.restlet.Request; import org.restlet.Response; diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SaasApplication.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SaasApplication.java index 94468f2440..75c7fcd094 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SaasApplication.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SaasApplication.java @@ -14,7 +14,6 @@ import org.restlet.Restlet; import org.restlet.data.ChallengeScheme; import org.restlet.routing.Router; -import org.restlet.security.*; /** * Sample SAAS application with a Basic authenticator guarding a hello world diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SecurityTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SecurityTestCase.java index 21baf2661f..ca3e83df39 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SecurityTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/SecurityTestCase.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.restlet.Component; import org.restlet.data.ChallengeResponse; import org.restlet.data.Status; import org.restlet.engine.Engine; From 27c7bc2402b9e378f41ea5b4401611968ba6f837 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Sat, 15 Mar 2025 23:12:44 -0400 Subject: [PATCH 04/21] Update changes.md --- changes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changes.md b/changes.md index e505307406..114da7fa7e 100644 --- a/changes.md +++ b/changes.md @@ -4,6 +4,7 @@ Changes log - 2.6 Release Candidate 1 (??-03-2025) - Enhancements - Added MultiPartFormDataRepresentation to Jetty extension to support generation and parsing. + - Added support for the "charset" parameter in HTTP BASIC challenges. Reported by Marc Lafon. - Misc - Upgraded Thymeleaf library to 3.1.3.RELEASE. - Upgraded Slf4j library to 5.12.0. From f30c57a49edc3e786a409acc128ff75b8f331d4a Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Sat, 15 Mar 2025 23:31:28 -0400 Subject: [PATCH 05/21] Update MultiPartFormDataRepresentation.java Added "location" parameter to help set a useful MultiPartConfig in addition to the default Jetty values --- .../jetty/MultiPartFormDataRepresentation.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java index 88f93f1ec8..accd793721 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Path; import org.eclipse.jetty.http.MultiPart; import org.eclipse.jetty.http.MultiPart.Part; @@ -89,14 +90,16 @@ public MultiPartFormDataRepresentation(Parts parts) { * Constructor that parses the content based on a given configuration into * {@link #getParts()}. Uses a default {@link MultiPartConfig}. * - * @param content The multipart entity to parse which should have a media - * type based on {@link MediaType#MULTIPART_FORM_DATA}, with - * a "boundary" parameter. + * @param content The multipart entity to parse which should have a media + * type based on {@link MediaType#MULTIPART_FORM_DATA}, with + * a "boundary" parameter. + * @param location The location where parsed files are stored for easier + * access. * @throws IOException */ - public MultiPartFormDataRepresentation(Representation content) - throws IOException { - this(content, new MultiPartConfig.Builder().build()); + public MultiPartFormDataRepresentation(Representation content, + Path location) throws IOException { + this(content, new MultiPartConfig.Builder().location(location).build()); } /** From b24ec678bd40e5bedc817cb64f29187e063e84ba Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Sun, 16 Mar 2025 12:31:28 +0100 Subject: [PATCH 06/21] HttpBasic test refacto --- .../engine/security/HttpBasicHelper.java | 240 +++++----- .../restlet/security/HttpBasicTestCase.java | 410 +++++++----------- 2 files changed, 277 insertions(+), 373 deletions(-) diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java index 6637751bc9..e34b27eb44 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java @@ -41,147 +41,131 @@ public HttpBasicHelper() { @Override public void formatRequest(ChallengeWriter cw, ChallengeRequest challenge, Response response, Series
httpHeaders) throws IOException { - String realm = challenge.getRealm(); - String charset = challenge.getParameters().getFirstValue("charset"); - - if (realm != null) { - cw.appendQuotedChallengeParameter("realm", realm); - } else { - getLogger().warning( - "The realm directive is required for all authentication schemes that issue a challenge."); - } - - if (charset != null) { - if ("UTF-8".equalsIgnoreCase(charset)) { - cw.appendQuotedChallengeParameter("charset", "UTF-8"); - } else { - getLogger().warning( - "The \"charset\" parameter must be \"UTF-8\" per RFC 7617."); - } - } + String realm = challenge.getRealm(); + String charset = challenge.getParameters().getFirstValue("charset"); + + if (realm != null) { + cw.appendQuotedChallengeParameter("realm", realm); + } else { + getLogger() + .warning("The realm directive is required for all authentication schemes that issue a challenge."); + } + + if (charset != null) { + if ("UTF-8".equalsIgnoreCase(charset)) { + cw.appendQuotedChallengeParameter("charset", "UTF-8"); + } else { + getLogger().warning("The \"charset\" parameter must be \"UTF-8\" per RFC 7617."); + } + } } @Override public void formatResponse(ChallengeWriter cw, ChallengeResponse challenge, Request request, Series
httpHeaders) { - try { - if (challenge == null) { - throw new RuntimeException( - "No challenge provided, unable to encode credentials"); - } else { - String charset = challenge.getParameters() - .getFirstValue("charset"); - - if (charset != null) { - if ("UTF-8".equalsIgnoreCase(charset)) { - charset = "UTF-8"; - } else { - getLogger().warning( - "The \"charset\" parameter must be \"UTF-8\" per RFC 7617. Using \"ISO-8859-1\" instead."); - charset = "ISO-8859-1"; - } - }else { - charset = "ISO-8859-1"; - } - - CharArrayWriter credentials = new CharArrayWriter(); - credentials.write(challenge.getIdentifier()); - credentials.write(":"); - credentials.write(challenge.getSecret()); - cw.append(Base64.getEncoder().encodeToString(IoUtils - .toByteArray(credentials.toCharArray(), charset))); - } - } catch (UnsupportedEncodingException e) { - throw new RuntimeException( - "Unsupported encoding, unable to encode credentials"); - } catch (IOException e) { - throw new RuntimeException( - "Unexpected exception, unable to encode credentials", e); - } + try { + if (challenge == null) { + throw new RuntimeException("No challenge provided, unable to encode credentials"); + } else { + String charset = challenge.getParameters().getFirstValue("charset"); + + if (charset != null) { + if ("UTF-8".equalsIgnoreCase(charset)) { + charset = "UTF-8"; + } else { + getLogger().warning( + "The \"charset\" parameter must be \"UTF-8\" per RFC 7617. Using \"ISO-8859-1\" instead."); + charset = "ISO-8859-1"; + } + } else { + charset = "ISO-8859-1"; + } + + CharArrayWriter credentials = new CharArrayWriter(); + credentials.write(challenge.getIdentifier()); + credentials.write(":"); + credentials.write(challenge.getSecret()); + cw.append(Base64.getEncoder().encodeToString(IoUtils.toByteArray(credentials.toCharArray(), charset))); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unsupported encoding, unable to encode credentials"); + } catch (IOException e) { + throw new RuntimeException("Unexpected exception, unable to encode credentials", e); + } } @Override public void parseRequest(ChallengeRequest challenge, Response response, Series
httpHeaders) { - if (challenge.getRawValue() != null) { - HeaderReader hr = new HeaderReader( - challenge.getRawValue()); - - try { - Parameter param = hr.readParameter(); - - while (param != null) { - try { - if ("realm".equals(param.getName())) { - challenge.setRealm(param.getValue()); - } else { - challenge.getParameters().add(param); - } - - if (hr.skipValueSeparator()) { - param = hr.readParameter(); - } else { - param = null; - } - } catch (Exception e) { - Context.getCurrentLogger().log(Level.WARNING, - "Unable to parse the challenge request header parameter", - e); - } - } - } catch (Exception e) { - Context.getCurrentLogger().log(Level.WARNING, - "Unable to parse the challenge request header parameter", - e); - } - } + if (challenge.getRawValue() != null) { + HeaderReader hr = new HeaderReader(challenge.getRawValue()); + + try { + Parameter param = hr.readParameter(); + + while (param != null) { + try { + if ("realm".equals(param.getName())) { + challenge.setRealm(param.getValue()); + } else { + challenge.getParameters().add(param); + } + + if (hr.skipValueSeparator()) { + param = hr.readParameter(); + } else { + param = null; + } + } catch (Exception e) { + Context.getCurrentLogger().log(Level.WARNING, + "Unable to parse the challenge request header parameter", e); + } + } + } catch (Exception e) { + Context.getCurrentLogger().log(Level.WARNING, "Unable to parse the challenge request header parameter", + e); + } + } } @Override public void parseResponse(ChallengeResponse challenge, Request request, Series
httpHeaders) { - try { - String charset = challenge.getParameters() - .getFirstValue("charset"); - - if (charset != null) { - if ("UTF-8".equalsIgnoreCase(charset)) { - charset = "UTF-8"; - } else { - getLogger().warning( - "The \"charset\" parameter must be \"UTF-8\" per RFC 7617. Using \"ISO-8859-1\" instead."); - charset = "ISO-8859-1"; - } - }else { - charset = "ISO-8859-1"; - } - - byte[] credentialsEncoded = Base64.getDecoder() - .decode(challenge.getRawValue()); - - if (credentialsEncoded == null) { - getLogger().info("Cannot decode credentials: " - + challenge.getRawValue()); - } - - String credentials = new String(credentialsEncoded, charset); - int separator = credentials.indexOf(':'); - - if (separator == -1) { - // Log the blocking - getLogger().info("Invalid credentials given by client with IP: " - + ((request != null) - ? request.getClientInfo().getAddress() - : "?")); - } else { - challenge.setIdentifier(credentials.substring(0, separator)); - challenge.setSecret(credentials.substring(separator + 1)); - } - } catch (UnsupportedEncodingException e) { - getLogger().log(Level.INFO, "Unsupported HTTP Basic encoding error", - e); - } catch (IllegalArgumentException e) { - getLogger().log(Level.INFO, - "Unable to decode the HTTP Basic credential", e); - } + try { + String charset = challenge.getParameters().getFirstValue("charset"); + + if (charset != null) { + if ("UTF-8".equalsIgnoreCase(charset)) { + charset = "UTF-8"; + } else { + getLogger().warning( + "The \"charset\" parameter must be \"UTF-8\" per RFC 7617. Using \"ISO-8859-1\" instead."); + charset = "ISO-8859-1"; + } + } else { + charset = "ISO-8859-1"; + } + + byte[] credentialsEncoded = Base64.getDecoder().decode(challenge.getRawValue()); + + if (credentialsEncoded == null) { + getLogger().info("Cannot decode credentials: " + challenge.getRawValue()); + } + + String credentials = new String(credentialsEncoded, charset); + int separator = credentials.indexOf(':'); + + if (separator == -1) { + // Log the blocking + getLogger().info("Invalid credentials given by client with IP: " + + ((request != null) ? request.getClientInfo().getAddress() : "?")); + } else { + challenge.setIdentifier(credentials.substring(0, separator)); + challenge.setSecret(credentials.substring(separator + 1)); + } + } catch (UnsupportedEncodingException e) { + getLogger().log(Level.INFO, "Unsupported HTTP Basic encoding error", e); + } catch (IllegalArgumentException e) { + getLogger().log(Level.INFO, "Unable to decode the HTTP Basic credential", e); + } } } diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java index 6d886907c4..6b1d72f9e3 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java @@ -9,15 +9,34 @@ package org.restlet.security; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.restlet.*; -import org.restlet.data.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.restlet.Application; +import org.restlet.Client; +import org.restlet.Component; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +import org.restlet.Server; +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; +import org.restlet.data.MediaType; +import org.restlet.data.Method; +import org.restlet.data.Protocol; +import org.restlet.data.Status; +import org.restlet.engine.io.IoUtils; /** * Restlet unit tests for HTTP Basic authentication client/server. @@ -27,244 +46,145 @@ */ public class HttpBasicTestCase { - public static class AuthenticatedRestlet extends Restlet { - @Override - public void handle(Request request, Response response) { - response.setEntity(AUTHENTICATED_MSG, MediaType.TEXT_PLAIN); - } - } - - public static class TestVerifier extends MapVerifier { - public TestVerifier() { - getLocalSecrets().put(SHORT_USERNAME, SHORT_PASSWORD.toCharArray()); - getLocalSecrets().put(LONG_USERNAME, LONG_PASSWORD.toCharArray()); - } - - @Override - public int verify(String identifier, char[] inputSecret) { - // NOTE: Allocating Strings are not really secure treatment of passwords - String almostSecret = new String(inputSecret); - - try { - return super.verify(identifier, inputSecret); - } finally { - // Clear secret from memory as soon as possible (This is better - // treatment, but useless due to our almostSecret - // copy) - Arrays.fill(inputSecret, '\000'); - } - } - } - - public static final String AUTHENTICATED_MSG = "You are authenticated"; - - public static final String LONG_PASSWORD = "thisLongPasswordIsExtremelySecure"; - - public static final String LONG_USERNAME = "aVeryLongUsernameIsIndeedRequiredForThisTest"; - - public static final String SHORT_PASSWORD = "pw15"; - - public static final String SHORT_USERNAME = "user13"; - - public static final String WRONG_USERNAME = "wrongUser"; - - private ChallengeAuthenticator authenticator; - - private Component component; - - private String uri; - - private MapVerifier verifier; - - @Test - public void guardLong() { - assertEquals( - Verifier.RESULT_VALID, - this.verifier.verify(LONG_USERNAME, LONG_PASSWORD.toCharArray()), - "Didn't authenticate short user/pwd" - ); - } - - @Test - public void guardLongWrong() { - assertEquals( - Verifier.RESULT_INVALID, - this.verifier.verify(LONG_USERNAME, SHORT_PASSWORD.toCharArray()), - "Authenticated long username with wrong password" - ); - } - - // Test our guard.checkSecret() stand-alone - @Test - public void guardShort() { - assertEquals( - Verifier.RESULT_VALID, - this.verifier.verify(SHORT_USERNAME, SHORT_PASSWORD.toCharArray()), - "Didn't authenticate short user/pwd" - ); - } - - @Test - public void guardShortWrong() { - assertEquals( - Verifier.RESULT_INVALID, - this.verifier.verify(SHORT_USERNAME, LONG_PASSWORD.toCharArray()), - "Authenticated short username with wrong password" - ); - } - - @Test - public void guardWrongUser() { - assertEquals( - Verifier.RESULT_INVALID, - this.verifier.verify(WRONG_USERNAME, SHORT_PASSWORD.toCharArray()), - "Authenticated wrong username" - ); - } - - public void HttpBasicLong() throws Exception { - Request request = new Request(Method.GET, this.uri); - Client client = new Client(Protocol.HTTP); - - ChallengeResponse authentication = new ChallengeResponse( - ChallengeScheme.HTTP_BASIC, LONG_USERNAME, LONG_PASSWORD); - request.setChallengeResponse(authentication); - - final Response response = client.handle(request); - assertEquals( - Status.SUCCESS_OK, response.getStatus(), - "Long username did not return 200 OK" - ); - assertEquals(AUTHENTICATED_MSG, response.getEntity().getText()); - - client.stop(); - } - - public void HttpBasicLongWrong() throws Exception { - final Request request = new Request(Method.GET, this.uri); - final Client client = new Client(Protocol.HTTP); - - final ChallengeResponse authentication = new ChallengeResponse( - ChallengeScheme.HTTP_BASIC, LONG_USERNAME, SHORT_PASSWORD); - request.setChallengeResponse(authentication); - - final Response response = client.handle(request); - - assertEquals( - Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus(), - "Long username w/wrong pw did not throw 403" - ); - - client.stop(); - } - - // Test various HTTP Basic auth connections - public void HttpBasicNone() throws Exception { - final Request request = new Request(Method.GET, this.uri); - final Client client = new Client(Protocol.HTTP); - final Response response = client.handle(request); - assertEquals( - Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus(), - "No user did not throw 401" - ); - client.stop(); - } - - public void HttpBasicShort() throws Exception { - final Request request = new Request(Method.GET, this.uri); - final Client client = new Client(Protocol.HTTP); - - final ChallengeResponse authentication = new ChallengeResponse( - ChallengeScheme.HTTP_BASIC, SHORT_USERNAME, SHORT_PASSWORD); - request.setChallengeResponse(authentication); - - final Response response = client.handle(request); - assertEquals( - Status.SUCCESS_OK, response.getStatus(), - "Short username did not return 200 OK" - ); - assertEquals(AUTHENTICATED_MSG, response.getEntity().getText()); - - client.stop(); - } - - public void HttpBasicShortWrong() throws Exception { - final Request request = new Request(Method.GET, this.uri); - final Client client = new Client(Protocol.HTTP); - - final ChallengeResponse authentication = new ChallengeResponse( - ChallengeScheme.HTTP_BASIC, SHORT_USERNAME, LONG_PASSWORD); - request.setChallengeResponse(authentication); - - final Response response = client.handle(request); - - assertEquals( - Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus(), - "Short username did not throw 401" - ); - - client.stop(); - } - - public void HttpBasicWrongUser() throws Exception { - final Request request = new Request(Method.GET, this.uri); - final Client client = new Client(Protocol.HTTP); - - final ChallengeResponse authentication = new ChallengeResponse( - ChallengeScheme.HTTP_BASIC, WRONG_USERNAME, SHORT_PASSWORD); - request.setChallengeResponse(authentication); - - final Response response = client.handle(request); - - assertEquals( - Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus(), - "Wrong username did not throw 401" - ); - - client.stop(); - } - - @BeforeEach - public void makeServer() throws Exception { - this.component = new Component(); - final Server server = this.component.getServers().add(Protocol.HTTP, 0); - - final Application application = new Application() { - @Override - public Restlet createInboundRoot() { - HttpBasicTestCase.this.verifier = new TestVerifier(); - HttpBasicTestCase.this.authenticator = new ChallengeAuthenticator( - getContext(), ChallengeScheme.HTTP_BASIC, - HttpBasicTestCase.class.getSimpleName()); - HttpBasicTestCase.this.authenticator - .setVerifier(HttpBasicTestCase.this.verifier); - HttpBasicTestCase.this.authenticator - .setNext(new AuthenticatedRestlet()); - return HttpBasicTestCase.this.authenticator; - } - }; - - this.component.getDefaultHost().attach(application); - this.component.start(); - this.uri = "http://localhost:" + server.getActualPort() + "/"; - } - - @AfterEach - public void stopServer() throws Exception { - if (this.component.isStarted()) { - this.component.stop(); - } - this.component = null; - } - - @Test - public void testHttpBasic() throws Exception { - HttpBasicWrongUser(); - HttpBasicShort(); - HttpBasicShortWrong(); - HttpBasicNone(); - HttpBasicLong(); - HttpBasicLongWrong(); - } + public static class AuthenticatedRestlet extends Restlet { + @Override + public void handle(Request request, Response response) { + response.setEntity(AUTHENTICATED_MSG, MediaType.TEXT_PLAIN); + } + } + + public static class TestVerifier extends MapVerifier { + public TestVerifier() { + getLocalSecrets().put(SHORT_USERNAME, SHORT_PASSWORD.toCharArray()); + getLocalSecrets().put(LONG_USERNAME, LONG_PASSWORD.toCharArray()); + } + + @Override + public int verify(String identifier, char[] inputSecret) { + // NOTE: Allocating Strings are not really secure treatment of passwords + String almostSecret = new String(inputSecret); + + try { + return super.verify(identifier, inputSecret); + } finally { + // Clear secret from memory as soon as possible (This is better + // treatment, but useless due to our almostSecret copy) + Arrays.fill(inputSecret, '\000'); + } + } + } + + public static class BlockerVerifier implements Verifier { + @Override + public int verify(Request request, Response response) { + return RESULT_INVALID; + } + } + + public static final String AUTHENTICATED_MSG = "You are authenticated"; + + public static final String LONG_PASSWORD = "thisLongPasswordIsExtremelySecure"; + + public static final String LONG_USERNAME = "aVeryLongUsernameIsIndeedRequiredForThisTest"; + + public static final String SHORT_PASSWORD = "pw15"; + + public static final String SHORT_USERNAME = "user13"; + + public static final String WRONG_USERNAME = "wrongUser"; + + static Stream invalidCredentials() { + return Stream.of(arguments(LONG_USERNAME, SHORT_PASSWORD), arguments(SHORT_USERNAME, LONG_PASSWORD), + arguments(WRONG_USERNAME, SHORT_PASSWORD)); + } + + static Stream validCredentials() { + return Stream.of(arguments(LONG_USERNAME, LONG_PASSWORD), arguments(SHORT_USERNAME, SHORT_PASSWORD)); + } + + @Nested + class TestMapVerifier { + + private final MapVerifier verifier = new TestVerifier(); + + @ParameterizedTest + @MethodSource("org.restlet.security.HttpBasicTestCase#invalidCredentials") + void testInvalidCredentials(final String login, final String password) { + assertEquals(Verifier.RESULT_INVALID, this.verifier.verify(login, password.toCharArray())); + } + + @ParameterizedTest + @MethodSource("org.restlet.security.HttpBasicTestCase#validCredentials") + void testValidCredentials(final String login, final String password) { + assertEquals(Verifier.RESULT_VALID, this.verifier.verify(login, password.toCharArray())); + } + + } + + @Nested + class TestHttpBasicServer { + private Component component; + private Request request; + private Client client; + + @Test + public void HttpBasicNone() throws Exception { + final Response response = client.handle(request); + assertEquals(Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus()); + } + + @ParameterizedTest + @MethodSource("org.restlet.security.HttpBasicTestCase#invalidCredentials") + void testInvalidCredentials(final String login, final String password) throws Exception { + ChallengeResponse authentication = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, login, password); + request.setChallengeResponse(authentication); + + final Response response = client.handle(request); + assertEquals(Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus()); + } + + @ParameterizedTest + @MethodSource("org.restlet.security.HttpBasicTestCase#validCredentials") + void testValidCredentials(final String login, final String password) throws Exception { + ChallengeResponse authentication = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, login, password); + request.setChallengeResponse(authentication); + + final Response response = client.handle(request); + assertEquals(Status.SUCCESS_OK, response.getStatus()); + assertEquals(AUTHENTICATED_MSG, response.getEntity().getText()); + } + + @BeforeEach + public void makeServer() throws Exception { + final String REALM = HttpBasicTestCase.class.getSimpleName(); + this.component = new Component(); + final Server server = this.component.getServers().add(Protocol.HTTP, 0); + + final Application application = new Application() { + @Override + public Restlet createInboundRoot() { + ChallengeAuthenticator authenticator = new ChallengeAuthenticator(getContext(), + ChallengeScheme.HTTP_BASIC, REALM); + authenticator.setVerifier(new TestVerifier()); + authenticator.setNext(new AuthenticatedRestlet()); + return authenticator; + } + }; + + this.component.getDefaultHost().attach(application); + this.component.start(); + request = new Request(Method.GET, "http://localhost:" + server.getActualPort()); + client = new Client(Protocol.HTTP); + } + + @AfterEach + public void cleanup() throws Exception { + client.stop(); + if (this.component.isStarted()) { + this.component.stop(); + } + this.component = null; + } + } } From d7179ed5e408f32ec33f46bd8bc9f385a9993898 Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Sun, 16 Mar 2025 12:43:04 +0100 Subject: [PATCH 07/21] HttpBasicHelper: fix potential NPE --- .../org/restlet/engine/security/HttpBasicHelper.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java index e34b27eb44..4a884e54ca 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Base64; +import java.util.Objects; import java.util.logging.Level; /** @@ -129,6 +130,11 @@ public void parseRequest(ChallengeRequest challenge, Response response, Series httpHeaders) { + if (challenge.getRawValue() == null) { + getLogger().info("Cannot decode credentials: " + challenge.getRawValue()); + return; + } + try { String charset = challenge.getParameters().getFirstValue("charset"); @@ -146,10 +152,6 @@ public void parseResponse(ChallengeResponse challenge, Request request, Series Date: Sun, 16 Mar 2025 12:54:10 +0100 Subject: [PATCH 08/21] HttpBasicHelper: fix potential NPE --- .../src/test/java/org/restlet/security/HttpBasicTestCase.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java index 6b1d72f9e3..328c2ab542 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/security/HttpBasicTestCase.java @@ -9,11 +9,10 @@ package org.restlet.security; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.util.Arrays; -import java.util.Base64; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -36,7 +35,6 @@ import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.data.Status; -import org.restlet.engine.io.IoUtils; /** * Restlet unit tests for HTTP Basic authentication client/server. From fef1ec95d2278ca605138861f083a4e4ee06093b Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Sun, 16 Mar 2025 14:21:42 +0100 Subject: [PATCH 09/21] Added tests cases for multipart --- .../MultiPartFormDataRepresentation.java | 378 ++++++++++-------- .../ext/jetty/MultiPartFormTestCase.java | 155 +++---- .../restlet/engine/header/ContentType.java | 23 +- 3 files changed, 273 insertions(+), 283 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java index accd793721..5853f42a7c 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -13,6 +13,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; +import java.util.*; import org.eclipse.jetty.http.MultiPart; import org.eclipse.jetty.http.MultiPart.Part; @@ -36,181 +37,206 @@ */ public class MultiPartFormDataRepresentation extends InputRepresentation { - /** - * Adds a randomly generated boundary to the media type parameters. - * - * @param mediaType The media type to update. - * @return The updated media type. - */ - public static MediaType addBoundary(MediaType mediaType) { - String boundary = MultiPart.generateBoundary(null, 24); - mediaType.getParameters().add("boundary", boundary); - return mediaType; - } - - /** - * Returns the value of the first mediatype parameter with "boundary" name. - * - * @param mediaType The media type that might contain a "boundary" - * parameter. - * @return The value of the first mediatype parameter with "boundary" name. - */ - public static String getBoundary(MediaType mediaType) { - String result = null; - - if (mediaType != null) { - mediaType.getParameters().getFirstValue("boundary"); - } - - return result; - } - - /** - * The boundary used to separate each part for the parsed or generated form. - */ - private volatile String boundary; - - /** The wrapped multipart form data either parsed or to be generated. */ - private volatile Parts parts; - - /** - * Constructor that wraps multiple parts and generates the content via - * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param parts The source parts to use when generating the representation. - */ - public MultiPartFormDataRepresentation(Parts parts) { - super(null, MediaType.MULTIPART_FORM_DATA); - this.boundary = getMediaType().getParameters() - .getFirstValue("boundary"); - this.parts = parts; - } - - /** - * Constructor that parses the content based on a given configuration into - * {@link #getParts()}. Uses a default {@link MultiPartConfig}. - * - * @param content The multipart entity to parse which should have a media - * type based on {@link MediaType#MULTIPART_FORM_DATA}, with - * a "boundary" parameter. - * @param location The location where parsed files are stored for easier - * access. - * @throws IOException - */ - public MultiPartFormDataRepresentation(Representation content, - Path location) throws IOException { - this(content, new MultiPartConfig.Builder().location(location).build()); - } - - /** - * Constructor that parses the content based on a given configuration into - * {@link #getParts()}. - * - * @param content The multipart entity to parse which should have a media - * type based on {@link MediaType#MULTIPART_FORM_DATA}, with - * a "boundary" parameter. - * @param config The multipart configuration. - * @throws IOException - */ - public MultiPartFormDataRepresentation(Representation content, - MultiPartConfig config) throws IOException { - this(ContentType.writeHeader(content), content.getStream(), config); - } - - /** - * Constructor that parses the content based on a given configuration into - * {@link #getParts()}. - * - * @param contentType The media type that should be based on - * {@link MediaType#MULTIPART_FORM_DATA}, with a - * "boundary" parameter. - * @param content The multipart entity to parse. - * @param config The multipart configuration. - * @throws IOException - */ - public MultiPartFormDataRepresentation(String contentType, - InputStream content, MultiPartConfig config) throws IOException { - super(null, MediaType.MULTIPART_FORM_DATA); - this.boundary = boundary; - - if (content != null) { - Content.Source contentSource = new InputStreamContentSource( - content); - Attributes.Mapped attributes = new Attributes.Mapped(); - - // Convert the request content into parts. - MultiPartFormData.onParts(contentSource, attributes, contentType, - config, new Promise.Invocable<>() { - @Override - public void failed(Throwable failure) { - throw new IllegalStateException( - "Unable to parse the multipart form data representation", - failure); - } - - @Override - public InvocationType getInvocationType() { - return InvocationType.BLOCKING; - } - - @Override - public void succeeded(MultiPartFormData.Parts parts) { - // Store the resulting parts - MultiPartFormDataRepresentation.this.parts = parts; - } - }); - } - } - - /** - * Returns the boundary used to separate each part for the parsed or - * generated form. - * - * @return The boundary used to separate each part for the parsed or - * generated form. - */ - public String getBoundary() { - return boundary; - } - - /** - * Returns the wrapped multipart form data either parsed or to be generated. - * - * @return The wrapped multipart form data either parsed or to be generated. - */ - public Parts getParts() { - return parts; - } - - /** - * Returns an input stream that generates the multipart form data - * serialization for the wrapped {@link #getParts()} object. - * - * @return An input stream that generates the multipart form data. - */ - @Override - public InputStream getStream() throws IOException { - MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource( - getBoundary()); - - for (Part part : this.parts) { - content.addPart(part); - } - - content.close(); - setStream(null); - return Content.Source.asInputStream(content); - } - - /** - * Sets the boundary used to separate each part for the parsed or generated - * form. - * - * @param boundary The boundary used to separate each part for the parsed or - * generated form. - */ - public void setBoundary(String boundary) { - this.boundary = boundary; - } + /** + * Adds a randomly generated boundary to the media type parameters. + * + * @param mediaType The media type to update. + * @return The updated media type. + */ + public static MediaType addBoundary(MediaType mediaType) { + // TODO should we duplicate the mediaType, in order to preserve the static + // constants defined in MediaType class? + String boundary = MultiPart.generateBoundary(null, 24); + mediaType.getParameters().add("boundary", boundary); + return mediaType; + } + + /** + * Returns the value of the first mediatype parameter with "boundary" name. + * + * @param mediaType The media type that might contain a "boundary" parameter. + * @return The value of the first mediatype parameter with "boundary" name. + */ + public static String getBoundary(MediaType mediaType) { + final String result; + + if (mediaType != null) { + result = mediaType.getParameters().getFirstValue("boundary"); + } else { + result = null; + } + + return result; + } + + /** + * The boundary used to separate each part for the parsed or generated form. + */ + private volatile String boundary; + + /** The wrapped multipart form data either parsed or to be generated. */ + private volatile List parts; + + /** + * Constructor that wraps multiple parts and generates the content via + * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param parts The source parts to use when generating the representation. + */ + public MultiPartFormDataRepresentation(Part... parts) { + super(null, MediaType.MULTIPART_FORM_DATA); + this.boundary = getMediaType().getParameters().getFirstValue("boundary"); + this.parts = Arrays.asList(parts); + } + + // TODO Should we support such constructor? + /** + * Constructor that wraps multiple parts and generates the content via + * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param representations The source parts to use when generating the + * representation. + */ + public MultiPartFormDataRepresentation(Representation... representations) { + this(MultiPart.generateBoundary(null, 24), representations); // TODO should we generate a random boundary? + } + + // TODO Should we support such constructor? + /** + * Constructor that wraps multiple parts and generates the content via + * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param representations The source parts to use when generating the + * representation. + */ + public MultiPartFormDataRepresentation(final String boundary, final Representation... representations) { + super(null, MediaType.MULTIPART_FORM_DATA); + + final String b = Objects.requireNonNullElse(boundary, getBoundary(getMediaType())); + this.boundary = Objects.requireNonNullElse(b, MultiPart.generateBoundary(null, 24)); // TODO should we generate + // a random boundary? + + this.parts = parts; + } + + /** + * Constructor that parses the content based on a given configuration into + * {@link #getParts()}. Uses a default {@link MultiPartConfig}. + * + * @param content The multipart entity to parse which should have a media type + * based on {@link MediaType#MULTIPART_FORM_DATA}, with a + * "boundary" parameter. + * @param location The location where parsed files are stored for easier access. + * @throws IOException + */ + public MultiPartFormDataRepresentation(Representation content, Path location) throws IOException { + this(content, new MultiPartConfig.Builder().location(location).build()); + } + + /** + * Constructor that parses the content based on a given configuration into + * {@link #getParts()}. + * + * @param content The multipart entity to parse which should have a media type + * based on {@link MediaType#MULTIPART_FORM_DATA}, with a + * "boundary" parameter. + * @param config The multipart configuration. + * @throws IOException + */ + public MultiPartFormDataRepresentation(Representation content, MultiPartConfig config) throws IOException { + this(ContentType.writeHeader(content), content.getStream(), config); + } + + /** + * Constructor that parses the content based on a given configuration into + * {@link #getParts()}. + * + * @param contentType The media type that should be based on + * {@link MediaType#MULTIPART_FORM_DATA}, with a "boundary" + * parameter. + * @param content The multipart entity to parse. + * @param config The multipart configuration. + * @throws IOException + */ + public MultiPartFormDataRepresentation(String contentType, InputStream content, MultiPartConfig config) + throws IOException { + super(null, MediaType.MULTIPART_FORM_DATA); + this.boundary = boundary; + + if (content != null) { + Content.Source contentSource = Content.Source.from(content); + Attributes.Mapped attributes = new Attributes.Mapped(); + + // Convert the request content into parts. + MultiPartFormData.onParts(contentSource, attributes, contentType, config, new Promise.Invocable<>() { + @Override + public void failed(Throwable failure) { + throw new IllegalStateException("Unable to parse the multipart form data representation", failure); + } + + @Override + public InvocationType getInvocationType() { + return InvocationType.BLOCKING; + } + + @Override + public void succeeded(MultiPartFormData.Parts parts) { + // Store the resulting parts + MultiPartFormDataRepresentation.this.parts = new ArrayList<>(); + parts.iterator().forEachRemaining(part -> MultiPartFormDataRepresentation.this.parts.add(part)); + } + }); + } + } + + /** + * Returns the boundary used to separate each part for the parsed or generated + * form. + * + * @return The boundary used to separate each part for the parsed or generated + * form. + */ + public String getBoundary() { + return boundary; + } + + /** + * Returns the wrapped multipart form data either parsed or to be generated. + * + * @return The wrapped multipart form data either parsed or to be generated. + */ + public List getParts() { + return parts; + } + + /** + * Returns an input stream that generates the multipart form data serialization + * for the wrapped {@link #getParts()} object. + * + * @return An input stream that generates the multipart form data. + */ + @Override + public InputStream getStream() throws IOException { + MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource(getBoundary()); + + for (Part part : this.parts) { + content.addPart(part); + } + + content.close(); + setStream(null); + return Content.Source.asInputStream(content); + } + + /** + * Sets the boundary used to separate each part for the parsed or generated + * form. + * + * @param boundary The boundary used to separate each part for the parsed or + * generated form. + */ + public void setBoundary(String boundary) { + this.boundary = boundary; + } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java index f2f12ccba0..a920be324f 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java @@ -9,111 +9,68 @@ package org.restlet.ext.jetty; +import org.eclipse.jetty.client.StringRequestContent; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.MultiPart; import org.junit.jupiter.api.Test; +import org.restlet.data.MediaType; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Test case for the {@link FormDataSet} class in multipart mode. + * Test case for the {@link MultiPartFormDataRepresentation} class in multipart + * mode. * * @author Jerome Louvel */ public class MultiPartFormTestCase { - /* - TODO restore test about support of multi part representations. - - - @Test - public void testWrite() throws IOException { - - // considered as a simple field entry - Representation textFile = new EmptyRepresentation(); - textFile.setMediaType(MediaType.TEXT_PLAIN); - - // considered as a file - Representation textFile2 = new StringRepresentation("test", - MediaType.TEXT_PLAIN); - textFile2.setDisposition(new Disposition()); - textFile2.getDisposition().setFilename("test.txt"); - - // considered as a file - Representation file = new EmptyRepresentation(); - file.setMediaType(MediaType.APPLICATION_OCTET_STREAM); - - String boundary = "-----------------------------1294919323195"; - String boundaryBis = "--" + boundary; - String expected; - - FormDataSet form = new FormDataSet(boundary); - form.getEntries().add(new FormData("number", "5555555555")); - form.getEntries().add(new FormData("clip", "rickroll")); - form.getEntries().add(new FormData("upload_file", file)); - form.getEntries().add(new FormData("upload_textfile", textFile)); - form.getEntries().add(new FormData("upload_textfile2", textFile2)); - form.getEntries().add(new FormData("tos", "agree")); - - expected = boundaryBis - + "\r\n" - + "Content-Disposition: form-data; name=\"number\"\r\n" - + "\r\n" - + "5555555555\r\n" - + boundaryBis - + "\r\n" - + "Content-Disposition: form-data; name=\"clip\"\r\n" - + "\r\n" - + "rickroll\r\n" - + boundaryBis - + "\r\n" - + "Content-Disposition: form-data; name=\"upload_file\"; filename=\"\"\r\n" - + "Content-Type: application/octet-stream\r\n" - + "\r\n" - + "\r\n" - + boundaryBis - + "\r\n" - + "Content-Disposition: form-data; name=\"upload_textfile\"\r\n" - + "\r\n" - + "\r\n" - + boundaryBis - + "\r\n" - + "Content-Disposition: form-data; name=\"upload_textfile2\"; filename=\"test.txt\"\r\n" - + "Content-Type: text/plain; charset=UTF-8\r\n" + "\r\n" - + "test" + "\r\n" + boundaryBis + "\r\n" - + "Content-Disposition: form-data; name=\"tos\"\r\n" + "\r\n" - + "agree\r\n" + boundaryBis + "--\r\n"; - assertEquals(expected, form.getText()); - } - */ - - /** - * Tests the multipart content-type. - */ - @Test - public void testContentType() { - /* -TODO restore test of Form class - FormDataSet form = null; - - form = new FormDataSet(); - form.setMultipart(true); - assertTrue(form.getMediaType().equals(MediaType.MULTIPART_FORM_DATA, - true)); - - form = new FormDataSet("test"); - assertTrue(form.isMultipart()); - assertTrue(form.getMediaType().equals(MediaType.MULTIPART_FORM_DATA, - true)); - assertEquals( - form.getMediaType().getParameters().getFirstValue("boundary"), - "test"); - form = new FormDataSet(); - - form.setMultipartBoundary("test2"); - assertTrue(form.isMultipart()); - assertTrue(form.getMediaType().equals(MediaType.MULTIPART_FORM_DATA, - true)); - assertEquals( - form.getMediaType().getParameters().getFirstValue("boundary"), - "test2"); - - */ - } + @Test + public void testWriteFromParts() throws IOException { + Path textFilePath = Files.createTempFile("multiPart", ""); + Files.write(textFilePath, "this is the content of the file".getBytes(StandardCharsets.UTF_8)); + MultiPart.PathPart filePart = new MultiPart.PathPart("icon", "text.txt", HttpFields.EMPTY, textFilePath); + + MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart("field", null, HttpFields.EMPTY, + new StringRequestContent("foo")); + + final String boundary = "-----------------------------1294919323195"; + + MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation(contentSourcePart, filePart); + rep.setBoundary(boundary); + + final String expected = """ + --%s\r + Content-Disposition: form-data; name="field"\r + \r + foo\r + --%s\r + Content-Disposition: form-data; name="icon"; filename="text.txt"\r + \r + this is the content of the file\r + --%s--\r + """.replace("%s", boundary); + assertEquals(expected, rep.getText()); + } + + /** + * Tests the multipart content-type. + */ + @Test + public void testContentType() { + MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart("field", null, HttpFields.EMPTY, + new StringRequestContent("foo")); + MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation(contentSourcePart); + rep.setBoundary("myBoundary"); + assertEquals(MediaType.MULTIPART_FORM_DATA, rep.getMediaType()); + + // Is this test really correct? + // assertEquals("myBoundary", + // rep.getMediaType().getParameters().getFirstValue("boundary")); + } } diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java index 6fe0387ab3..8d8737fec5 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/header/ContentType.java @@ -51,22 +51,29 @@ public static MediaType readMediaType(String contentType) { * @return The HTTP "Content-Type" header. */ public static String writeHeader(MediaType mediaType, CharacterSet characterSet) { - String result = mediaType.toString(); + StringBuilder result = new StringBuilder(mediaType.toString()); // Specify the character set parameter if required + // TODO I wonder if the given parameter "characterSet" overrides the mediaType's charset'? + /* if ((mediaType.getParameters().getFirstValue("charset") == null) && (characterSet != null)) { result = result + "; charset=" + characterSet.getName(); } - + */ + for (Parameter param : mediaType.getParameters()) { - if ((param != null) && !param.getName().equals("charset")) { - result = result + "; " + param.getName() + "=" - + param.getValue(); - } + if (param == null) { + continue; + } + if (characterSet != null && param.getName().equals("charset")) { + // TODO I wonder if the given parameter "characterSet" overrides the mediaType's charset'? + result.append("; ").append(param.getName()).append("=").append(characterSet); + } else { + result.append("; ").append(param.getName()).append("=").append(param.getValue()); + } } - return result; - + return result.toString(); } /** From 9f6bca1ee02f4001dbc0ff6edece653f3d9fde7e Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Sun, 16 Mar 2025 11:58:52 -0400 Subject: [PATCH 10/21] Added logic to duplicate MediaType along with the addition of parameter --- changes.md | 1 + .../MultiPartFormDataRepresentation.java | 8 +---- .../main/java/org/restlet/data/MediaType.java | 32 +++++++++++++++++++ .../main/java/org/restlet/data/Parameter.java | 14 ++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/changes.md b/changes.md index 114da7fa7e..f96f8f40d8 100644 --- a/changes.md +++ b/changes.md @@ -5,6 +5,7 @@ Changes log - Enhancements - Added MultiPartFormDataRepresentation to Jetty extension to support generation and parsing. - Added support for the "charset" parameter in HTTP BASIC challenges. Reported by Marc Lafon. + - Added MediaType constructors to help with cloning and customization needs. - Misc - Upgraded Thymeleaf library to 3.1.3.RELEASE. - Upgraded Slf4j library to 5.12.0. diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java index 5853f42a7c..579e1c9d61 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -19,9 +19,7 @@ import org.eclipse.jetty.http.MultiPart.Part; import org.eclipse.jetty.http.MultiPartConfig; import org.eclipse.jetty.http.MultiPartFormData; -import org.eclipse.jetty.http.MultiPartFormData.Parts; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.content.InputStreamContentSource; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Promise; import org.restlet.data.MediaType; @@ -44,11 +42,7 @@ public class MultiPartFormDataRepresentation extends InputRepresentation { * @return The updated media type. */ public static MediaType addBoundary(MediaType mediaType) { - // TODO should we duplicate the mediaType, in order to preserve the static - // constants defined in MediaType class? - String boundary = MultiPart.generateBoundary(null, 24); - mediaType.getParameters().add("boundary", boundary); - return mediaType; + return new MediaType(mediaType, "boundary", MultiPart.generateBoundary(null, 24)); } /** diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java index ed6117c703..e5e4eb78ef 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java @@ -605,6 +605,38 @@ public static MediaType valueOf(String name) { /** The list of parameters. */ private volatile Series parameters; + /** + * Constructor that clones an original media type. + * + * @param original The original media type to clone. + * @param paramName The name of the parameter to add. + * @param paramValue The value of the parameter to add. + */ + public MediaType(MediaType original, String paramName, String paramValue) { + this(original, new Parameter(paramName, paramValue)); + } + + /** + * Constructor that clones an original media type. + * + * @param original The original media type to clone. + * @param parameter The parameter to add. + */ + public MediaType(MediaType original, Parameter parameter) { + this(original, parameter == null ? null : parameter.createSeries()); + } + + /** + * Constructor that clones an original media type. + * + * @param original The original media type to clone. + * @param parameters The list of parameters to add. + */ + public MediaType(MediaType original, Series parameters) { + this((original == null) ? null : original.getName(), parameters, + (original == null) ? null : original.getDescription()); + } + /** * Constructor. * diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Parameter.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Parameter.java index dcf616e011..743c35942f 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Parameter.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Parameter.java @@ -11,6 +11,7 @@ import org.restlet.engine.util.SystemUtils; import org.restlet.util.NamedValue; +import org.restlet.util.Series; import java.io.IOException; import java.util.Objects; @@ -29,6 +30,19 @@ public class Parameter implements Comparable, NamedValue { /** The second object. */ private volatile String value; + /** + * Creates a series that includes the current parameter as the initial + * entry. + * + * @return A series that includes the current parameter as the initial + * entry. + */ + public Series createSeries() { + Series result = new Form(); + result.add(this); + return result; + } + /** * Creates a parameter. * From e737f79cb8a7d0baac9c4491db4b952a34c70707 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:50:29 -0400 Subject: [PATCH 11/21] Enhanced MultiPartFormDataRepresentation Cleanup behavior to generate random boundary just before its usage and only when needed. Added method to create a Part based on a Representation --- .../ext/jetty/MultiPartFormDataRepresentation.java | 1 - .../src/main/java/org/restlet/data/MediaType.java | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java index 579e1c9d61..abbb20ad56 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -11,7 +11,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.nio.file.Path; import java.util.*; diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java index e5e4eb78ef..b8fb5b1dcf 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java @@ -609,8 +609,8 @@ public static MediaType valueOf(String name) { * Constructor that clones an original media type. * * @param original The original media type to clone. - * @param paramName The name of the parameter to add. - * @param paramValue The value of the parameter to add. + * @param paramName The name of the unique parameter to set. + * @param paramValue The value of the unique parameter to set. */ public MediaType(MediaType original, String paramName, String paramValue) { this(original, new Parameter(paramName, paramValue)); @@ -620,7 +620,7 @@ public MediaType(MediaType original, String paramName, String paramValue) { * Constructor that clones an original media type. * * @param original The original media type to clone. - * @param parameter The parameter to add. + * @param parameter The unique parameter to set. */ public MediaType(MediaType original, Parameter parameter) { this(original, parameter == null ? null : parameter.createSeries()); @@ -630,7 +630,7 @@ public MediaType(MediaType original, Parameter parameter) { * Constructor that clones an original media type. * * @param original The original media type to clone. - * @param parameters The list of parameters to add. + * @param parameters The list of parameters to set. */ public MediaType(MediaType original, Series parameters) { this((original == null) ? null : original.getName(), parameters, From 427bf7676966558813cfd4931e8dd9ec647930b6 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:51:20 -0400 Subject: [PATCH 12/21] Update MultiPartFormDataRepresentation.java Cleanup behavior to generate random boundary just before its usage and only when needed. Added method to create a Part based on a Representation --- .../MultiPartFormDataRepresentation.java | 423 ++++++++++-------- 1 file changed, 225 insertions(+), 198 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java index abbb20ad56..cde2265fb2 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -12,13 +12,17 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.MultiPart; import org.eclipse.jetty.http.MultiPart.Part; import org.eclipse.jetty.http.MultiPartConfig; import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.InputStreamContentSource; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Promise; import org.restlet.data.MediaType; @@ -34,202 +38,225 @@ */ public class MultiPartFormDataRepresentation extends InputRepresentation { - /** - * Adds a randomly generated boundary to the media type parameters. - * - * @param mediaType The media type to update. - * @return The updated media type. - */ - public static MediaType addBoundary(MediaType mediaType) { - return new MediaType(mediaType, "boundary", MultiPart.generateBoundary(null, 24)); - } - - /** - * Returns the value of the first mediatype parameter with "boundary" name. - * - * @param mediaType The media type that might contain a "boundary" parameter. - * @return The value of the first mediatype parameter with "boundary" name. - */ - public static String getBoundary(MediaType mediaType) { - final String result; - - if (mediaType != null) { - result = mediaType.getParameters().getFirstValue("boundary"); - } else { - result = null; - } - - return result; - } - - /** - * The boundary used to separate each part for the parsed or generated form. - */ - private volatile String boundary; - - /** The wrapped multipart form data either parsed or to be generated. */ - private volatile List parts; - - /** - * Constructor that wraps multiple parts and generates the content via - * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param parts The source parts to use when generating the representation. - */ - public MultiPartFormDataRepresentation(Part... parts) { - super(null, MediaType.MULTIPART_FORM_DATA); - this.boundary = getMediaType().getParameters().getFirstValue("boundary"); - this.parts = Arrays.asList(parts); - } - - // TODO Should we support such constructor? - /** - * Constructor that wraps multiple parts and generates the content via - * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param representations The source parts to use when generating the - * representation. - */ - public MultiPartFormDataRepresentation(Representation... representations) { - this(MultiPart.generateBoundary(null, 24), representations); // TODO should we generate a random boundary? - } - - // TODO Should we support such constructor? - /** - * Constructor that wraps multiple parts and generates the content via - * {@link #write(OutputStream)} as a {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param representations The source parts to use when generating the - * representation. - */ - public MultiPartFormDataRepresentation(final String boundary, final Representation... representations) { - super(null, MediaType.MULTIPART_FORM_DATA); - - final String b = Objects.requireNonNullElse(boundary, getBoundary(getMediaType())); - this.boundary = Objects.requireNonNullElse(b, MultiPart.generateBoundary(null, 24)); // TODO should we generate - // a random boundary? - - this.parts = parts; - } - - /** - * Constructor that parses the content based on a given configuration into - * {@link #getParts()}. Uses a default {@link MultiPartConfig}. - * - * @param content The multipart entity to parse which should have a media type - * based on {@link MediaType#MULTIPART_FORM_DATA}, with a - * "boundary" parameter. - * @param location The location where parsed files are stored for easier access. - * @throws IOException - */ - public MultiPartFormDataRepresentation(Representation content, Path location) throws IOException { - this(content, new MultiPartConfig.Builder().location(location).build()); - } - - /** - * Constructor that parses the content based on a given configuration into - * {@link #getParts()}. - * - * @param content The multipart entity to parse which should have a media type - * based on {@link MediaType#MULTIPART_FORM_DATA}, with a - * "boundary" parameter. - * @param config The multipart configuration. - * @throws IOException - */ - public MultiPartFormDataRepresentation(Representation content, MultiPartConfig config) throws IOException { - this(ContentType.writeHeader(content), content.getStream(), config); - } - - /** - * Constructor that parses the content based on a given configuration into - * {@link #getParts()}. - * - * @param contentType The media type that should be based on - * {@link MediaType#MULTIPART_FORM_DATA}, with a "boundary" - * parameter. - * @param content The multipart entity to parse. - * @param config The multipart configuration. - * @throws IOException - */ - public MultiPartFormDataRepresentation(String contentType, InputStream content, MultiPartConfig config) - throws IOException { - super(null, MediaType.MULTIPART_FORM_DATA); - this.boundary = boundary; - - if (content != null) { - Content.Source contentSource = Content.Source.from(content); - Attributes.Mapped attributes = new Attributes.Mapped(); - - // Convert the request content into parts. - MultiPartFormData.onParts(contentSource, attributes, contentType, config, new Promise.Invocable<>() { - @Override - public void failed(Throwable failure) { - throw new IllegalStateException("Unable to parse the multipart form data representation", failure); - } - - @Override - public InvocationType getInvocationType() { - return InvocationType.BLOCKING; - } - - @Override - public void succeeded(MultiPartFormData.Parts parts) { - // Store the resulting parts - MultiPartFormDataRepresentation.this.parts = new ArrayList<>(); - parts.iterator().forEachRemaining(part -> MultiPartFormDataRepresentation.this.parts.add(part)); - } - }); - } - } - - /** - * Returns the boundary used to separate each part for the parsed or generated - * form. - * - * @return The boundary used to separate each part for the parsed or generated - * form. - */ - public String getBoundary() { - return boundary; - } - - /** - * Returns the wrapped multipart form data either parsed or to be generated. - * - * @return The wrapped multipart form data either parsed or to be generated. - */ - public List getParts() { - return parts; - } - - /** - * Returns an input stream that generates the multipart form data serialization - * for the wrapped {@link #getParts()} object. - * - * @return An input stream that generates the multipart form data. - */ - @Override - public InputStream getStream() throws IOException { - MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource(getBoundary()); - - for (Part part : this.parts) { - content.addPart(part); - } - - content.close(); - setStream(null); - return Content.Source.asInputStream(content); - } - - /** - * Sets the boundary used to separate each part for the parsed or generated - * form. - * - * @param boundary The boundary used to separate each part for the parsed or - * generated form. - */ - public void setBoundary(String boundary) { - this.boundary = boundary; - } + /** + * Creates a #{@link Part} object based on a {@link Representation} plus + * metadata. + * + * @param name The name of the part. + * @param fileName The client suggest file name for storing the part. + * @param partContent The part content. + * @return The Jetty #{@link Part} object created. + * @throws IOException + */ + public static Part createPart(String name, String fileName, + Representation partContent) throws IOException { + return new MultiPart.ContentSourcePart(name, fileName, HttpFields.EMPTY, + new InputStreamContentSource(partContent.getStream())); + } + + /** + * Returns the value of the first mediatype parameter with "boundary" name. + * + * @param mediaType The media type that might contain a "boundary" + * parameter. + * @return The value of the first mediatype parameter with "boundary" name. + */ + public static String getBoundary(MediaType mediaType) { + final String result; + + if (mediaType != null) { + result = mediaType.getParameters().getFirstValue("boundary"); + } else { + result = null; + } + + return result; + } + + /** + * The boundary used to separate each part for the parsed or generated form. + */ + private volatile String boundary; + + /** The wrapped multipart form data either parsed or to be generated. */ + private volatile List parts; + + /** + * Constructor that wraps multiple parts and GENERATES the content via + * {@link #getStream()} as a {@link MediaType#MULTIPART_FORM_DATA}. + * + * Unless a boundary is manually set via {@link #setBoundary(String)}, one + * will be randomly generated when {@link #getStream()} is invoked. + * + * @param parts The source parts to use when generating the representation. + */ + public MultiPartFormDataRepresentation(Part... parts) { + this(Arrays.asList(parts)); + } + + /** + * Constructor that wraps multiple parts and GENERATES the content via + * {@link #getStream()} as a {@link MediaType#MULTIPART_FORM_DATA}. + * + * Unless a boundary is manually set via {@link #setBoundary(String)}, one + * will be randomly generated when {@link #getStream()} is invoked. + * + * @param parts The source parts to use when generating the representation. + */ + public MultiPartFormDataRepresentation(List parts) { + super(null, MediaType.MULTIPART_FORM_DATA); + this.boundary = null; + this.parts = parts; + } + + /** + * Constructor that PARSES the content based on a given configuration into + * {@link #getParts()}. + * + * @param multiPartEntity The multipart entity to parse which should have a + * media type based on + * {@link MediaType#MULTIPART_FORM_DATA}, with a + * "boundary" parameter. + * @param config The multipart configuration. + * @throws IOException + */ + public MultiPartFormDataRepresentation(Representation multiPartEntity, + MultiPartConfig config) throws IOException { + this(ContentType.writeHeader(multiPartEntity), + multiPartEntity.getStream(), config); + } + + /** + * Constructor that PARSES the content based on a given configuration into + * {@link #getParts()}. Uses a default {@link MultiPartConfig}. + * + * @param multiPartEntity The multipart entity to parse which should have a + * media type based on + * {@link MediaType#MULTIPART_FORM_DATA}, with a + * "boundary" parameter. + * @param storeLocation The location where parsed files are stored for + * easier access. + * @throws IOException + */ + public MultiPartFormDataRepresentation(Representation multiPartEntity, + Path storeLocation) throws IOException { + this(multiPartEntity, + new MultiPartConfig.Builder().location(storeLocation).build()); + } + + /** + * Constructor that PARSES the content based on a given configuration into + * {@link #getParts()}. + * + * @param contentType The media type that should be based on + * {@link MediaType#MULTIPART_FORM_DATA}, with a + * "boundary" parameter. + * @param multiPartEntity The multipart entity to parse. + * @param config The multipart configuration. + * @throws IOException + */ + public MultiPartFormDataRepresentation(String contentType, + InputStream multiPartEntity, MultiPartConfig config) + throws IOException { + super(null, MediaType.MULTIPART_FORM_DATA); + this.boundary = boundary; + + if (multiPartEntity != null) { + Content.Source contentSource = Content.Source.from(multiPartEntity); + Attributes.Mapped attributes = new Attributes.Mapped(); + + // Convert the request content into parts. + MultiPartFormData.onParts(contentSource, attributes, contentType, + config, new Promise.Invocable<>() { + @Override + public void failed(Throwable failure) { + throw new IllegalStateException( + "Unable to parse the multipart form data representation", + failure); + } + + @Override + public InvocationType getInvocationType() { + return InvocationType.BLOCKING; + } + + @Override + public void succeeded(MultiPartFormData.Parts parts) { + // Store the resulting parts + MultiPartFormDataRepresentation.this.parts = new ArrayList<>(); + parts.iterator().forEachRemaining( + part -> MultiPartFormDataRepresentation.this.parts + .add(part)); + } + }); + } + } + + /** + * Returns the boundary used to separate each part for the parsed or + * generated form. + * + * @return The boundary used to separate each part for the parsed or + * generated form. + */ + public String getBoundary() { + return boundary; + } + + /** + * Returns the wrapped multipart form data either parsed or to be generated. + * + * @return The wrapped multipart form data either parsed or to be generated. + */ + public List getParts() { + return parts; + } + + /** + * Returns an input stream that generates the multipart form data + * serialization for the wrapped {@link #getParts()} object. + * + * If the {@link #getBoundary()} is null, as random one is generated and set + * as an attribute of the {@link #getMediaType()}. + * + * + * @return An input stream that generates the multipart form data. + */ + @Override + public InputStream getStream() throws IOException { + if (getBoundary() == null) { + setBoundary(MultiPart.generateBoundary(null, 24)); + } + + if (getMediaType() == null) { + setMediaType(new MediaType(MediaType.MULTIPART_FORM_DATA, + "boundary", getBoundary())); + } else { + setMediaType( + new MediaType(getMediaType(), "boundary", getBoundary())); + } + + MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource( + getBoundary()); + + for (Part part : this.parts) { + content.addPart(part); + } + + content.close(); + setStream(null); + return Content.Source.asInputStream(content); + } + + /** + * Sets the boundary used to separate each part for the parsed or generated + * form. + * + * @param boundary The boundary used to separate each part for the parsed or + * generated form. + */ + public void setBoundary(String boundary) { + this.boundary = boundary; + } } From 54a00b61012c55b5a6f1bbf72080185a74890c49 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:00:42 -0400 Subject: [PATCH 13/21] Fixed boundary setting issue and adjusted test case --- .../MultiPartFormDataRepresentation.java | 114 +- .../ext/jetty/MultiPartFormTestCase.java | 91 +- .../main/java/org/restlet/data/MediaType.java | 1521 +++++++++-------- 3 files changed, 954 insertions(+), 772 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java index cde2265fb2..8c4cb4d14f 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java @@ -38,6 +38,29 @@ */ public class MultiPartFormDataRepresentation extends InputRepresentation { + /** + * Sets a boundary to an existing media type. If the original mediatype + * already has a "boundary" parameter, it will be erased. * + * + * @param mediaType The media type to update. + * @param boundary The boundary to add as a parameter. + * @return The updated media type. + */ + public static MediaType setBoundary(MediaType mediaType, String boundary) { + MediaType result = null; + + if (mediaType != null) { + if (mediaType.getParameters().getFirst("boundary") != null) { + result = new MediaType(mediaType.getParent(), "boundary", + boundary); + } else { + result = new MediaType(mediaType, "boundary", boundary); + } + } + + return result; + } + /** * Creates a #{@link Part} object based on a {@link Representation} plus * metadata. @@ -82,11 +105,9 @@ public static String getBoundary(MediaType mediaType) { private volatile List parts; /** - * Constructor that wraps multiple parts and GENERATES the content via - * {@link #getStream()} as a {@link MediaType#MULTIPART_FORM_DATA}. - * - * Unless a boundary is manually set via {@link #setBoundary(String)}, one - * will be randomly generated when {@link #getStream()} is invoked. + * Constructor that wraps multiple parts, set a random boundary, then + * GENERATES the content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. * * @param parts The source parts to use when generating the representation. */ @@ -95,17 +116,54 @@ public MultiPartFormDataRepresentation(Part... parts) { } /** - * Constructor that wraps multiple parts and GENERATES the content via - * {@link #getStream()} as a {@link MediaType#MULTIPART_FORM_DATA}. + * Constructor that wraps multiple parts, set a boundary, then GENERATES the + * content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. * - * Unless a boundary is manually set via {@link #setBoundary(String)}, one - * will be randomly generated when {@link #getStream()} is invoked. + * @param parts The source parts to use when generating the representation. + */ + public MultiPartFormDataRepresentation(String boundary, Part... parts) { + this(boundary, Arrays.asList(parts)); + } + + /** + * Constructor that wraps multiple parts, set a random boundary, then + * GENERATES the content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. * * @param parts The source parts to use when generating the representation. */ public MultiPartFormDataRepresentation(List parts) { - super(null, MediaType.MULTIPART_FORM_DATA); - this.boundary = null; + this(MultiPart.generateBoundary(null, 24), parts); + } + + /** + * Constructor that wraps multiple parts, set a boundary, then GENERATES the + * content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param boundary The boundary to add as a parameter. + * @param parts The source parts to use when generating the + * representation. + */ + public MultiPartFormDataRepresentation(String boundary, List parts) { + this(MediaType.MULTIPART_FORM_DATA, boundary, parts); + } + + /** + * Constructor that wraps multiple parts, set a media type with a boundary, + * then GENERATES the content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param mediaType The media type to set. + * @param boundary The boundary to add as a parameter. + * @param parts The source parts to use when generating the + * representation. + */ + public MultiPartFormDataRepresentation(MediaType mediaType, String boundary, + List parts) { + super(null, setBoundary(mediaType, boundary)); + this.boundary = boundary; this.parts = parts; } @@ -134,14 +192,14 @@ public MultiPartFormDataRepresentation(Representation multiPartEntity, * media type based on * {@link MediaType#MULTIPART_FORM_DATA}, with a * "boundary" parameter. - * @param storeLocation The location where parsed files are stored for + * @param storageLocation The location where parsed files are stored for * easier access. * @throws IOException */ public MultiPartFormDataRepresentation(Representation multiPartEntity, - Path storeLocation) throws IOException { - this(multiPartEntity, - new MultiPartConfig.Builder().location(storeLocation).build()); + Path storageLocation) throws IOException { + this(multiPartEntity, new MultiPartConfig.Builder() + .location(storageLocation).build()); } /** @@ -214,26 +272,15 @@ public List getParts() { /** * Returns an input stream that generates the multipart form data - * serialization for the wrapped {@link #getParts()} object. - * - * If the {@link #getBoundary()} is null, as random one is generated and set - * as an attribute of the {@link #getMediaType()}. - * + * serialization for the wrapped {@link #getParts()} object. The "boundary" + * must be non null when invoking this method. * * @return An input stream that generates the multipart form data. */ @Override public InputStream getStream() throws IOException { if (getBoundary() == null) { - setBoundary(MultiPart.generateBoundary(null, 24)); - } - - if (getMediaType() == null) { - setMediaType(new MediaType(MediaType.MULTIPART_FORM_DATA, - "boundary", getBoundary())); - } else { - setMediaType( - new MediaType(getMediaType(), "boundary", getBoundary())); + throw new IllegalArgumentException("The boundary can't be null"); } MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource( @@ -250,13 +297,20 @@ public InputStream getStream() throws IOException { /** * Sets the boundary used to separate each part for the parsed or generated - * form. + * form. It will also update the {@link MediaType}'s "boundary" attribute. * * @param boundary The boundary used to separate each part for the parsed or * generated form. */ public void setBoundary(String boundary) { this.boundary = boundary; + + if (getMediaType() == null) { + setMediaType(new MediaType(MediaType.MULTIPART_FORM_DATA, + "boundary", boundary)); + } else { + setMediaType(setBoundary(getMediaType(), boundary)); + } } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java index a920be324f..f5379bac91 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java @@ -9,18 +9,17 @@ package org.restlet.ext.jetty; -import org.eclipse.jetty.client.StringRequestContent; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.MultiPart; -import org.junit.jupiter.api.Test; -import org.restlet.data.MediaType; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.eclipse.jetty.client.StringRequestContent; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.MultiPart; +import org.junit.jupiter.api.Test; /** * Test case for the {@link MultiPartFormDataRepresentation} class in multipart @@ -30,47 +29,51 @@ */ public class MultiPartFormTestCase { - @Test - public void testWriteFromParts() throws IOException { - Path textFilePath = Files.createTempFile("multiPart", ""); - Files.write(textFilePath, "this is the content of the file".getBytes(StandardCharsets.UTF_8)); - MultiPart.PathPart filePart = new MultiPart.PathPart("icon", "text.txt", HttpFields.EMPTY, textFilePath); - - MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart("field", null, HttpFields.EMPTY, - new StringRequestContent("foo")); + @Test + public void testWriteFromParts() throws IOException { + Path textFilePath = Files.createTempFile("multiPart", ""); + Files.write(textFilePath, "this is the content of the file" + .getBytes(StandardCharsets.UTF_8)); + MultiPart.PathPart filePart = new MultiPart.PathPart("icon", "text.txt", + HttpFields.EMPTY, textFilePath); - final String boundary = "-----------------------------1294919323195"; + MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart( + "field", null, HttpFields.EMPTY, + new StringRequestContent("foo")); - MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation(contentSourcePart, filePart); - rep.setBoundary(boundary); + final String boundary = "-----------------------------1294919323195"; - final String expected = """ - --%s\r - Content-Disposition: form-data; name="field"\r - \r - foo\r - --%s\r - Content-Disposition: form-data; name="icon"; filename="text.txt"\r - \r - this is the content of the file\r - --%s--\r - """.replace("%s", boundary); - assertEquals(expected, rep.getText()); - } + MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation( + contentSourcePart, filePart); + rep.setBoundary(boundary); - /** - * Tests the multipart content-type. - */ - @Test - public void testContentType() { - MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart("field", null, HttpFields.EMPTY, - new StringRequestContent("foo")); - MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation(contentSourcePart); - rep.setBoundary("myBoundary"); - assertEquals(MediaType.MULTIPART_FORM_DATA, rep.getMediaType()); + final String expected = """ + --%s\r + Content-Disposition: form-data; name="field"\r + \r + foo\r + --%s\r + Content-Disposition: form-data; name="icon"; filename="text.txt"\r + \r + this is the content of the file\r + --%s--\r + """ + .replace("%s", boundary); + assertEquals(expected, rep.getText()); + } - // Is this test really correct? - // assertEquals("myBoundary", - // rep.getMediaType().getParameters().getFirstValue("boundary")); - } + /** + * Tests the multipart content-type. + */ + @Test + public void testContentType() { + MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart( + "field", null, HttpFields.EMPTY, + new StringRequestContent("foo")); + MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation( + "myInitialBoundary", contentSourcePart); + rep.setBoundary("myActualBoundary"); + assertEquals("multipart/form-data; boundary=myActualBoundary", + rep.getMediaType().toString()); + } } diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java index b8fb5b1dcf..0e2126df3a 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/MediaType.java @@ -30,580 +30,695 @@ */ public final class MediaType extends Metadata { - /** - * Illegal ASCII characters as defined in RFC 1521.
- * Keep the underscore for the ordering - * - * @see RFC 1521 - */ - private static final String _TSPECIALS = "()<>@,;:/[]?=\\\""; + /** + * Illegal ASCII characters as defined in RFC 1521.
+ * Keep the underscore for the ordering + * + * @see RFC 1521 + */ + private static final String _TSPECIALS = "()<>@,;:/[]?=\\\""; + + /** + * The known media types registered with {@link #register(String, String)}, + * retrievable using {@link #valueOf(String)}.
+ * Keep the underscore for the ordering. + */ + private static volatile Map _types = null; + + public static final MediaType ALL = register("*/*", "All media"); - /** - * The known media types registered with {@link #register(String, String)}, - * retrievable using {@link #valueOf(String)}.
- * Keep the underscore for the ordering. - */ - private static volatile Map _types = null; + public static final MediaType APPLICATION_ALL = register("application/*", + "All application documents"); - public static final MediaType ALL = register("*/*", "All media"); + public static final MediaType APPLICATION_ALL_JSON = register( + "application/*+json", "All application/*+json documents"); - public static final MediaType APPLICATION_ALL = register("application/*", "All application documents"); + public static final MediaType APPLICATION_ALL_XML = register( + "application/*+xml", "All application/*+xml documents"); - public static final MediaType APPLICATION_ALL_JSON = register("application/*+json", - "All application/*+json documents"); + public static final MediaType APPLICATION_ATOM = register( + "application/atom+xml", "Atom document"); - public static final MediaType APPLICATION_ALL_XML = register("application/*+xml", - "All application/*+xml documents"); + public static final MediaType APPLICATION_ATOMPUB_CATEGORY = register( + "application/atomcat+xml", "Atom category document"); - public static final MediaType APPLICATION_ATOM = register("application/atom+xml", "Atom document"); + public static final MediaType APPLICATION_ATOMPUB_SERVICE = register( + "application/atomsvc+xml", "Atom service document"); - public static final MediaType APPLICATION_ATOMPUB_CATEGORY = register("application/atomcat+xml", - "Atom category document"); + public static final MediaType APPLICATION_CAB = register( + "application/vnd.ms-cab-compressed", "Microsoft Cabinet archive"); - public static final MediaType APPLICATION_ATOMPUB_SERVICE = register("application/atomsvc+xml", - "Atom service document"); + public static final MediaType APPLICATION_COMPRESS = register( + "application/x-compress", "Compressed file"); - public static final MediaType APPLICATION_CAB = register("application/vnd.ms-cab-compressed", - "Microsoft Cabinet archive"); + public static final MediaType APPLICATION_ECORE = register( + "application/x-ecore+xmi+xml", "EMOF ECore metamodel"); - public static final MediaType APPLICATION_COMPRESS = register("application/x-compress", "Compressed file"); + public static final MediaType APPLICATION_EXCEL = register( + "application/vnd.ms-excel", "Microsoft Excel document"); - public static final MediaType APPLICATION_ECORE = register("application/x-ecore+xmi+xml", "EMOF ECore metamodel"); + public static final MediaType APPLICATION_FLASH = register( + "application/x-shockwave-flash", "Shockwave Flash object"); - public static final MediaType APPLICATION_EXCEL = register("application/vnd.ms-excel", "Microsoft Excel document"); + public static final MediaType APPLICATION_GNU_TAR = register( + "application/x-gtar", "GNU Tar archive"); - public static final MediaType APPLICATION_FLASH = register("application/x-shockwave-flash", - "Shockwave Flash object"); + public static final MediaType APPLICATION_GNU_ZIP = register( + "application/x-gzip", "GNU Zip archive"); - public static final MediaType APPLICATION_GNU_TAR = register("application/x-gtar", "GNU Tar archive"); + public static final MediaType APPLICATION_HTTP_COOKIES = register( + "application/x-http-cookies", "HTTP cookies"); - public static final MediaType APPLICATION_GNU_ZIP = register("application/x-gzip", "GNU Zip archive"); + public static final MediaType APPLICATION_JAVA = register( + "application/java", "Java class"); - public static final MediaType APPLICATION_HTTP_COOKIES = register("application/x-http-cookies", "HTTP cookies"); + public static final MediaType APPLICATION_JAVA_ARCHIVE = register( + "application/java-archive", "Java archive"); - public static final MediaType APPLICATION_JAVA = register("application/java", "Java class"); + public static final MediaType APPLICATION_JAVA_OBJECT = register( + "application/x-java-serialized-object", "Java serialized object"); - public static final MediaType APPLICATION_JAVA_ARCHIVE = register("application/java-archive", "Java archive"); + public static final MediaType APPLICATION_JAVA_OBJECT_GWT = register( + "text/x-gwt-rpc", "Java serialized object (using GWT-RPC encoder)"); - public static final MediaType APPLICATION_JAVA_OBJECT = register("application/x-java-serialized-object", - "Java serialized object"); + public static final MediaType APPLICATION_JAVA_OBJECT_XML = register( + "text/x-gwt-rpc+xml", + "Java serialized object (using JavaBeans XML encoder)"); - public static final MediaType APPLICATION_JAVA_OBJECT_GWT = register("text/x-gwt-rpc", - "Java serialized object (using GWT-RPC encoder)"); + public static final MediaType APPLICATION_JAVASCRIPT = register( + "application/x-javascript", "Javascript document"); - public static final MediaType APPLICATION_JAVA_OBJECT_XML = register("text/x-gwt-rpc+xml", - "Java serialized object (using JavaBeans XML encoder)"); + public static final MediaType APPLICATION_JNLP = register( + "application/x-java-jnlp-file", "JNLP"); - public static final MediaType APPLICATION_JAVASCRIPT = register("application/x-javascript", "Javascript document"); + public static final MediaType APPLICATION_JSON = register( + "application/json", "JavaScript Object Notation document"); - public static final MediaType APPLICATION_JNLP = register("application/x-java-jnlp-file", "JNLP"); + public static final MediaType APPLICATION_JSON_ACTIVITY = register( + "application/activity+json", "Activity Streams JSON document"); - public static final MediaType APPLICATION_JSON = register("application/json", - "JavaScript Object Notation document"); + public static final MediaType APPLICATION_JSON_PATCH = register( + "application/json-patch", "JSON patch document"); - public static final MediaType APPLICATION_JSON_ACTIVITY = register("application/activity+json", - "Activity Streams JSON document"); + public static final MediaType APPLICATION_JSON_SMILE = register( + "application/x-json-smile", + "JavaScript Object Notation smile document"); - public static final MediaType APPLICATION_JSON_PATCH = register("application/json-patch", "JSON patch document"); + public static final MediaType APPLICATION_KML = register( + "application/vnd.google-earth.kml+xml", + "Google Earth/Maps KML document"); - public static final MediaType APPLICATION_JSON_SMILE = register("application/x-json-smile", - "JavaScript Object Notation smile document"); + public static final MediaType APPLICATION_KMZ = register( + "application/vnd.google-earth.kmz", + "Google Earth/Maps KMZ document"); - public static final MediaType APPLICATION_KML = register("application/vnd.google-earth.kml+xml", - "Google Earth/Maps KML document"); + public static final MediaType APPLICATION_LATEX = register( + "application/x-latex", "LaTeX"); - public static final MediaType APPLICATION_KMZ = register("application/vnd.google-earth.kmz", - "Google Earth/Maps KMZ document"); + public static final MediaType APPLICATION_MAC_BINHEX40 = register( + "application/mac-binhex40", "Mac binhex40"); - public static final MediaType APPLICATION_LATEX = register("application/x-latex", "LaTeX"); + public static final MediaType APPLICATION_MATHML = register( + "application/mathml+xml", "MathML XML document"); - public static final MediaType APPLICATION_MAC_BINHEX40 = register("application/mac-binhex40", "Mac binhex40"); + public static final MediaType APPLICATION_MSML = register( + "application/msml+xml", "Media Server Markup Language"); - public static final MediaType APPLICATION_MATHML = register("application/mathml+xml", "MathML XML document"); + public static final MediaType APPLICATION_MSOFFICE_DOCM = register( + "application/vnd.ms-word.document.macroEnabled.12", + "Office Word 2007 macro-enabled document"); - public static final MediaType APPLICATION_MSML = register("application/msml+xml", "Media Server Markup Language"); + public static final MediaType APPLICATION_MSOFFICE_DOCX = register( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Microsoft Office Word 2007 document"); - public static final MediaType APPLICATION_MSOFFICE_DOCM = register( - "application/vnd.ms-word.document.macroEnabled.12", "Office Word 2007 macro-enabled document"); + public static final MediaType APPLICATION_MSOFFICE_DOTM = register( + "application/vnd.ms-word.template.macroEnabled.12", + "Office Word 2007 macro-enabled document template"); - public static final MediaType APPLICATION_MSOFFICE_DOCX = register( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "Microsoft Office Word 2007 document"); + public static final MediaType APPLICATION_MSOFFICE_DOTX = register( + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "Office Word 2007 template"); - public static final MediaType APPLICATION_MSOFFICE_DOTM = register( - "application/vnd.ms-word.template.macroEnabled.12", "Office Word 2007 macro-enabled document template"); + public static final MediaType APPLICATION_MSOFFICE_ONETOC = register( + "application/onenote", "Microsoft Office OneNote 2007 TOC"); - public static final MediaType APPLICATION_MSOFFICE_DOTX = register( - "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "Office Word 2007 template"); + public static final MediaType APPLICATION_MSOFFICE_ONETOC2 = register( + "application/onenote", "Office OneNote 2007 TOC"); - public static final MediaType APPLICATION_MSOFFICE_ONETOC = register("application/onenote", - "Microsoft Office OneNote 2007 TOC"); + public static final MediaType APPLICATION_MSOFFICE_POTM = register( + "application/vnd.ms-powerpoint.template.macroEnabled.12", + "Office PowerPoint 2007 macro-enabled presentation template"); - public static final MediaType APPLICATION_MSOFFICE_ONETOC2 = register("application/onenote", - "Office OneNote 2007 TOC"); + public static final MediaType APPLICATION_MSOFFICE_POTX = register( + "application/vnd.openxmlformats-officedocument.presentationml.template", + "Office PowerPoint 2007 template"); - public static final MediaType APPLICATION_MSOFFICE_POTM = register( - "application/vnd.ms-powerpoint.template.macroEnabled.12", - "Office PowerPoint 2007 macro-enabled presentation template"); + public static final MediaType APPLICATION_MSOFFICE_PPAM = register( + "application/vnd.ms-powerpoint.addin.macroEnabled.12", + "Office PowerPoint 2007 add-in"); - public static final MediaType APPLICATION_MSOFFICE_POTX = register( - "application/vnd.openxmlformats-officedocument.presentationml.template", "Office PowerPoint 2007 template"); + public static final MediaType APPLICATION_MSOFFICE_PPSM = register( + "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + "Office PowerPoint 2007 macro-enabled slide show"); - public static final MediaType APPLICATION_MSOFFICE_PPAM = register( - "application/vnd.ms-powerpoint.addin.macroEnabled.12", "Office PowerPoint 2007 add-in"); + public static final MediaType APPLICATION_MSOFFICE_PPSX = register( + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "Office PowerPoint 2007 slide show"); - public static final MediaType APPLICATION_MSOFFICE_PPSM = register( - "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", - "Office PowerPoint 2007 macro-enabled slide show"); + public static final MediaType APPLICATION_MSOFFICE_PPTM = register( + "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + "Office PowerPoint 2007 macro-enabled presentation"); - public static final MediaType APPLICATION_MSOFFICE_PPSX = register( - "application/vnd.openxmlformats-officedocument.presentationml.slideshow", - "Office PowerPoint 2007 slide show"); + public static final MediaType APPLICATION_MSOFFICE_PPTX = register( + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "Microsoft Office PowerPoint 2007 presentation"); - public static final MediaType APPLICATION_MSOFFICE_PPTM = register( - "application/vnd.ms-powerpoint.presentation.macroEnabled.12", - "Office PowerPoint 2007 macro-enabled presentation"); + public static final MediaType APPLICATION_MSOFFICE_SLDM = register( + "application/vnd.ms-powerpoint.slide.macroEnabled.12", + "Office PowerPoint 2007 macro-enabled slide"); - public static final MediaType APPLICATION_MSOFFICE_PPTX = register( - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "Microsoft Office PowerPoint 2007 presentation"); + public static final MediaType APPLICATION_MSOFFICE_SLDX = register( + "application/vnd.openxmlformats-officedocument.presentationml.slide", + "Office PowerPoint 2007 slide"); - public static final MediaType APPLICATION_MSOFFICE_SLDM = register( - "application/vnd.ms-powerpoint.slide.macroEnabled.12", "Office PowerPoint 2007 macro-enabled slide"); + public static final MediaType APPLICATION_MSOFFICE_XLAM = register( + "application/vnd.ms-excel.addin.macroEnabled.12", + "Office Excel 2007 add-in"); - public static final MediaType APPLICATION_MSOFFICE_SLDX = register( - "application/vnd.openxmlformats-officedocument.presentationml.slide", "Office PowerPoint 2007 slide"); + public static final MediaType APPLICATION_MSOFFICE_XLSB = register( + "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + "Office Excel 2007 binary workbook"); - public static final MediaType APPLICATION_MSOFFICE_XLAM = register("application/vnd.ms-excel.addin.macroEnabled.12", - "Office Excel 2007 add-in"); + public static final MediaType APPLICATION_MSOFFICE_XLSM = register( + "application/vnd.ms-excel.sheet.macroEnabled.12", + "Office Excel 2007 macro-enabled workbook"); - public static final MediaType APPLICATION_MSOFFICE_XLSB = register( - "application/vnd.ms-excel.sheet.binary.macroEnabled.12", "Office Excel 2007 binary workbook"); + public static final MediaType APPLICATION_MSOFFICE_XLSX = register( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Microsoft Office Excel 2007 workbook"); - public static final MediaType APPLICATION_MSOFFICE_XLSM = register("application/vnd.ms-excel.sheet.macroEnabled.12", - "Office Excel 2007 macro-enabled workbook"); + public static final MediaType APPLICATION_MSOFFICE_XLTM = register( + "application/vnd.ms-excel.template.macroEnabled.12", + "Office Excel 2007 macro-enabled workbook template"); - public static final MediaType APPLICATION_MSOFFICE_XLSX = register( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "Microsoft Office Excel 2007 workbook"); + public static final MediaType APPLICATION_MSOFFICE_XLTX = register( + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "Office Excel 2007 template"); - public static final MediaType APPLICATION_MSOFFICE_XLTM = register( - "application/vnd.ms-excel.template.macroEnabled.12", "Office Excel 2007 macro-enabled workbook template"); + public static final MediaType APPLICATION_OCTET_STREAM = register( + "application/octet-stream", "Raw octet stream"); - public static final MediaType APPLICATION_MSOFFICE_XLTX = register( - "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "Office Excel 2007 template"); + public static final MediaType APPLICATION_OPENOFFICE_ODB = register( + "application/vnd.oasis.opendocument.database", + "OpenDocument Database"); - public static final MediaType APPLICATION_OCTET_STREAM = register("application/octet-stream", "Raw octet stream"); + public static final MediaType APPLICATION_OPENOFFICE_ODC = register( + "application/vnd.oasis.opendocument.chart", "OpenDocument Chart"); - public static final MediaType APPLICATION_OPENOFFICE_ODB = register("application/vnd.oasis.opendocument.database", - "OpenDocument Database"); + public static final MediaType APPLICATION_OPENOFFICE_ODF = register( + "application/vnd.oasis.opendocument.formula", + "OpenDocument Formula"); - public static final MediaType APPLICATION_OPENOFFICE_ODC = register("application/vnd.oasis.opendocument.chart", - "OpenDocument Chart"); + public static final MediaType APPLICATION_OPENOFFICE_ODG = register( + "application/vnd.oasis.opendocument.graphics", + "OpenDocument Drawing"); - public static final MediaType APPLICATION_OPENOFFICE_ODF = register("application/vnd.oasis.opendocument.formula", - "OpenDocument Formula"); + public static final MediaType APPLICATION_OPENOFFICE_ODI = register( + "application/vnd.oasis.opendocument.image", "OpenDocument Image "); - public static final MediaType APPLICATION_OPENOFFICE_ODG = register("application/vnd.oasis.opendocument.graphics", - "OpenDocument Drawing"); + public static final MediaType APPLICATION_OPENOFFICE_ODM = register( + "application/vnd.oasis.opendocument.text-master", + "OpenDocument Master Document"); - public static final MediaType APPLICATION_OPENOFFICE_ODI = register("application/vnd.oasis.opendocument.image", - "OpenDocument Image "); + public static final MediaType APPLICATION_OPENOFFICE_ODP = register( + "application/vnd.oasis.opendocument.presentation", + "OpenDocument Presentation "); - public static final MediaType APPLICATION_OPENOFFICE_ODM = register( - "application/vnd.oasis.opendocument.text-master", "OpenDocument Master Document"); + public static final MediaType APPLICATION_OPENOFFICE_ODS = register( + "application/vnd.oasis.opendocument.spreadsheet", + "OpenDocument Spreadsheet"); - public static final MediaType APPLICATION_OPENOFFICE_ODP = register( - "application/vnd.oasis.opendocument.presentation", "OpenDocument Presentation "); + public static final MediaType APPLICATION_OPENOFFICE_ODT = register( + "application/vnd.oasis.opendocument.text ", "OpenDocument Text"); - public static final MediaType APPLICATION_OPENOFFICE_ODS = register( - "application/vnd.oasis.opendocument.spreadsheet", "OpenDocument Spreadsheet"); + public static final MediaType APPLICATION_OPENOFFICE_OTG = register( + "application/vnd.oasis.opendocument.graphics-template", + "OpenDocument Drawing Template"); - public static final MediaType APPLICATION_OPENOFFICE_ODT = register("application/vnd.oasis.opendocument.text ", - "OpenDocument Text"); + public static final MediaType APPLICATION_OPENOFFICE_OTH = register( + "application/vnd.oasis.opendocument.text-web", + "HTML Document Template"); - public static final MediaType APPLICATION_OPENOFFICE_OTG = register( - "application/vnd.oasis.opendocument.graphics-template", "OpenDocument Drawing Template"); + public static final MediaType APPLICATION_OPENOFFICE_OTP = register( + "application/vnd.oasis.opendocument.presentation-template", + "OpenDocument Presentation Template"); - public static final MediaType APPLICATION_OPENOFFICE_OTH = register("application/vnd.oasis.opendocument.text-web", - "HTML Document Template"); + public static final MediaType APPLICATION_OPENOFFICE_OTS = register( + "application/vnd.oasis.opendocument.spreadsheet-template", + "OpenDocument Spreadsheet Template"); - public static final MediaType APPLICATION_OPENOFFICE_OTP = register( - "application/vnd.oasis.opendocument.presentation-template", "OpenDocument Presentation Template"); + public static final MediaType APPLICATION_OPENOFFICE_OTT = register( + "application/vnd.oasis.opendocument.text-template", + "OpenDocument Text Template"); - public static final MediaType APPLICATION_OPENOFFICE_OTS = register( - "application/vnd.oasis.opendocument.spreadsheet-template", "OpenDocument Spreadsheet Template"); + public static final MediaType APPLICATION_OPENOFFICE_OXT = register( + "application/vnd.openofficeorg.extension", + "OpenOffice.org extension"); - public static final MediaType APPLICATION_OPENOFFICE_OTT = register( - "application/vnd.oasis.opendocument.text-template", "OpenDocument Text Template"); + public static final MediaType APPLICATION_PDF = register("application/pdf", + "Adobe PDF document"); - public static final MediaType APPLICATION_OPENOFFICE_OXT = register("application/vnd.openofficeorg.extension", - "OpenOffice.org extension"); + public static final MediaType APPLICATION_POSTSCRIPT = register( + "application/postscript", "Postscript document"); - public static final MediaType APPLICATION_PDF = register("application/pdf", "Adobe PDF document"); + public static final MediaType APPLICATION_POWERPOINT = register( + "application/vnd.ms-powerpoint", "Microsoft Powerpoint document"); - public static final MediaType APPLICATION_POSTSCRIPT = register("application/postscript", "Postscript document"); + public static final MediaType APPLICATION_PROJECT = register( + "application/vnd.ms-project", "Microsoft Project document"); - public static final MediaType APPLICATION_POWERPOINT = register("application/vnd.ms-powerpoint", - "Microsoft Powerpoint document"); + public static final MediaType APPLICATION_RDF_TRIG = register( + "application/x-trig", + "Plain text serialized Resource Description Framework document"); - public static final MediaType APPLICATION_PROJECT = register("application/vnd.ms-project", - "Microsoft Project document"); + public static final MediaType APPLICATION_RDF_TRIX = register( + "application/trix", + "Simple XML serialized Resource Description Framework document"); - public static final MediaType APPLICATION_RDF_TRIG = register("application/x-trig", - "Plain text serialized Resource Description Framework document"); + public static final MediaType APPLICATION_RDF_XML = register( + "application/rdf+xml", + "Normalized XML serialized Resource Description Framework document"); - public static final MediaType APPLICATION_RDF_TRIX = register("application/trix", - "Simple XML serialized Resource Description Framework document"); + public static final MediaType APPLICATION_RELAXNG_COMPACT = register( + "application/relax-ng-compact-syntax", + "Relax NG Schema document, Compact syntax"); - public static final MediaType APPLICATION_RDF_XML = register("application/rdf+xml", - "Normalized XML serialized Resource Description Framework document"); + public static final MediaType APPLICATION_RELAXNG_XML = register( + "application/x-relax-ng+xml", + "Relax NG Schema document, XML syntax"); - public static final MediaType APPLICATION_RELAXNG_COMPACT = register("application/relax-ng-compact-syntax", - "Relax NG Schema document, Compact syntax"); + public static final MediaType APPLICATION_RSS = register( + "application/rss+xml", "Really Simple Syndication document"); - public static final MediaType APPLICATION_RELAXNG_XML = register("application/x-relax-ng+xml", - "Relax NG Schema document, XML syntax"); + public static final MediaType APPLICATION_RTF = register("application/rtf", + "Rich Text Format document"); - public static final MediaType APPLICATION_RSS = register("application/rss+xml", - "Really Simple Syndication document"); + public static final MediaType APPLICATION_SDP = register("application/sdp", + "Session Description Protocol"); - public static final MediaType APPLICATION_RTF = register("application/rtf", "Rich Text Format document"); + public static final MediaType APPLICATION_SPARQL_RESULTS_JSON = register( + "application/sparql-results+json", + "SPARQL Query Results JSON document"); - public static final MediaType APPLICATION_SDP = register("application/sdp", "Session Description Protocol"); + public static final MediaType APPLICATION_SPARQL_RESULTS_XML = register( + "application/sparql-results+xml", + "SPARQL Query Results XML document"); - public static final MediaType APPLICATION_SPARQL_RESULTS_JSON = register("application/sparql-results+json", - "SPARQL Query Results JSON document"); + public static final MediaType APPLICATION_SPSS_SAV = register( + "application/x-spss-sav", "SPSS Data"); - public static final MediaType APPLICATION_SPARQL_RESULTS_XML = register("application/sparql-results+xml", - "SPARQL Query Results XML document"); + public static final MediaType APPLICATION_SPSS_SPS = register( + "application/x-spss-sps", "SPSS Script Syntax"); - public static final MediaType APPLICATION_SPSS_SAV = register("application/x-spss-sav", "SPSS Data"); + public static final MediaType APPLICATION_STATA_STA = register( + "application/x-stata", "Stata data file"); - public static final MediaType APPLICATION_SPSS_SPS = register("application/x-spss-sps", "SPSS Script Syntax"); + public static final MediaType APPLICATION_STUFFIT = register( + "application/x-stuffit", "Stuffit archive"); - public static final MediaType APPLICATION_STATA_STA = register("application/x-stata", "Stata data file"); + public static final MediaType APPLICATION_TAR = register( + "application/x-tar", "Tar archive"); - public static final MediaType APPLICATION_STUFFIT = register("application/x-stuffit", "Stuffit archive"); + public static final MediaType APPLICATION_TEX = register( + "application/x-tex", "Tex file"); - public static final MediaType APPLICATION_TAR = register("application/x-tar", "Tar archive"); + public static final MediaType APPLICATION_TROFF_MAN = register( + "application/x-troff-man", "LaTeX"); - public static final MediaType APPLICATION_TEX = register("application/x-tex", "Tex file"); + public static final MediaType APPLICATION_VOICEXML = register( + "application/voicexml+xml", "VoiceXML"); - public static final MediaType APPLICATION_TROFF_MAN = register("application/x-troff-man", "LaTeX"); + public static final MediaType APPLICATION_W3C_SCHEMA = register( + "application/x-xsd+xml", "W3C XML Schema document"); - public static final MediaType APPLICATION_VOICEXML = register("application/voicexml+xml", "VoiceXML"); + public static final MediaType APPLICATION_W3C_XSLT = register( + "application/xslt+xml", "W3C XSLT Stylesheet"); - public static final MediaType APPLICATION_W3C_SCHEMA = register("application/x-xsd+xml", "W3C XML Schema document"); + public static final MediaType APPLICATION_WADL = register( + "application/vnd.sun.wadl+xml", + "Web Application Description Language document"); - public static final MediaType APPLICATION_W3C_XSLT = register("application/xslt+xml", "W3C XSLT Stylesheet"); + public static final MediaType APPLICATION_WORD = register( + "application/msword", "Microsoft Word document"); - public static final MediaType APPLICATION_WADL = register("application/vnd.sun.wadl+xml", - "Web Application Description Language document"); + public static final MediaType APPLICATION_WWW_FORM = register( + "application/x-www-form-urlencoded", "Web form (URL encoded)"); - public static final MediaType APPLICATION_WORD = register("application/msword", "Microsoft Word document"); + public static final MediaType APPLICATION_XHTML = register( + "application/xhtml+xml", "XHTML document"); - public static final MediaType APPLICATION_WWW_FORM = register("application/x-www-form-urlencoded", - "Web form (URL encoded)"); + public static final MediaType APPLICATION_XMI = register( + "application/xmi+xml", "XMI document"); - public static final MediaType APPLICATION_XHTML = register("application/xhtml+xml", "XHTML document"); + public static final MediaType APPLICATION_XML = register("application/xml", + "XML document"); - public static final MediaType APPLICATION_XMI = register("application/xmi+xml", "XMI document"); + public static final MediaType APPLICATION_XML_DTD = register( + "application/xml-dtd", "XML DTD"); - public static final MediaType APPLICATION_XML = register("application/xml", "XML document"); + public static final MediaType APPLICATION_XQUERY = register( + "application/xquery", "XQuery document"); - public static final MediaType APPLICATION_XML_DTD = register("application/xml-dtd", "XML DTD"); + public static final MediaType APPLICATION_XUL = register( + "application/vnd.mozilla.xul+xml", "XUL document"); - public static final MediaType APPLICATION_XQUERY = register("application/xquery", "XQuery document"); + public static final MediaType APPLICATION_YAML = register( + "application/x-yaml", "YAML document"); - public static final MediaType APPLICATION_XUL = register("application/vnd.mozilla.xul+xml", "XUL document"); + public static final MediaType APPLICATION_ZIP = register("application/zip", + "Zip archive"); - public static final MediaType APPLICATION_YAML = register("application/x-yaml", "YAML document"); + public static final MediaType AUDIO_ALL = register("audio/*", "All audios"); - public static final MediaType APPLICATION_ZIP = register("application/zip", "Zip archive"); + public static final MediaType AUDIO_BASIC = register("audio/basic", + "AU audio"); - public static final MediaType AUDIO_ALL = register("audio/*", "All audios"); + public static final MediaType AUDIO_MIDI = register("audio/midi", + "MIDI audio"); - public static final MediaType AUDIO_BASIC = register("audio/basic", "AU audio"); + public static final MediaType AUDIO_MPEG = register("audio/mpeg", + "MPEG audio (MP3)"); - public static final MediaType AUDIO_MIDI = register("audio/midi", "MIDI audio"); + public static final MediaType AUDIO_REAL = register("audio/x-pn-realaudio", + "Real audio"); - public static final MediaType AUDIO_MPEG = register("audio/mpeg", "MPEG audio (MP3)"); + public static final MediaType AUDIO_WAV = register("audio/x-wav", + "Waveform audio"); - public static final MediaType AUDIO_REAL = register("audio/x-pn-realaudio", "Real audio"); + public static final MediaType IMAGE_ALL = register("image/*", "All images"); - public static final MediaType AUDIO_WAV = register("audio/x-wav", "Waveform audio"); + public static final MediaType IMAGE_BMP = register("image/bmp", + "Windows bitmap"); - public static final MediaType IMAGE_ALL = register("image/*", "All images"); + public static final MediaType IMAGE_GIF = register("image/gif", + "GIF image"); - public static final MediaType IMAGE_BMP = register("image/bmp", "Windows bitmap"); + public static final MediaType IMAGE_ICON = register("image/x-icon", + "Windows icon (Favicon)"); - public static final MediaType IMAGE_GIF = register("image/gif", "GIF image"); + public static final MediaType IMAGE_JPEG = register("image/jpeg", + "JPEG image"); - public static final MediaType IMAGE_ICON = register("image/x-icon", "Windows icon (Favicon)"); + public static final MediaType IMAGE_PNG = register("image/png", + "PNG image"); - public static final MediaType IMAGE_JPEG = register("image/jpeg", "JPEG image"); + public static final MediaType IMAGE_SVG = register("image/svg+xml", + "Scalable Vector Graphics"); - public static final MediaType IMAGE_PNG = register("image/png", "PNG image"); + public static final MediaType IMAGE_TIFF = register("image/tiff", + "TIFF image"); - public static final MediaType IMAGE_SVG = register("image/svg+xml", "Scalable Vector Graphics"); + public static final MediaType MESSAGE_ALL = register("message/*", + "All messages"); - public static final MediaType IMAGE_TIFF = register("image/tiff", "TIFF image"); + public static final MediaType MESSAGE_HTTP = register("message/http", + "HTTP message"); - public static final MediaType MESSAGE_ALL = register("message/*", "All messages"); + public static final MediaType MODEL_ALL = register("model/*", "All models"); - public static final MediaType MESSAGE_HTTP = register("message/http", "HTTP message"); + public static final MediaType MODEL_VRML = register("model/vrml", "VRML"); - public static final MediaType MODEL_ALL = register("model/*", "All models"); + public static final MediaType MULTIPART_ALL = register("multipart/*", + "All multipart data"); - public static final MediaType MODEL_VRML = register("model/vrml", "VRML"); + public static final MediaType MULTIPART_FORM_DATA = register( + "multipart/form-data", "Multipart form data"); - public static final MediaType MULTIPART_ALL = register("multipart/*", "All multipart data"); + public static final MediaType TEXT_ALL = register("text/*", "All texts"); - public static final MediaType MULTIPART_FORM_DATA = register("multipart/form-data", "Multipart form data"); + public static final MediaType TEXT_CALENDAR = register("text/calendar", + "iCalendar event"); - public static final MediaType TEXT_ALL = register("text/*", "All texts"); + public static final MediaType TEXT_CSS = register("text/css", + "CSS stylesheet"); - public static final MediaType TEXT_CALENDAR = register("text/calendar", "iCalendar event"); + public static final MediaType TEXT_CSV = register("text/csv", + "Comma-separated Values"); - public static final MediaType TEXT_CSS = register("text/css", "CSS stylesheet"); + public static final MediaType TEXT_DAT = register("text/x-fixed-field", + "Fixed-width Values"); - public static final MediaType TEXT_CSV = register("text/csv", "Comma-separated Values"); + public static final MediaType TEXT_HTML = register("text/html", + "HTML document"); - public static final MediaType TEXT_DAT = register("text/x-fixed-field", "Fixed-width Values"); + public static final MediaType TEXT_J2ME_APP_DESCRIPTOR = register( + "text/vnd.sun.j2me.app-descriptor", "J2ME Application Descriptor"); - public static final MediaType TEXT_HTML = register("text/html", "HTML document"); + public static final MediaType TEXT_JAVASCRIPT = register("text/javascript", + "Javascript document"); - public static final MediaType TEXT_J2ME_APP_DESCRIPTOR = register("text/vnd.sun.j2me.app-descriptor", - "J2ME Application Descriptor"); + public static final MediaType TEXT_PLAIN = register("text/plain", + "Plain text"); - public static final MediaType TEXT_JAVASCRIPT = register("text/javascript", "Javascript document"); + public static final MediaType TEXT_RDF_N3 = register("text/n3", + "N3 serialized Resource Description Framework document"); - public static final MediaType TEXT_PLAIN = register("text/plain", "Plain text"); + public static final MediaType TEXT_RDF_NTRIPLES = register("text/n-triples", + "N-Triples serialized Resource Description Framework document"); - public static final MediaType TEXT_RDF_N3 = register("text/n3", - "N3 serialized Resource Description Framework document"); + public static final MediaType TEXT_TSV = register( + "text/tab-separated-values", "Tab-separated Values"); - public static final MediaType TEXT_RDF_NTRIPLES = register("text/n-triples", - "N-Triples serialized Resource Description Framework document"); + public static final MediaType TEXT_TURTLE = register("text/turtle", + "Plain text serialized Resource Description Framework document"); - public static final MediaType TEXT_TSV = register("text/tab-separated-values", "Tab-separated Values"); + public static final MediaType TEXT_URI_LIST = register("text/uri-list", + "List of URIs"); - public static final MediaType TEXT_TURTLE = register("text/turtle", - "Plain text serialized Resource Description Framework document"); + public static final MediaType TEXT_VCARD = register("text/x-vcard", + "vCard"); - public static final MediaType TEXT_URI_LIST = register("text/uri-list", "List of URIs"); + public static final MediaType TEXT_XML = register("text/xml", "XML text"); - public static final MediaType TEXT_VCARD = register("text/x-vcard", "vCard"); + public static final MediaType TEXT_YAML = register("text/x-yaml", + "YAML document"); - public static final MediaType TEXT_XML = register("text/xml", "XML text"); + public static final MediaType VIDEO_ALL = register("video/*", "All videos"); - public static final MediaType TEXT_YAML = register("text/x-yaml", "YAML document"); + public static final MediaType VIDEO_AVI = register("video/x-msvideo", + "AVI video"); - public static final MediaType VIDEO_ALL = register("video/*", "All videos"); + public static final MediaType VIDEO_MP4 = register("video/mp4", + "MPEG-4 video"); + + public static final MediaType VIDEO_MPEG = register("video/mpeg", + "MPEG video"); + + public static final MediaType VIDEO_QUICKTIME = register("video/quicktime", + "Quicktime video"); + + public static final MediaType VIDEO_WMV = register("video/x-ms-wmv", + "Windows movie"); + + /** + * Returns the first of the most specific media type of the given array of + * {@link MediaType}s. + *

+ * Examples: + *

    + *
  • "text/plain" is more specific than "text/*" or "image/*"
  • + *
  • "text/html" is same specific as "application/pdf" or "image/jpg"
  • + *
  • "text/*" is same specific than "application/*" or "image/*"
  • + *
  • "*/*" is the most unspecific MediaType
  • + *
+ * + * @param mediaTypes An array of media types. + * @return The most concrete MediaType. + * @throws IllegalArgumentException If the array is null or empty. + */ + public static MediaType getMostSpecific(MediaType... mediaTypes) + throws IllegalArgumentException { + if ((mediaTypes == null) || (mediaTypes.length == 0)) { + throw new IllegalArgumentException( + "You must give at least one MediaType"); + } + + if (mediaTypes.length == 1) { + return mediaTypes[0]; + } + + MediaType mostSpecific = mediaTypes[0]; + + for (int i = 1; i < mediaTypes.length; i++) { + MediaType mediaType = mediaTypes[i]; + + if (mediaType != null) { + if (mediaType.getMainType().equals("*")) { + continue; + } + + if (mostSpecific.getMainType().equals("*")) { + mostSpecific = mediaType; + continue; + } + + if (mostSpecific.getSubType().contains("*")) { + mostSpecific = mediaType; + continue; + } + } + } + + return mostSpecific; + } - public static final MediaType VIDEO_AVI = register("video/x-msvideo", "AVI video"); + /** + * Returns the known media types map. + * + * @return the known media types map. + */ + private static Map getTypes() { + if (_types == null) { + _types = new HashMap(); + } + return _types; + } - public static final MediaType VIDEO_MP4 = register("video/mp4", "MPEG-4 video"); + /** + * Normalizes the specified token. + * + * @param token Token to normalize. + * @return The normalized token. + * @throws IllegalArgumentException if token is not legal. + */ + private static String normalizeToken(String token) { + int length; + char c; + + // Makes sure we're not dealing with a "*" token. + token = token.trim(); + if (token.isEmpty() || "*".equals(token)) + return "*"; + + // Makes sure the token is RFC compliant. + length = token.length(); + for (int i = 0; i < length; i++) { + c = token.charAt(i); + if (c <= 32 || c >= 127 || _TSPECIALS.indexOf(c) != -1) + throw new IllegalArgumentException("Illegal token: " + token); + } + + return token; + } - public static final MediaType VIDEO_MPEG = register("video/mpeg", "MPEG video"); + /** + * Normalizes the specified media type. + * + * @param name The name of the type to normalize. + * @param parameters The parameters of the type to normalize. + * @return The normalized type. + */ + private static String normalizeType(String name, + Series parameters) { + int slashIndex; + int colonIndex; + String mainType; + String subType; + StringBuilder params = null; + + // Ignore null names (backward compatibility). + if (name == null) + return null; + + // Check presence of parameters + if ((colonIndex = name.indexOf(';')) != -1) { + params = new StringBuilder(name.substring(colonIndex)); + name = name.substring(0, colonIndex); + } + + // No main / sub separator, assumes name/*. + if ((slashIndex = name.indexOf('/')) == -1) { + mainType = normalizeToken(name); + subType = "*"; + } else { + // Normalizes the main and sub types. + mainType = normalizeToken(name.substring(0, slashIndex)); + subType = normalizeToken(name.substring(slashIndex + 1)); + } + + // Merge parameters taken from the name and the method argument. + if (parameters != null && !parameters.isEmpty()) { + try { + if (params == null) { + params = new StringBuilder(); + } + HeaderWriter hw = new HeaderWriter() { + @Override + public HeaderWriter append(Parameter value) { + return appendExtension(value); + } + }; + for (int i = 0; i < parameters.size(); i++) { + Parameter p = parameters.get(i); + hw.appendParameterSeparator(); + hw.appendSpace(); + hw.append(p); + } + params.append(hw.toString()); + hw.close(); + } catch (IOException e) { + Context.getCurrentLogger().log(Level.INFO, + "Unable to parse the media type parameter", e); + } + } + + return (params == null) ? mainType + '/' + subType + : mainType + '/' + subType + params.toString(); + } - public static final MediaType VIDEO_QUICKTIME = register("video/quicktime", "Quicktime video"); + /** + * Register a media type as a known type that can later be retrieved using + * {@link #valueOf(String)}. If the type already exists, the existing type + * is returned, otherwise a new instance is created. + * + * @param name The name. + * @param description The description. + * @return The registered media type + */ + public static synchronized MediaType register(String name, + String description) { - public static final MediaType VIDEO_WMV = register("video/x-ms-wmv", "Windows movie"); + if (!getTypes().containsKey(name)) { + final MediaType type = new MediaType(name, description); + getTypes().put(name, type); + } - /** - * Returns the first of the most specific media type of the given array of - * {@link MediaType}s. - *

- * Examples: - *

    - *
  • "text/plain" is more specific than "text/*" or "image/*"
  • - *
  • "text/html" is same specific as "application/pdf" or "image/jpg"
  • - *
  • "text/*" is same specific than "application/*" or "image/*"
  • - *
  • "*/*" is the most unspecific MediaType
  • - *
- * - * @param mediaTypes An array of media types. - * @return The most concrete MediaType. - * @throws IllegalArgumentException If the array is null or empty. - */ - public static MediaType getMostSpecific(MediaType... mediaTypes) throws IllegalArgumentException { - if ((mediaTypes == null) || (mediaTypes.length == 0)) { - throw new IllegalArgumentException("You must give at least one MediaType"); - } + return getTypes().get(name); + } - if (mediaTypes.length == 1) { - return mediaTypes[0]; - } + /** + * Returns the media type associated to a name. If an existing constant + * exists then it is returned, otherwise a new instance is created. + * + * @param name The name. + * @return The associated media type. + */ + public static MediaType valueOf(String name) { + MediaType result = null; - MediaType mostSpecific = mediaTypes[0]; + if (!StringUtils.isNullOrEmpty(name)) { + result = getTypes().get(name); + if (result == null) { + result = new MediaType(name); + } + } - for (int i = 1; i < mediaTypes.length; i++) { - MediaType mediaType = mediaTypes[i]; + return result; + } - if (mediaType != null) { - if (mediaType.getMainType().equals("*")) { - continue; - } - - if (mostSpecific.getMainType().equals("*")) { - mostSpecific = mediaType; - continue; - } - - if (mostSpecific.getSubType().contains("*")) { - mostSpecific = mediaType; - continue; - } - } - } - - return mostSpecific; - } - - /** - * Returns the known media types map. - * - * @return the known media types map. - */ - private static Map getTypes() { - if (_types == null) { - _types = new HashMap(); - } - return _types; - } - - /** - * Normalizes the specified token. - * - * @param token Token to normalize. - * @return The normalized token. - * @throws IllegalArgumentException if token is not legal. - */ - private static String normalizeToken(String token) { - int length; - char c; - - // Makes sure we're not dealing with a "*" token. - token = token.trim(); - if (token.isEmpty() || "*".equals(token)) - return "*"; - - // Makes sure the token is RFC compliant. - length = token.length(); - for (int i = 0; i < length; i++) { - c = token.charAt(i); - if (c <= 32 || c >= 127 || _TSPECIALS.indexOf(c) != -1) - throw new IllegalArgumentException("Illegal token: " + token); - } - - return token; - } - - /** - * Normalizes the specified media type. - * - * @param name The name of the type to normalize. - * @param parameters The parameters of the type to normalize. - * @return The normalized type. - */ - private static String normalizeType(String name, Series parameters) { - int slashIndex; - int colonIndex; - String mainType; - String subType; - StringBuilder params = null; - - // Ignore null names (backward compatibility). - if (name == null) - return null; - - // Check presence of parameters - if ((colonIndex = name.indexOf(';')) != -1) { - params = new StringBuilder(name.substring(colonIndex)); - name = name.substring(0, colonIndex); - } - - // No main / sub separator, assumes name/*. - if ((slashIndex = name.indexOf('/')) == -1) { - mainType = normalizeToken(name); - subType = "*"; - } else { - // Normalizes the main and sub types. - mainType = normalizeToken(name.substring(0, slashIndex)); - subType = normalizeToken(name.substring(slashIndex + 1)); - } - - // Merge parameters taken from the name and the method argument. - if (parameters != null && !parameters.isEmpty()) { - try { - if (params == null) { - params = new StringBuilder(); - } - HeaderWriter hw = new HeaderWriter() { - @Override - public HeaderWriter append(Parameter value) { - return appendExtension(value); - } - }; - for (int i = 0; i < parameters.size(); i++) { - Parameter p = parameters.get(i); - hw.appendParameterSeparator(); - hw.appendSpace(); - hw.append(p); - } - params.append(hw.toString()); - hw.close(); - } catch (IOException e) { - Context.getCurrentLogger().log(Level.INFO, "Unable to parse the media type parameter", e); - } - } - - return (params == null) ? mainType + '/' + subType : mainType + '/' + subType + params.toString(); - } - - /** - * Register a media type as a known type that can later be retrieved using - * {@link #valueOf(String)}. If the type already exists, the existing type is - * returned, otherwise a new instance is created. - * - * @param name The name. - * @param description The description. - * @return The registered media type - */ - public static synchronized MediaType register(String name, String description) { - - if (!getTypes().containsKey(name)) { - final MediaType type = new MediaType(name, description); - getTypes().put(name, type); - } - - return getTypes().get(name); - } - - /** - * Returns the media type associated to a name. If an existing constant exists - * then it is returned, otherwise a new instance is created. - * - * @param name The name. - * @return The associated media type. - */ - public static MediaType valueOf(String name) { - MediaType result = null; - - if (!StringUtils.isNullOrEmpty(name)) { - result = getTypes().get(name); - if (result == null) { - result = new MediaType(name); - } - } - - return result; - } - - /** The list of parameters. */ - private volatile Series parameters; + /** The list of parameters. */ + private volatile Series parameters; /** * Constructor that clones an original media type. @@ -619,15 +734,16 @@ public MediaType(MediaType original, String paramName, String paramValue) { /** * Constructor that clones an original media type. * - * @param original The original media type to clone. - * @param parameter The unique parameter to set. + * @param original The original media type to clone. + * @param parameter The unique parameter to set. */ public MediaType(MediaType original, Parameter parameter) { this(original, parameter == null ? null : parameter.createSeries()); } /** - * Constructor that clones an original media type. + * Constructor that clones an original media type by extracting its parent + * media type then adding a new set of parameters. * * @param original The original media type to clone. * @param parameters The list of parameters to set. @@ -637,279 +753,288 @@ public MediaType(MediaType original, Series parameters) { (original == null) ? null : original.getDescription()); } - /** - * Constructor. - * - * @param name The name. - */ - public MediaType(String name) { - this(name, null, "Media type or range of media types"); - } - - /** - * Constructor. - * - * @param name The name. - * @param parameters The list of parameters. - */ - public MediaType(String name, Series parameters) { - this(name, parameters, "Media type or range of media types"); - } - - /** - * Constructor. - * - * @param name The name. - * @param parameters The list of parameters. - * @param description The description. - */ - @SuppressWarnings("unchecked") - public MediaType(String name, Series parameters, String description) { - super(normalizeType(name, parameters), description); - - if (parameters != null) { - this.parameters = (Series) Series.unmodifiableSeries(parameters); - } - } - - /** - * Constructor. - * - * @param name The name. - * @param description The description. - */ - public MediaType(String name, String description) { - this(name, null, description); - } - - /** {@inheritDoc} */ - @Override - public boolean equals(Object obj) { - return equals(obj, false); - } - - /** - * Test the equality of two media types, with the possibility to ignore the - * parameters. - * - * @param obj The object to compare to. - * @param ignoreParameters Indicates if parameters should be ignored during - * comparison. - * @return True if both media types are equal. - */ - public boolean equals(Object obj, boolean ignoreParameters) { - boolean result = (obj == this); - - // if obj == this no need to go further - if (!result) { - // if obj isn't a mediatype or is null don't evaluate further - if (obj instanceof MediaType) { - final MediaType that = (MediaType) obj; - if (getMainType().equals(that.getMainType()) && getSubType().equals(that.getSubType())) { - result = ignoreParameters || getParameters().equals(that.getParameters()); - } - } - } - - return result; - } - - /** - * Returns the main type. - * - * @return The main type. - */ - public String getMainType() { - String result = null; - - if (getName() != null) { - int index = getName().indexOf('/'); - - // Some clients appear to use name types without subtypes - if (index == -1) { - index = getName().indexOf(';'); - } - - if (index == -1) { - result = getName(); - } else { - result = getName().substring(0, index); - } - } - - return result; - } - - /** - * Returns the unmodifiable list of parameters corresponding to subtype - * modifiers. Creates a new instance if no one has been set. - * - * @return The list of parameters. - */ - @SuppressWarnings("unchecked") - public Series getParameters() { - // Lazy initialization with double-check. - Series p = this.parameters; - if (p == null) { - synchronized (this) { - p = this.parameters; - if (p == null) { - Series params = null; - - if (getName() != null) { - int index = getName().indexOf(';'); - - if (index != -1) { - params = new Form(getName().substring(index + 1).trim(), ';'); - } - } - - if (params == null) { - params = new Series(Parameter.class); - } - - this.parameters = p = (Series) Series.unmodifiableSeries(params); - } - } - } - return p; - } - - /** - * {@inheritDoc}
- * In case the media type has parameters, this method returns the concatenation - * of the main type and the subtype. If the subtype is not equal to "*", it - * returns the concatenation of the main type and "*". Otherwise, it returns - * either the {@link #ALL} media type if it is already the {@link #ALL} media - * type, or null. - */ - @Override - public MediaType getParent() { - MediaType result = null; - - if (getParameters().size() > 0) { - result = MediaType.valueOf(getMainType() + "/" + getSubType()); - } else { - if (getSubType().equals("*")) { - result = equals(ALL) ? null : ALL; - } else { - result = MediaType.valueOf(getMainType() + "/*"); - } - } - - return result; - } - - /** - * Returns the sub-type. - * - * @return The sub-type. - */ - public String getSubType() { - String result = null; - - if (getName() != null) { - final int slash = getName().indexOf('/'); - - if (slash == -1) { - // No subtype found, assume that all subtypes are accepted - result = "*"; - } else { - final int separator = getName().indexOf(';'); - if (separator == -1) { - result = getName().substring(slash + 1); - } else { - result = getName().substring(slash + 1, separator); - } - } - } - - return result; - } - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return SystemUtils.hashCode(super.hashCode(), getParameters()); - } - - /** - * Indicates if a given media type is included in the current one @see - * {@link #includes(Metadata, boolean)}. It ignores the parameters. - * - * @param included The media type to test for inclusion. - * @return True if the given media type is included in the current one. - * @see #isCompatible(Metadata) - */ - @Override - public boolean includes(Metadata included) { - return includes(included, true); - } - - /** - * Indicates if a given media type is included in the current one @see - * {@link #includes(Metadata, boolean)}. The test is true if both types are - * equal or if the given media type is within the range of the current one. For - * example, ALL includes all media types. Parameters are ignored for this - * comparison. A null media type is considered as included into the current one. - * It ignores the parameters. - *

- * Examples: - *

    - *
  • TEXT_ALL.includes(TEXT_PLAIN) returns true
  • - *
  • TEXT_PLAIN.includes(TEXT_ALL) returns false
  • - *
- * - * @param included The media type to test for inclusion. - * @return True if the given media type is included in the current one. - * @see #isCompatible(Metadata) - */ - public boolean includes(Metadata included, boolean ignoreParameters) { - boolean result = equals(ALL) || equals(included); - - if (!result && (included instanceof MediaType)) { - MediaType includedMediaType = (MediaType) included; - - if (getMainType().equals(includedMediaType.getMainType())) { - // Both media types are different - if (getSubType().equals(includedMediaType.getSubType())) { - if (ignoreParameters) { - result = true; - } else { - // Check parameters: - // Media type A includes media type B if for each param - // name/value pair in A, B contains the same name/value. - result = true; - for (int i = 0; result && i < getParameters().size(); i++) { - Parameter param = getParameters().get(i); - Parameter includedParam = includedMediaType.getParameters().getFirst(param.getName()); - - // If there was no param with the same name, or the - // param with the same name had a different value, - // then no match. - result = (includedParam != null && param.getValue().equals(includedParam.getValue())); - } - } - } else if (getSubType().equals("*")) { - result = true; - } else if (getSubType().startsWith("*+") - && includedMediaType.getSubType().endsWith(getSubType().substring(2))) { - result = true; - } - } - } - - return result; - } - - /** - * Checks if the current media type is concrete. A media type is concrete if - * neither the main type nor the sub-type are equal to "*". - * - * @return True if this media type is concrete. - */ - public boolean isConcrete() { - return !getName().contains("*"); - } + /** + * Constructor. + * + * @param name The name. + */ + public MediaType(String name) { + this(name, null, "Media type or range of media types"); + } + + /** + * Constructor. + * + * @param name The name. + * @param parameters The list of parameters. + */ + public MediaType(String name, Series parameters) { + this(name, parameters, "Media type or range of media types"); + } + + /** + * Constructor. + * + * @param name The name. + * @param parameters The list of parameters. + * @param description The description. + */ + @SuppressWarnings("unchecked") + public MediaType(String name, Series parameters, + String description) { + super(normalizeType(name, parameters), description); + + if (parameters != null) { + this.parameters = (Series) Series + .unmodifiableSeries(parameters); + } + } + + /** + * Constructor. + * + * @param name The name. + * @param description The description. + */ + public MediaType(String name, String description) { + this(name, null, description); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object obj) { + return equals(obj, false); + } + + /** + * Test the equality of two media types, with the possibility to ignore the + * parameters. + * + * @param obj The object to compare to. + * @param ignoreParameters Indicates if parameters should be ignored during + * comparison. + * @return True if both media types are equal. + */ + public boolean equals(Object obj, boolean ignoreParameters) { + boolean result = (obj == this); + + // if obj == this no need to go further + if (!result) { + // if obj isn't a mediatype or is null don't evaluate further + if (obj instanceof MediaType) { + final MediaType that = (MediaType) obj; + if (getMainType().equals(that.getMainType()) + && getSubType().equals(that.getSubType())) { + result = ignoreParameters + || getParameters().equals(that.getParameters()); + } + } + } + + return result; + } + + /** + * Returns the main type. + * + * @return The main type. + */ + public String getMainType() { + String result = null; + + if (getName() != null) { + int index = getName().indexOf('/'); + + // Some clients appear to use name types without subtypes + if (index == -1) { + index = getName().indexOf(';'); + } + + if (index == -1) { + result = getName(); + } else { + result = getName().substring(0, index); + } + } + + return result; + } + + /** + * Returns the unmodifiable list of parameters corresponding to subtype + * modifiers. Creates a new instance if no one has been set. + * + * @return The list of parameters. + */ + @SuppressWarnings("unchecked") + public Series getParameters() { + // Lazy initialization with double-check. + Series p = this.parameters; + if (p == null) { + synchronized (this) { + p = this.parameters; + if (p == null) { + Series params = null; + + if (getName() != null) { + int index = getName().indexOf(';'); + + if (index != -1) { + params = new Form( + getName().substring(index + 1).trim(), ';'); + } + } + + if (params == null) { + params = new Series(Parameter.class); + } + + this.parameters = p = (Series) Series + .unmodifiableSeries(params); + } + } + } + return p; + } + + /** + * {@inheritDoc}
+ * In case the media type has parameters, this method returns the + * concatenation of the main type and the subtype. If the subtype is not + * equal to "*", it returns the concatenation of the main type and "*". + * Otherwise, it returns either the {@link #ALL} media type if it is already + * the {@link #ALL} media type, or null. + */ + @Override + public MediaType getParent() { + MediaType result = null; + + if (getParameters().size() > 0) { + result = MediaType.valueOf(getMainType() + "/" + getSubType()); + } else { + if (getSubType().equals("*")) { + result = equals(ALL) ? null : ALL; + } else { + result = MediaType.valueOf(getMainType() + "/*"); + } + } + + return result; + } + + /** + * Returns the sub-type. + * + * @return The sub-type. + */ + public String getSubType() { + String result = null; + + if (getName() != null) { + final int slash = getName().indexOf('/'); + + if (slash == -1) { + // No subtype found, assume that all subtypes are accepted + result = "*"; + } else { + final int separator = getName().indexOf(';'); + if (separator == -1) { + result = getName().substring(slash + 1); + } else { + result = getName().substring(slash + 1, separator); + } + } + } + + return result; + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return SystemUtils.hashCode(super.hashCode(), getParameters()); + } + + /** + * Indicates if a given media type is included in the current one @see + * {@link #includes(Metadata, boolean)}. It ignores the parameters. + * + * @param included The media type to test for inclusion. + * @return True if the given media type is included in the current one. + * @see #isCompatible(Metadata) + */ + @Override + public boolean includes(Metadata included) { + return includes(included, true); + } + + /** + * Indicates if a given media type is included in the current one @see + * {@link #includes(Metadata, boolean)}. The test is true if both types are + * equal or if the given media type is within the range of the current one. + * For example, ALL includes all media types. Parameters are ignored for + * this comparison. A null media type is considered as included into the + * current one. It ignores the parameters. + *

+ * Examples: + *

    + *
  • TEXT_ALL.includes(TEXT_PLAIN) returns true
  • + *
  • TEXT_PLAIN.includes(TEXT_ALL) returns false
  • + *
+ * + * @param included The media type to test for inclusion. + * @return True if the given media type is included in the current one. + * @see #isCompatible(Metadata) + */ + public boolean includes(Metadata included, boolean ignoreParameters) { + boolean result = equals(ALL) || equals(included); + + if (!result && (included instanceof MediaType)) { + MediaType includedMediaType = (MediaType) included; + + if (getMainType().equals(includedMediaType.getMainType())) { + // Both media types are different + if (getSubType().equals(includedMediaType.getSubType())) { + if (ignoreParameters) { + result = true; + } else { + // Check parameters: + // Media type A includes media type B if for each param + // name/value pair in A, B contains the same name/value. + result = true; + for (int i = 0; result + && i < getParameters().size(); i++) { + Parameter param = getParameters().get(i); + Parameter includedParam = includedMediaType + .getParameters().getFirst(param.getName()); + + // If there was no param with the same name, or the + // param with the same name had a different value, + // then no match. + result = (includedParam != null && param.getValue() + .equals(includedParam.getValue())); + } + } + } else if (getSubType().equals("*")) { + result = true; + } else if (getSubType().startsWith("*+") && includedMediaType + .getSubType().endsWith(getSubType().substring(2))) { + result = true; + } + } + } + + return result; + } + + /** + * Checks if the current media type is concrete. A media type is concrete if + * neither the main type nor the sub-type are equal to "*". + * + * @return True if this media type is concrete. + */ + public boolean isConcrete() { + return !getName().contains("*"); + } } From 114b44c2ab4dba60d4518a44c0581ab167b4221c Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:03:18 -0400 Subject: [PATCH 14/21] Renamed MultiPartFormDataRep to MultiPartRep Eventually the logic could be reused for related media types --- ...tion.java => MultiPartRepresentation.java} | 22 +++++++++---------- .../ext/jetty/MultiPartFormTestCase.java | 6 ++--- 2 files changed, 14 insertions(+), 14 deletions(-) rename org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/{MultiPartFormDataRepresentation.java => MultiPartRepresentation.java} (92%) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java similarity index 92% rename from org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java rename to org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index 8c4cb4d14f..e8601e64de 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartFormDataRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -36,7 +36,7 @@ * * @author Jerome Louvel */ -public class MultiPartFormDataRepresentation extends InputRepresentation { +public class MultiPartRepresentation extends InputRepresentation { /** * Sets a boundary to an existing media type. If the original mediatype @@ -111,7 +111,7 @@ public static String getBoundary(MediaType mediaType) { * * @param parts The source parts to use when generating the representation. */ - public MultiPartFormDataRepresentation(Part... parts) { + public MultiPartRepresentation(Part... parts) { this(Arrays.asList(parts)); } @@ -122,7 +122,7 @@ public MultiPartFormDataRepresentation(Part... parts) { * * @param parts The source parts to use when generating the representation. */ - public MultiPartFormDataRepresentation(String boundary, Part... parts) { + public MultiPartRepresentation(String boundary, Part... parts) { this(boundary, Arrays.asList(parts)); } @@ -133,7 +133,7 @@ public MultiPartFormDataRepresentation(String boundary, Part... parts) { * * @param parts The source parts to use when generating the representation. */ - public MultiPartFormDataRepresentation(List parts) { + public MultiPartRepresentation(List parts) { this(MultiPart.generateBoundary(null, 24), parts); } @@ -146,7 +146,7 @@ public MultiPartFormDataRepresentation(List parts) { * @param parts The source parts to use when generating the * representation. */ - public MultiPartFormDataRepresentation(String boundary, List parts) { + public MultiPartRepresentation(String boundary, List parts) { this(MediaType.MULTIPART_FORM_DATA, boundary, parts); } @@ -160,7 +160,7 @@ public MultiPartFormDataRepresentation(String boundary, List parts) { * @param parts The source parts to use when generating the * representation. */ - public MultiPartFormDataRepresentation(MediaType mediaType, String boundary, + public MultiPartRepresentation(MediaType mediaType, String boundary, List parts) { super(null, setBoundary(mediaType, boundary)); this.boundary = boundary; @@ -178,7 +178,7 @@ public MultiPartFormDataRepresentation(MediaType mediaType, String boundary, * @param config The multipart configuration. * @throws IOException */ - public MultiPartFormDataRepresentation(Representation multiPartEntity, + public MultiPartRepresentation(Representation multiPartEntity, MultiPartConfig config) throws IOException { this(ContentType.writeHeader(multiPartEntity), multiPartEntity.getStream(), config); @@ -196,7 +196,7 @@ public MultiPartFormDataRepresentation(Representation multiPartEntity, * easier access. * @throws IOException */ - public MultiPartFormDataRepresentation(Representation multiPartEntity, + public MultiPartRepresentation(Representation multiPartEntity, Path storageLocation) throws IOException { this(multiPartEntity, new MultiPartConfig.Builder() .location(storageLocation).build()); @@ -213,7 +213,7 @@ public MultiPartFormDataRepresentation(Representation multiPartEntity, * @param config The multipart configuration. * @throws IOException */ - public MultiPartFormDataRepresentation(String contentType, + public MultiPartRepresentation(String contentType, InputStream multiPartEntity, MultiPartConfig config) throws IOException { super(null, MediaType.MULTIPART_FORM_DATA); @@ -241,9 +241,9 @@ public InvocationType getInvocationType() { @Override public void succeeded(MultiPartFormData.Parts parts) { // Store the resulting parts - MultiPartFormDataRepresentation.this.parts = new ArrayList<>(); + MultiPartRepresentation.this.parts = new ArrayList<>(); parts.iterator().forEachRemaining( - part -> MultiPartFormDataRepresentation.this.parts + part -> MultiPartRepresentation.this.parts .add(part)); } }); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java index f5379bac91..f8a85471fd 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test; /** - * Test case for the {@link MultiPartFormDataRepresentation} class in multipart + * Test case for the {@link MultiPartRepresentation} class in multipart * mode. * * @author Jerome Louvel @@ -43,7 +43,7 @@ public void testWriteFromParts() throws IOException { final String boundary = "-----------------------------1294919323195"; - MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation( + MultiPartRepresentation rep = new MultiPartRepresentation( contentSourcePart, filePart); rep.setBoundary(boundary); @@ -70,7 +70,7 @@ public void testContentType() { MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart( "field", null, HttpFields.EMPTY, new StringRequestContent("foo")); - MultiPartFormDataRepresentation rep = new MultiPartFormDataRepresentation( + MultiPartRepresentation rep = new MultiPartRepresentation( "myInitialBoundary", contentSourcePart); rep.setBoundary("myActualBoundary"); assertEquals("multipart/form-data; boundary=myActualBoundary", From 7417cab648c50ec32ad081ed97f23ae272e37f31 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:05:39 -0400 Subject: [PATCH 15/21] Update MultiPartRepresentation.java --- .../ext/jetty/MultiPartRepresentation.java | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index e8601e64de..724a0fce44 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -38,29 +38,6 @@ */ public class MultiPartRepresentation extends InputRepresentation { - /** - * Sets a boundary to an existing media type. If the original mediatype - * already has a "boundary" parameter, it will be erased. * - * - * @param mediaType The media type to update. - * @param boundary The boundary to add as a parameter. - * @return The updated media type. - */ - public static MediaType setBoundary(MediaType mediaType, String boundary) { - MediaType result = null; - - if (mediaType != null) { - if (mediaType.getParameters().getFirst("boundary") != null) { - result = new MediaType(mediaType.getParent(), "boundary", - boundary); - } else { - result = new MediaType(mediaType, "boundary", boundary); - } - } - - return result; - } - /** * Creates a #{@link Part} object based on a {@link Representation} plus * metadata. @@ -97,34 +74,35 @@ public static String getBoundary(MediaType mediaType) { } /** - * The boundary used to separate each part for the parsed or generated form. + * Sets a boundary to an existing media type. If the original mediatype + * already has a "boundary" parameter, it will be erased. * + * + * @param mediaType The media type to update. + * @param boundary The boundary to add as a parameter. + * @return The updated media type. */ - private volatile String boundary; + public static MediaType setBoundary(MediaType mediaType, String boundary) { + MediaType result = null; - /** The wrapped multipart form data either parsed or to be generated. */ - private volatile List parts; + if (mediaType != null) { + if (mediaType.getParameters().getFirst("boundary") != null) { + result = new MediaType(mediaType.getParent(), "boundary", + boundary); + } else { + result = new MediaType(mediaType, "boundary", boundary); + } + } - /** - * Constructor that wraps multiple parts, set a random boundary, then - * GENERATES the content via {@link #getStream()} as a - * {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param parts The source parts to use when generating the representation. - */ - public MultiPartRepresentation(Part... parts) { - this(Arrays.asList(parts)); + return result; } /** - * Constructor that wraps multiple parts, set a boundary, then GENERATES the - * content via {@link #getStream()} as a - * {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param parts The source parts to use when generating the representation. + * The boundary used to separate each part for the parsed or generated form. */ - public MultiPartRepresentation(String boundary, Part... parts) { - this(boundary, Arrays.asList(parts)); - } + private volatile String boundary; + + /** The wrapped multipart form data either parsed or to be generated. */ + private volatile List parts; /** * Constructor that wraps multiple parts, set a random boundary, then @@ -137,19 +115,6 @@ public MultiPartRepresentation(List parts) { this(MultiPart.generateBoundary(null, 24), parts); } - /** - * Constructor that wraps multiple parts, set a boundary, then GENERATES the - * content via {@link #getStream()} as a - * {@link MediaType#MULTIPART_FORM_DATA}. - * - * @param boundary The boundary to add as a parameter. - * @param parts The source parts to use when generating the - * representation. - */ - public MultiPartRepresentation(String boundary, List parts) { - this(MediaType.MULTIPART_FORM_DATA, boundary, parts); - } - /** * Constructor that wraps multiple parts, set a media type with a boundary, * then GENERATES the content via {@link #getStream()} as a @@ -167,6 +132,17 @@ public MultiPartRepresentation(MediaType mediaType, String boundary, this.parts = parts; } + /** + * Constructor that wraps multiple parts, set a random boundary, then + * GENERATES the content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param parts The source parts to use when generating the representation. + */ + public MultiPartRepresentation(Part... parts) { + this(Arrays.asList(parts)); + } + /** * Constructor that PARSES the content based on a given configuration into * {@link #getParts()}. @@ -250,6 +226,30 @@ public void succeeded(MultiPartFormData.Parts parts) { } } + /** + * Constructor that wraps multiple parts, set a boundary, then GENERATES the + * content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param boundary The boundary to add as a parameter. + * @param parts The source parts to use when generating the + * representation. + */ + public MultiPartRepresentation(String boundary, List parts) { + this(MediaType.MULTIPART_FORM_DATA, boundary, parts); + } + + /** + * Constructor that wraps multiple parts, set a boundary, then GENERATES the + * content via {@link #getStream()} as a + * {@link MediaType#MULTIPART_FORM_DATA}. + * + * @param parts The source parts to use when generating the representation. + */ + public MultiPartRepresentation(String boundary, Part... parts) { + this(boundary, Arrays.asList(parts)); + } + /** * Returns the boundary used to separate each part for the parsed or * generated form. From 2f46349464216241a36c3393969f6befa2bff242 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:12:54 -0400 Subject: [PATCH 16/21] Fixed multipart parsing logic Also added related unit test --- .../ext/jetty/MultiPartRepresentation.java | 89 +++++++++++-------- ...a => MultiPartRepresentationTestCase.java} | 51 ++++++++++- 2 files changed, 101 insertions(+), 39 deletions(-) rename org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/{MultiPartFormTestCase.java => MultiPartRepresentationTestCase.java} (58%) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index 724a0fce44..2a09aaae7b 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -26,7 +26,6 @@ import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Promise; import org.restlet.data.MediaType; -import org.restlet.engine.header.ContentType; import org.restlet.representation.InputRepresentation; import org.restlet.representation.Representation; @@ -156,8 +155,8 @@ public MultiPartRepresentation(Part... parts) { */ public MultiPartRepresentation(Representation multiPartEntity, MultiPartConfig config) throws IOException { - this(ContentType.writeHeader(multiPartEntity), - multiPartEntity.getStream(), config); + this(multiPartEntity.getMediaType(), multiPartEntity.getStream(), + config); } /** @@ -182,47 +181,65 @@ public MultiPartRepresentation(Representation multiPartEntity, * Constructor that PARSES the content based on a given configuration into * {@link #getParts()}. * - * @param contentType The media type that should be based on + * @param mediaType The media type that should be based on * {@link MediaType#MULTIPART_FORM_DATA}, with a * "boundary" parameter. * @param multiPartEntity The multipart entity to parse. * @param config The multipart configuration. * @throws IOException */ - public MultiPartRepresentation(String contentType, + public MultiPartRepresentation(MediaType mediaType, InputStream multiPartEntity, MultiPartConfig config) throws IOException { - super(null, MediaType.MULTIPART_FORM_DATA); - this.boundary = boundary; - - if (multiPartEntity != null) { - Content.Source contentSource = Content.Source.from(multiPartEntity); - Attributes.Mapped attributes = new Attributes.Mapped(); - - // Convert the request content into parts. - MultiPartFormData.onParts(contentSource, attributes, contentType, - config, new Promise.Invocable<>() { - @Override - public void failed(Throwable failure) { - throw new IllegalStateException( - "Unable to parse the multipart form data representation", - failure); - } - - @Override - public InvocationType getInvocationType() { - return InvocationType.BLOCKING; - } - - @Override - public void succeeded(MultiPartFormData.Parts parts) { - // Store the resulting parts - MultiPartRepresentation.this.parts = new ArrayList<>(); - parts.iterator().forEachRemaining( - part -> MultiPartRepresentation.this.parts - .add(part)); - } - }); + super(null, mediaType); + + if (MediaType.MULTIPART_FORM_DATA.equals(getMediaType(), true)) { + this.boundary = getMediaType().getParameters() + .getFirstValue("boundary"); + + if (this.boundary != null) { + if (multiPartEntity != null) { + Content.Source contentSource = Content.Source + .from(multiPartEntity); + Attributes.Mapped attributes = new Attributes.Mapped(); + + // Convert the request content into parts. + MultiPartFormData.onParts(contentSource, attributes, + mediaType.toString(), config, + new Promise.Invocable<>() { + @Override + public void failed(Throwable failure) { + throw new IllegalStateException( + "Unable to parse the multipart form data representation", + failure); + } + + @Override + public InvocationType getInvocationType() { + return InvocationType.BLOCKING; + } + + @Override + public void succeeded( + MultiPartFormData.Parts parts) { + // Store the resulting parts + MultiPartRepresentation.this.parts = new ArrayList<>(); + parts.iterator().forEachRemaining( + part -> MultiPartRepresentation.this.parts + .add(part)); + } + }); + } else { + throw new IllegalArgumentException( + "The multipart entity can't be null"); + } + } else { + throw new IllegalArgumentException( + "The content type must have a \"boundary\" parameter"); + } + } else { + throw new IllegalArgumentException( + "The content type must be \"multipart/form-data\" with a \"boundary\" parameter"); } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartRepresentationTestCase.java similarity index 58% rename from org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java rename to org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartRepresentationTestCase.java index f8a85471fd..70a6760edd 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartFormTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/MultiPartRepresentationTestCase.java @@ -10,8 +10,10 @@ package org.restlet.ext.jetty; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -19,15 +21,17 @@ import org.eclipse.jetty.client.StringRequestContent; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.MultiPart; +import org.eclipse.jetty.http.MultiPart.Part; import org.junit.jupiter.api.Test; +import org.restlet.data.MediaType; +import org.restlet.representation.StringRepresentation; /** - * Test case for the {@link MultiPartRepresentation} class in multipart - * mode. + * Test case for the {@link MultiPartRepresentation} class in multipart mode. * * @author Jerome Louvel */ -public class MultiPartFormTestCase { +public class MultiPartRepresentationTestCase { @Test public void testWriteFromParts() throws IOException { @@ -76,4 +80,45 @@ public void testContentType() { assertEquals("multipart/form-data; boundary=myActualBoundary", rep.getMediaType().toString()); } + + @Test + public void testParseIntoParts() throws IOException { + final String boundary = "-----------------------------1294919323195"; + final String multipartEntityContent = """ + --%s\r + Content-Disposition: form-data; name="field"\r + \r + foo\r + --%s\r + Content-Disposition: form-data; name="icon"; filename="text.txt"\r + \r + this is the content of the file\r + --%s--\r + """ + .replace("%s", boundary); + + StringRepresentation multipartEntity = new StringRepresentation( + multipartEntityContent); + multipartEntity.setMediaType( + MediaType.valueOf("multipart/form-data; boundary=" + boundary)); + Path tempDir = Files + .createTempDirectory("multipartRepresentationTestCase"); + MultiPartRepresentation rep = new MultiPartRepresentation( + multipartEntity, tempDir); + + Part part1 = rep.getParts().get(0); + assertEquals("field", part1.getName()); + assertNull(part1.getFileName()); + assertEquals(3, part1.getLength()); + assertEquals("foo", part1.getContentAsString(null)); + + Part part2 = rep.getParts().get(1); + assertEquals("icon", part2.getName()); + assertEquals("text.txt", part2.getFileName()); + assertEquals(31, part2.getLength()); + assertEquals("this is the content of the file", + part2.getContentAsString(null)); + + } + } From 49a3cef605569bccfd4daf1961908d3fd133d843 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:14:28 -0400 Subject: [PATCH 17/21] Update changes.md --- changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes.md b/changes.md index f96f8f40d8..e873316df9 100644 --- a/changes.md +++ b/changes.md @@ -3,7 +3,7 @@ Changes log - 2.6 Release Candidate 1 (??-03-2025) - Enhancements - - Added MultiPartFormDataRepresentation to Jetty extension to support generation and parsing. + - Added MultiPartRepresentation to Jetty extension to support generation and parsing. - Added support for the "charset" parameter in HTTP BASIC challenges. Reported by Marc Lafon. - Added MediaType constructors to help with cloning and customization needs. - Misc From 309bd2364f59d8027a6a3bfbc3c1adc6594fa1b2 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:52:30 -0400 Subject: [PATCH 18/21] Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau --- .../java/org/restlet/ext/jetty/MultiPartRepresentation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index 2a09aaae7b..67ba21781b 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -42,7 +42,7 @@ public class MultiPartRepresentation extends InputRepresentation { * metadata. * * @param name The name of the part. - * @param fileName The client suggest file name for storing the part. + * @param fileName The client suggests file name for storing the part. * @param partContent The part content. * @return The Jetty #{@link Part} object created. * @throws IOException From fd08ce7d87b7a3e0fca24788cc14eb8eec7b2225 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:52:46 -0400 Subject: [PATCH 19/21] Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau --- .../java/org/restlet/ext/jetty/MultiPartRepresentation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index 67ba21781b..5a693a8728 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -54,7 +54,7 @@ public static Part createPart(String name, String fileName, } /** - * Returns the value of the first mediatype parameter with "boundary" name. + * Returns the value of the first media-type parameter with "boundary" name. * * @param mediaType The media type that might contain a "boundary" * parameter. From 9995a206308d409cf8f8149727156d3b1c5ae6da Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:52:54 -0400 Subject: [PATCH 20/21] Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau --- .../java/org/restlet/ext/jetty/MultiPartRepresentation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index 5a693a8728..d62548ff6b 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -58,7 +58,7 @@ public static Part createPart(String name, String fileName, * * @param mediaType The media type that might contain a "boundary" * parameter. - * @return The value of the first mediatype parameter with "boundary" name. + * @return The value of the first media-type parameter with "boundary" name. */ public static String getBoundary(MediaType mediaType) { final String result; From 57e3a571dc76f8763f2b42d5d0deec5b9950b43c Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:53:02 -0400 Subject: [PATCH 21/21] Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau --- .../java/org/restlet/ext/jetty/MultiPartRepresentation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java index d62548ff6b..0a4e1e138a 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java @@ -290,7 +290,7 @@ public List getParts() { /** * Returns an input stream that generates the multipart form data * serialization for the wrapped {@link #getParts()} object. The "boundary" - * must be non null when invoking this method. + * must be non-null when invoking this method. * * @return An input stream that generates the multipart form data. */