001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.http.codec.multipart; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.List; 024import java.util.Map; 025import java.util.stream.Collectors; 026 027import reactor.core.publisher.Flux; 028import reactor.core.publisher.Mono; 029 030import org.springframework.core.ResolvableType; 031import org.springframework.core.codec.Hints; 032import org.springframework.core.log.LogFormatUtils; 033import org.springframework.http.MediaType; 034import org.springframework.http.ReactiveHttpInputMessage; 035import org.springframework.http.codec.HttpMessageReader; 036import org.springframework.http.codec.LoggingCodecSupport; 037import org.springframework.lang.Nullable; 038import org.springframework.util.Assert; 039import org.springframework.util.LinkedMultiValueMap; 040import org.springframework.util.MultiValueMap; 041 042/** 043 * {@code HttpMessageReader} for reading {@code "multipart/form-data"} requests 044 * into a {@code MultiValueMap<String, Part>}. 045 * 046 * <p>Note that this reader depends on access to an 047 * {@code HttpMessageReader<Part>} for the actual parsing of multipart content. 048 * The purpose of this reader is to collect the parts into a map. 049 * 050 * @author Rossen Stoyanchev 051 * @since 5.0 052 */ 053public class MultipartHttpMessageReader extends LoggingCodecSupport 054 implements HttpMessageReader<MultiValueMap<String, Part>> { 055 056 private static final ResolvableType MULTIPART_VALUE_TYPE = ResolvableType.forClassWithGenerics( 057 MultiValueMap.class, String.class, Part.class); 058 059 static final List<MediaType> MIME_TYPES = Collections.unmodifiableList(Arrays.asList( 060 MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED, MediaType.MULTIPART_RELATED)); 061 062 063 private final HttpMessageReader<Part> partReader; 064 065 066 public MultipartHttpMessageReader(HttpMessageReader<Part> partReader) { 067 Assert.notNull(partReader, "'partReader' is required"); 068 this.partReader = partReader; 069 } 070 071 072 /** 073 * Return the configured parts reader. 074 * @since 5.1.11 075 */ 076 public HttpMessageReader<Part> getPartReader() { 077 return this.partReader; 078 } 079 080 @Override 081 public List<MediaType> getReadableMediaTypes() { 082 return MIME_TYPES; 083 } 084 085 @Override 086 public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) { 087 if (MULTIPART_VALUE_TYPE.isAssignableFrom(elementType)) { 088 if (mediaType == null) { 089 return true; 090 } 091 for (MediaType supportedMediaType : MIME_TYPES) { 092 if (supportedMediaType.isCompatibleWith(mediaType)) { 093 return true; 094 } 095 } 096 } 097 return false; 098 } 099 100 101 @Override 102 public Flux<MultiValueMap<String, Part>> read(ResolvableType elementType, 103 ReactiveHttpInputMessage message, Map<String, Object> hints) { 104 105 return Flux.from(readMono(elementType, message, hints)); 106 } 107 108 109 @Override 110 public Mono<MultiValueMap<String, Part>> readMono(ResolvableType elementType, 111 ReactiveHttpInputMessage inputMessage, Map<String, Object> hints) { 112 113 114 Map<String, Object> allHints = Hints.merge(hints, Hints.SUPPRESS_LOGGING_HINT, true); 115 116 return this.partReader.read(elementType, inputMessage, allHints) 117 .collectMultimap(Part::name) 118 .doOnNext(map -> 119 LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + 120 (isEnableLoggingRequestDetails() ? 121 LogFormatUtils.formatValue(map, !traceOn) : 122 "parts " + map.keySet() + " (content masked)")) 123 ) 124 .map(this::toMultiValueMap); 125 } 126 127 private LinkedMultiValueMap<String, Part> toMultiValueMap(Map<String, Collection<Part>> map) { 128 return new LinkedMultiValueMap<>(map.entrySet().stream() 129 .collect(Collectors.toMap(Map.Entry::getKey, e -> toList(e.getValue())))); 130 } 131 132 private List<Part> toList(Collection<Part> collection) { 133 return collection instanceof List ? (List<Part>) collection : new ArrayList<>(collection); 134 } 135 136}