Skip to content

Commit

Permalink
Introduce PartEvent
Browse files Browse the repository at this point in the history
This commit introduces the PartEvent API. PartEvents are either
- FormPartEvents, representing a form field, or
- FilePartEvents, representing a file upload.

The PartEventHttpMessageReader is a HttpMessageReader that splits
multipart data into a stream of PartEvents. Form fields generate one
FormPartEvent; file uploads produce at least one FilePartEvent. The last
element that makes up a particular part will have isLast set to true.

The PartEventHttpMessageWriter is a HttpMessageWriter that writes a
Publisher<PartEvent> to a outgoing HTTP message. This writer is
particularly useful for relaying a multipart request on the server.

Closes spring-projectsgh-28006
  • Loading branch information
poutsma committed Apr 20, 2022
1 parent 081c646 commit be7fa3a
Show file tree
Hide file tree
Showing 22 changed files with 1,436 additions and 56 deletions.
@@ -0,0 +1,176 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.http.codec.multipart;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;

/**
* Default implementations of {@link PartEvent} and subtypes.
*
* @author Arjen Poutsma
* @since 6.0
*/
abstract class DefaultPartEvents {

public static FormPartEvent form(HttpHeaders headers) {
Assert.notNull(headers, "Headers must not be null");
return new DefaultFormFieldPartEvent(headers);
}

public static FormPartEvent form(HttpHeaders headers, String value) {
Assert.notNull(headers, "Headers must not be null");
Assert.notNull(value, "Value must not be null");
return new DefaultFormFieldPartEvent(headers, value);
}

public static FilePartEvent file(HttpHeaders headers, DataBuffer dataBuffer, boolean isLast) {
Assert.notNull(headers, "Headers must not be null");
Assert.notNull(dataBuffer, "DataBuffer must not be null");
return new DefaultFilePartEvent(headers, dataBuffer, isLast);
}

public static FilePartEvent file(HttpHeaders headers) {
Assert.notNull(headers, "Headers must not be null");
return new DefaultFilePartEvent(headers);
}

public static PartEvent create(HttpHeaders headers, DataBuffer dataBuffer, boolean isLast) {
Assert.notNull(headers, "Headers must not be null");
Assert.notNull(dataBuffer, "DataBuffer must not be null");
if (headers.getContentDisposition().getFilename() != null) {
return file(headers, dataBuffer, isLast);
}
else {
return new DefaultPartEvent(headers, dataBuffer, isLast);
}
}

public static PartEvent create(HttpHeaders headers) {
Assert.notNull(headers, "Headers must not be null");
if (headers.getContentDisposition().getFilename() != null) {
return file(headers);
}
else {
return new DefaultPartEvent(headers);
}
}



private static abstract class AbstractPartEvent implements PartEvent {

private final HttpHeaders headers;


protected AbstractPartEvent(HttpHeaders headers) {
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
}

@Override
public HttpHeaders headers() {
return this.headers;
}
}


/**
* Default implementation of {@link PartEvent}.
*/
private static class DefaultPartEvent extends AbstractPartEvent {

private static final DataBuffer EMPTY = DefaultDataBufferFactory.sharedInstance.allocateBuffer(0);

private final DataBuffer content;

private final boolean last;


public DefaultPartEvent(HttpHeaders headers) {
this(headers, EMPTY, true);
}

public DefaultPartEvent(HttpHeaders headers, DataBuffer content, boolean last) {
super(headers);
this.content = content;
this.last = last;
}

@Override
public DataBuffer content() {
return this.content;
}

@Override
public boolean isLast() {
return this.last;
}

}

/**
* Default implementation of {@link FormPartEvent}.
*/
private static final class DefaultFormFieldPartEvent extends AbstractPartEvent implements FormPartEvent {

private static final String EMPTY = "";

private final String value;


public DefaultFormFieldPartEvent(HttpHeaders headers) {
this(headers, EMPTY);
}

public DefaultFormFieldPartEvent(HttpHeaders headers, String value) {
super(headers);
this.value = value;
}

@Override
public String value() {
return this.value;
}

@Override
public DataBuffer content() {
byte[] bytes = this.value.getBytes(MultipartUtils.charset(headers()));
return DefaultDataBufferFactory.sharedInstance.wrap(bytes);
}

@Override
public boolean isLast() {
return true;
}
}

/**
* Default implementation of {@link FilePartEvent}.
*/
private static class DefaultFilePartEvent extends DefaultPartEvent implements FilePartEvent {

public DefaultFilePartEvent(HttpHeaders headers) {
super(headers);
}

public DefaultFilePartEvent(HttpHeaders headers, DataBuffer content, boolean last) {
super(headers, content, last);
}
}
}
Expand Up @@ -32,7 +32,6 @@
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.http.HttpMessage;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.HttpMessageReader;
Expand Down Expand Up @@ -218,7 +217,7 @@ public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage
@Override
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Flux.defer(() -> {
byte[] boundary = boundary(message);
byte[] boundary = MultipartUtils.boundary(message, this.headersCharset);
if (boundary == null) {
return Flux.error(new DecodingException("No multipart boundary found in Content-Type: \"" +
message.getHeaders().getContentType() + "\""));
Expand All @@ -231,20 +230,4 @@ public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage mess
});
}

@Nullable
private byte[] boundary(HttpMessage message) {
MediaType contentType = message.getHeaders().getContentType();
if (contentType != null) {
String boundary = contentType.getParameter("boundary");
if (boundary != null) {
int len = boundary.length();
if (len > 2 && boundary.charAt(0) == '"' && boundary.charAt(len - 1) == '"') {
boundary = boundary.substring(1, len - 1);
}
return boundary.getBytes(this.headersCharset);
}
}
return null;
}

}

0 comments on commit be7fa3a

Please sign in to comment.