View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http.multipart;
17  
18  import io.netty.handler.codec.http.DefaultHttpRequest;
19  import io.netty.handler.codec.http.HttpConstants;
20  import io.netty.handler.codec.http.HttpRequest;
21  
22  import java.io.IOException;
23  import java.nio.charset.Charset;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.IdentityHashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  
32  /**
33   * Default factory giving {@link Attribute} and {@link FileUpload} according to constructor.
34   *
35   * <p>According to the constructor, {@link Attribute} and {@link FileUpload} can be:</p>
36   * <ul>
37   * <li>MemoryAttribute, DiskAttribute or MixedAttribute</li>
38   * <li>MemoryFileUpload, DiskFileUpload or MixedFileUpload</li>
39   * </ul>
40   * A good example of releasing HttpData once all work is done is as follow:<br>
41   * <pre>{@code
42   *   for (InterfaceHttpData httpData: decoder.getBodyHttpDatas()) {
43   *     httpData.release();
44   *     factory.removeHttpDataFromClean(request, httpData);
45   *   }
46   *   factory.cleanAllHttpData();
47   *   decoder.destroy();
48   *  }</pre>
49   */
50  public class DefaultHttpDataFactory implements HttpDataFactory {
51  
52      /**
53       * Proposed default MINSIZE as 16 KB.
54       */
55      public static final long MINSIZE = 0x4000;
56      /**
57       * Proposed default MAXSIZE = -1 as UNLIMITED
58       */
59      public static final long MAXSIZE = -1;
60  
61      private final boolean useDisk;
62  
63      private final boolean checkSize;
64  
65      private long minSize;
66  
67      private long maxSize = MAXSIZE;
68  
69      private Charset charset = HttpConstants.DEFAULT_CHARSET;
70  
71      private String baseDir;
72  
73      private boolean deleteOnExit; // false is a good default cause true leaks
74  
75      /**
76       * Keep all {@link HttpData}s until cleaning methods are called.
77       * We need to use {@link IdentityHashMap} because different requests may be equal.
78       * See {@link DefaultHttpRequest#hashCode} and {@link DefaultHttpRequest#equals}.
79       * Similarly, when removing data items, we need to check their identities because
80       * different data items may be equal.
81       */
82      private final Map<HttpRequest, List<HttpData>> requestFileDeleteMap =
83              Collections.synchronizedMap(new IdentityHashMap<HttpRequest, List<HttpData>>());
84  
85      /**
86       * HttpData will be in memory if less than default size (16KB).
87       * The type will be Mixed.
88       */
89      public DefaultHttpDataFactory() {
90          useDisk = false;
91          checkSize = true;
92          minSize = MINSIZE;
93      }
94  
95      public DefaultHttpDataFactory(Charset charset) {
96          this();
97          this.charset = charset;
98      }
99  
100     /**
101      * HttpData will be always on Disk if useDisk is True, else always in Memory if False
102      */
103     public DefaultHttpDataFactory(boolean useDisk) {
104         this.useDisk = useDisk;
105         checkSize = false;
106     }
107 
108     public DefaultHttpDataFactory(boolean useDisk, Charset charset) {
109         this(useDisk);
110         this.charset = charset;
111     }
112     /**
113      * HttpData will be on Disk if the size of the file is greater than minSize, else it
114      * will be in memory. The type will be Mixed.
115      */
116     public DefaultHttpDataFactory(long minSize) {
117         useDisk = false;
118         checkSize = true;
119         this.minSize = minSize;
120     }
121 
122     public DefaultHttpDataFactory(long minSize, Charset charset) {
123         this(minSize);
124         this.charset = charset;
125     }
126 
127     /**
128      * Override global {@link DiskAttribute#baseDirectory} and {@link DiskFileUpload#baseDirectory} values.
129      *
130      * @param baseDir directory path where to store disk attributes and file uploads.
131      */
132     public void setBaseDir(String baseDir) {
133         this.baseDir = baseDir;
134     }
135 
136     /**
137      * Override global {@link DiskAttribute#deleteOnExitTemporaryFile} and
138      * {@link DiskFileUpload#deleteOnExitTemporaryFile} values.
139      *
140      * @param deleteOnExit true if temporary files should be deleted with the JVM, false otherwise.
141      */
142     public void setDeleteOnExit(boolean deleteOnExit) {
143         this.deleteOnExit = deleteOnExit;
144     }
145 
146     @Override
147     public void setMaxLimit(long maxSize) {
148         this.maxSize = maxSize;
149     }
150 
151     /**
152      * @return the associated list of {@link HttpData} for the request
153      */
154     private List<HttpData> getList(HttpRequest request) {
155         List<HttpData> list = requestFileDeleteMap.get(request);
156         if (list == null) {
157             list = new ArrayList<HttpData>();
158             requestFileDeleteMap.put(request, list);
159         }
160         return list;
161     }
162 
163     @Override
164     public Attribute createAttribute(HttpRequest request, String name) {
165         if (useDisk) {
166             Attribute attribute = new DiskAttribute(name, charset, baseDir, deleteOnExit);
167             attribute.setMaxSize(maxSize);
168             List<HttpData> list = getList(request);
169             list.add(attribute);
170             return attribute;
171         }
172         if (checkSize) {
173             Attribute attribute = new MixedAttribute(name, minSize, charset, baseDir, deleteOnExit);
174             attribute.setMaxSize(maxSize);
175             List<HttpData> list = getList(request);
176             list.add(attribute);
177             return attribute;
178         }
179         MemoryAttribute attribute = new MemoryAttribute(name);
180         attribute.setMaxSize(maxSize);
181         return attribute;
182     }
183 
184     @Override
185     public Attribute createAttribute(HttpRequest request, String name, long definedSize) {
186         if (useDisk) {
187             Attribute attribute = new DiskAttribute(name, definedSize, charset, baseDir, deleteOnExit);
188             attribute.setMaxSize(maxSize);
189             List<HttpData> list = getList(request);
190             list.add(attribute);
191             return attribute;
192         }
193         if (checkSize) {
194             Attribute attribute = new MixedAttribute(name, definedSize, minSize, charset, baseDir, deleteOnExit);
195             attribute.setMaxSize(maxSize);
196             List<HttpData> list = getList(request);
197             list.add(attribute);
198             return attribute;
199         }
200         MemoryAttribute attribute = new MemoryAttribute(name, definedSize);
201         attribute.setMaxSize(maxSize);
202         return attribute;
203     }
204 
205     /**
206      * Utility method
207      */
208     private static void checkHttpDataSize(HttpData data) {
209         try {
210             data.checkSize(data.length());
211         } catch (IOException ignored) {
212             throw new IllegalArgumentException("Attribute bigger than maxSize allowed");
213         }
214     }
215 
216     @Override
217     public Attribute createAttribute(HttpRequest request, String name, String value) {
218         if (useDisk) {
219             Attribute attribute;
220             try {
221                 attribute = new DiskAttribute(name, value, charset, baseDir, deleteOnExit);
222                 attribute.setMaxSize(maxSize);
223             } catch (IOException e) {
224                 // revert to Mixed mode
225                 attribute = new MixedAttribute(name, value, minSize, charset, baseDir, deleteOnExit);
226                 attribute.setMaxSize(maxSize);
227             }
228             checkHttpDataSize(attribute);
229             List<HttpData> list = getList(request);
230             list.add(attribute);
231             return attribute;
232         }
233         if (checkSize) {
234             Attribute attribute = new MixedAttribute(name, value, minSize, charset, baseDir, deleteOnExit);
235             attribute.setMaxSize(maxSize);
236             checkHttpDataSize(attribute);
237             List<HttpData> list = getList(request);
238             list.add(attribute);
239             return attribute;
240         }
241         try {
242             MemoryAttribute attribute = new MemoryAttribute(name, value, charset);
243             attribute.setMaxSize(maxSize);
244             checkHttpDataSize(attribute);
245             return attribute;
246         } catch (IOException e) {
247             throw new IllegalArgumentException(e);
248         }
249     }
250 
251     @Override
252     public FileUpload createFileUpload(HttpRequest request, String name, String filename,
253             String contentType, String contentTransferEncoding, Charset charset,
254             long size) {
255         if (useDisk) {
256             FileUpload fileUpload = new DiskFileUpload(name, filename, contentType,
257                     contentTransferEncoding, charset, size, baseDir, deleteOnExit);
258             fileUpload.setMaxSize(maxSize);
259             checkHttpDataSize(fileUpload);
260             List<HttpData> list = getList(request);
261             list.add(fileUpload);
262             return fileUpload;
263         }
264         if (checkSize) {
265             FileUpload fileUpload = new MixedFileUpload(name, filename, contentType,
266                     contentTransferEncoding, charset, size, minSize, baseDir, deleteOnExit);
267             fileUpload.setMaxSize(maxSize);
268             checkHttpDataSize(fileUpload);
269             List<HttpData> list = getList(request);
270             list.add(fileUpload);
271             return fileUpload;
272         }
273         MemoryFileUpload fileUpload = new MemoryFileUpload(name, filename, contentType,
274                 contentTransferEncoding, charset, size);
275         fileUpload.setMaxSize(maxSize);
276         checkHttpDataSize(fileUpload);
277         return fileUpload;
278     }
279 
280     @Override
281     public void removeHttpDataFromClean(HttpRequest request, InterfaceHttpData data) {
282         if (!(data instanceof HttpData)) {
283             return;
284         }
285 
286         // Do not use getList because it adds empty list to requestFileDeleteMap
287         // if request is not found
288         List<HttpData> list = requestFileDeleteMap.get(request);
289         if (list == null) {
290             return;
291         }
292 
293         // Can't simply call list.remove(data), because different data items may be equal.
294         // Need to check identity.
295         Iterator<HttpData> i = list.iterator();
296         while (i.hasNext()) {
297             HttpData n = i.next();
298             if (n == data) {
299                 i.remove();
300 
301                 // Remove empty list to avoid memory leak
302                 if (list.isEmpty()) {
303                     requestFileDeleteMap.remove(request);
304                 }
305 
306                 return;
307             }
308         }
309     }
310 
311     @Override
312     public void cleanRequestHttpData(HttpRequest request) {
313         List<HttpData> list = requestFileDeleteMap.remove(request);
314         if (list != null) {
315             for (HttpData data : list) {
316                 data.release();
317             }
318         }
319     }
320 
321     @Override
322     public void cleanAllHttpData() {
323         Iterator<Entry<HttpRequest, List<HttpData>>> i = requestFileDeleteMap.entrySet().iterator();
324         while (i.hasNext()) {
325             Entry<HttpRequest, List<HttpData>> e = i.next();
326 
327             // Calling i.remove() here will cause "java.lang.IllegalStateException: Entry was removed"
328             // at e.getValue() below
329 
330             List<HttpData> list = e.getValue();
331             for (HttpData data : list) {
332                 data.release();
333             }
334 
335             i.remove();
336         }
337     }
338 
339     @Override
340     public void cleanRequestHttpDatas(HttpRequest request) {
341         cleanRequestHttpData(request);
342     }
343 
344     @Override
345     public void cleanAllHttpDatas() {
346         cleanAllHttpData();
347     }
348 }