Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
DiskFileItem |
|
| 2.4615384615384617;2.462 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one or more | |
3 | * contributor license agreements. See the NOTICE file distributed with | |
4 | * this work for additional information regarding copyright ownership. | |
5 | * The ASF licenses this file to You under the Apache License, Version 2.0 | |
6 | * (the "License"); you may not use this file except in compliance with | |
7 | * the License. You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | package org.apache.commons.fileupload.disk; | |
18 | ||
19 | import static java.lang.String.format; | |
20 | ||
21 | import java.io.ByteArrayInputStream; | |
22 | import java.io.File; | |
23 | import java.io.FileInputStream; | |
24 | import java.io.FileOutputStream; | |
25 | import java.io.IOException; | |
26 | import java.io.InputStream; | |
27 | import java.io.OutputStream; | |
28 | import java.io.UnsupportedEncodingException; | |
29 | import java.util.Map; | |
30 | import java.util.UUID; | |
31 | import java.util.concurrent.atomic.AtomicInteger; | |
32 | ||
33 | import org.apache.commons.fileupload.FileItem; | |
34 | import org.apache.commons.fileupload.FileItemHeaders; | |
35 | import org.apache.commons.fileupload.FileUploadException; | |
36 | import org.apache.commons.fileupload.ParameterParser; | |
37 | import org.apache.commons.fileupload.util.Streams; | |
38 | import org.apache.commons.io.FileUtils; | |
39 | import org.apache.commons.io.IOUtils; | |
40 | import org.apache.commons.io.output.DeferredFileOutputStream; | |
41 | ||
42 | /** | |
43 | * <p> The default implementation of the | |
44 | * {@link org.apache.commons.fileupload.FileItem FileItem} interface. | |
45 | * | |
46 | * <p> After retrieving an instance of this class from a {@link | |
47 | * DiskFileItemFactory} instance (see | |
48 | * {@link org.apache.commons.fileupload.servlet.ServletFileUpload | |
49 | * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may | |
50 | * either request all contents of file at once using {@link #get()} or | |
51 | * request an {@link java.io.InputStream InputStream} with | |
52 | * {@link #getInputStream()} and process the file without attempting to load | |
53 | * it into memory, which may come handy with large files. | |
54 | * | |
55 | * <p>Temporary files, which are created for file items, should be | |
56 | * deleted later on. The best way to do this is using a | |
57 | * {@link org.apache.commons.io.FileCleaningTracker}, which you can set on the | |
58 | * {@link DiskFileItemFactory}. However, if you do use such a tracker, | |
59 | * then you must consider the following: Temporary files are automatically | |
60 | * deleted as soon as they are no longer needed. (More precisely, when the | |
61 | * corresponding instance of {@link java.io.File} is garbage collected.) | |
62 | * This is done by the so-called reaper thread, which is started and stopped | |
63 | * automatically by the {@link org.apache.commons.io.FileCleaningTracker} when | |
64 | * there are files to be tracked. | |
65 | * It might make sense to terminate that thread, for example, if | |
66 | * your web application ends. See the section on "Resource cleanup" | |
67 | * in the users guide of commons-fileupload.</p> | |
68 | * | |
69 | * @since FileUpload 1.1 | |
70 | */ | |
71 | public class DiskFileItem | |
72 | implements FileItem { | |
73 | ||
74 | // ----------------------------------------------------- Manifest constants | |
75 | ||
76 | /** | |
77 | * Default content charset to be used when no explicit charset | |
78 | * parameter is provided by the sender. Media subtypes of the | |
79 | * "text" type are defined to have a default charset value of | |
80 | * "ISO-8859-1" when received via HTTP. | |
81 | */ | |
82 | public static final String DEFAULT_CHARSET = "ISO-8859-1"; | |
83 | ||
84 | // ----------------------------------------------------------- Data members | |
85 | ||
86 | /** | |
87 | * UID used in unique file name generation. | |
88 | */ | |
89 | private static final String UID = | |
90 | 1 | UUID.randomUUID().toString().replace('-', '_'); |
91 | ||
92 | /** | |
93 | * Counter used in unique identifier generation. | |
94 | */ | |
95 | 1 | private static final AtomicInteger COUNTER = new AtomicInteger(0); |
96 | ||
97 | /** | |
98 | * The name of the form field as provided by the browser. | |
99 | */ | |
100 | private String fieldName; | |
101 | ||
102 | /** | |
103 | * The content type passed by the browser, or <code>null</code> if | |
104 | * not defined. | |
105 | */ | |
106 | private final String contentType; | |
107 | ||
108 | /** | |
109 | * Whether or not this item is a simple form field. | |
110 | */ | |
111 | private boolean isFormField; | |
112 | ||
113 | /** | |
114 | * The original filename in the user's filesystem. | |
115 | */ | |
116 | private final String fileName; | |
117 | ||
118 | /** | |
119 | * The size of the item, in bytes. This is used to cache the size when a | |
120 | * file item is moved from its original location. | |
121 | */ | |
122 | 2174 | private long size = -1; |
123 | ||
124 | ||
125 | /** | |
126 | * The threshold above which uploads will be stored on disk. | |
127 | */ | |
128 | private final int sizeThreshold; | |
129 | ||
130 | /** | |
131 | * The directory in which uploaded files will be stored, if stored on disk. | |
132 | */ | |
133 | private final File repository; | |
134 | ||
135 | /** | |
136 | * Cached contents of the file. | |
137 | */ | |
138 | private byte[] cachedContent; | |
139 | ||
140 | /** | |
141 | * Output stream for this item. | |
142 | */ | |
143 | private transient DeferredFileOutputStream dfos; | |
144 | ||
145 | /** | |
146 | * The temporary file to use. | |
147 | */ | |
148 | private transient File tempFile; | |
149 | ||
150 | /** | |
151 | * The file items headers. | |
152 | */ | |
153 | private FileItemHeaders headers; | |
154 | ||
155 | /** | |
156 | * Default content charset to be used when no explicit charset | |
157 | * parameter is provided by the sender. | |
158 | */ | |
159 | 2174 | private String defaultCharset = DEFAULT_CHARSET; |
160 | ||
161 | // ----------------------------------------------------------- Constructors | |
162 | ||
163 | /** | |
164 | * Constructs a new <code>DiskFileItem</code> instance. | |
165 | * | |
166 | * @param fieldName The name of the form field. | |
167 | * @param contentType The content type passed by the browser or | |
168 | * <code>null</code> if not specified. | |
169 | * @param isFormField Whether or not this item is a plain form field, as | |
170 | * opposed to a file upload. | |
171 | * @param fileName The original filename in the user's filesystem, or | |
172 | * <code>null</code> if not specified. | |
173 | * @param sizeThreshold The threshold, in bytes, below which items will be | |
174 | * retained in memory and above which they will be | |
175 | * stored as a file. | |
176 | * @param repository The data repository, which is the directory in | |
177 | * which files will be created, should the item size | |
178 | * exceed the threshold. | |
179 | */ | |
180 | public DiskFileItem(String fieldName, | |
181 | String contentType, boolean isFormField, String fileName, | |
182 | 2174 | int sizeThreshold, File repository) { |
183 | 2174 | this.fieldName = fieldName; |
184 | 2174 | this.contentType = contentType; |
185 | 2174 | this.isFormField = isFormField; |
186 | 2174 | this.fileName = fileName; |
187 | 2174 | this.sizeThreshold = sizeThreshold; |
188 | 2174 | this.repository = repository; |
189 | 2174 | } |
190 | ||
191 | // ------------------------------- Methods from javax.activation.DataSource | |
192 | ||
193 | /** | |
194 | * Returns an {@link java.io.InputStream InputStream} that can be | |
195 | * used to retrieve the contents of the file. | |
196 | * | |
197 | * @return An {@link java.io.InputStream InputStream} that can be | |
198 | * used to retrieve the contents of the file. | |
199 | * | |
200 | * @throws IOException if an error occurs. | |
201 | */ | |
202 | @Override | |
203 | public InputStream getInputStream() | |
204 | throws IOException { | |
205 | 0 | if (!isInMemory()) { |
206 | 0 | return new FileInputStream(dfos.getFile()); |
207 | } | |
208 | ||
209 | 0 | if (cachedContent == null) { |
210 | 0 | cachedContent = dfos.getData(); |
211 | } | |
212 | 0 | return new ByteArrayInputStream(cachedContent); |
213 | } | |
214 | ||
215 | /** | |
216 | * Returns the content type passed by the agent or <code>null</code> if | |
217 | * not defined. | |
218 | * | |
219 | * @return The content type passed by the agent or <code>null</code> if | |
220 | * not defined. | |
221 | */ | |
222 | @Override | |
223 | public String getContentType() { | |
224 | 43 | return contentType; |
225 | } | |
226 | ||
227 | /** | |
228 | * Returns the content charset passed by the agent or <code>null</code> if | |
229 | * not defined. | |
230 | * | |
231 | * @return The content charset passed by the agent or <code>null</code> if | |
232 | * not defined. | |
233 | */ | |
234 | public String getCharSet() { | |
235 | 35 | ParameterParser parser = new ParameterParser(); |
236 | 35 | parser.setLowerCaseNames(true); |
237 | // Parameter parser can handle null input | |
238 | 35 | Map<String, String> params = parser.parse(getContentType(), ';'); |
239 | 35 | return params.get("charset"); |
240 | } | |
241 | ||
242 | /** | |
243 | * Returns the original filename in the client's filesystem. | |
244 | * | |
245 | * @return The original filename in the client's filesystem. | |
246 | * @throws org.apache.commons.fileupload.InvalidFileNameException The file name contains a NUL character, | |
247 | * which might be an indicator of a security attack. If you intend to | |
248 | * use the file name anyways, catch the exception and use | |
249 | * {@link org.apache.commons.fileupload.InvalidFileNameException#getName()}. | |
250 | */ | |
251 | @Override | |
252 | public String getName() { | |
253 | 19 | return Streams.checkFileName(fileName); |
254 | } | |
255 | ||
256 | // ------------------------------------------------------- FileItem methods | |
257 | ||
258 | /** | |
259 | * Provides a hint as to whether or not the file contents will be read | |
260 | * from memory. | |
261 | * | |
262 | * @return <code>true</code> if the file contents will be read | |
263 | * from memory; <code>false</code> otherwise. | |
264 | */ | |
265 | @Override | |
266 | public boolean isInMemory() { | |
267 | 2432 | if (cachedContent != null) { |
268 | 2 | return true; |
269 | } | |
270 | 2430 | return dfos.isInMemory(); |
271 | } | |
272 | ||
273 | /** | |
274 | * Returns the size of the file. | |
275 | * | |
276 | * @return The size of the file, in bytes. | |
277 | */ | |
278 | @Override | |
279 | public long getSize() { | |
280 | 536 | if (size >= 0) { |
281 | 0 | return size; |
282 | 536 | } else if (cachedContent != null) { |
283 | 0 | return cachedContent.length; |
284 | 536 | } else if (dfos.isInMemory()) { |
285 | 4 | return dfos.getData().length; |
286 | } else { | |
287 | 532 | return dfos.getFile().length(); |
288 | } | |
289 | } | |
290 | ||
291 | /** | |
292 | * Returns the contents of the file as an array of bytes. If the | |
293 | * contents of the file were not yet cached in memory, they will be | |
294 | * loaded from the disk storage and cached. | |
295 | * | |
296 | * @return The contents of the file as an array of bytes | |
297 | * or {@code null} if the data cannot be read | |
298 | */ | |
299 | @Override | |
300 | public byte[] get() { | |
301 | 1451 | if (isInMemory()) { |
302 | 922 | if (cachedContent == null && dfos != null) { |
303 | 920 | cachedContent = dfos.getData(); |
304 | } | |
305 | 922 | return cachedContent; |
306 | } | |
307 | ||
308 | 529 | byte[] fileData = new byte[(int) getSize()]; |
309 | 529 | InputStream fis = null; |
310 | ||
311 | try { | |
312 | 529 | fis = new FileInputStream(dfos.getFile()); |
313 | 529 | IOUtils.readFully(fis, fileData); |
314 | 0 | } catch (IOException e) { |
315 | 0 | fileData = null; |
316 | } finally { | |
317 | 529 | IOUtils.closeQuietly(fis); |
318 | 529 | } |
319 | ||
320 | 529 | return fileData; |
321 | } | |
322 | ||
323 | /** | |
324 | * Returns the contents of the file as a String, using the specified | |
325 | * encoding. This method uses {@link #get()} to retrieve the | |
326 | * contents of the file. | |
327 | * | |
328 | * @param charset The charset to use. | |
329 | * | |
330 | * @return The contents of the file, as a string. | |
331 | * | |
332 | * @throws UnsupportedEncodingException if the requested character | |
333 | * encoding is not available. | |
334 | */ | |
335 | @Override | |
336 | public String getString(final String charset) | |
337 | throws UnsupportedEncodingException { | |
338 | 0 | return new String(get(), charset); |
339 | } | |
340 | ||
341 | /** | |
342 | * Returns the contents of the file as a String, using the default | |
343 | * character encoding. This method uses {@link #get()} to retrieve the | |
344 | * contents of the file. | |
345 | * | |
346 | * <b>TODO</b> Consider making this method throw UnsupportedEncodingException. | |
347 | * | |
348 | * @return The contents of the file, as a string. | |
349 | */ | |
350 | @Override | |
351 | public String getString() { | |
352 | 35 | byte[] rawdata = get(); |
353 | 35 | String charset = getCharSet(); |
354 | 35 | if (charset == null) { |
355 | 35 | charset = defaultCharset; |
356 | } | |
357 | try { | |
358 | 35 | return new String(rawdata, charset); |
359 | 0 | } catch (UnsupportedEncodingException e) { |
360 | 0 | return new String(rawdata); |
361 | } | |
362 | } | |
363 | ||
364 | /** | |
365 | * A convenience method to write an uploaded item to disk. The client code | |
366 | * is not concerned with whether or not the item is stored in memory, or on | |
367 | * disk in a temporary location. They just want to write the uploaded item | |
368 | * to a file. | |
369 | * <p> | |
370 | * This implementation first attempts to rename the uploaded item to the | |
371 | * specified destination file, if the item was originally written to disk. | |
372 | * Otherwise, the data will be copied to the specified file. | |
373 | * <p> | |
374 | * This method is only guaranteed to work <em>once</em>, the first time it | |
375 | * is invoked for a particular item. This is because, in the event that the | |
376 | * method renames a temporary file, that file will no longer be available | |
377 | * to copy or rename again at a later time. | |
378 | * | |
379 | * @param file The <code>File</code> into which the uploaded item should | |
380 | * be stored. | |
381 | * | |
382 | * @throws Exception if an error occurs. | |
383 | */ | |
384 | @Override | |
385 | public void write(File file) throws Exception { | |
386 | 0 | if (isInMemory()) { |
387 | 0 | FileOutputStream fout = null; |
388 | try { | |
389 | 0 | fout = new FileOutputStream(file); |
390 | 0 | fout.write(get()); |
391 | 0 | fout.close(); |
392 | } finally { | |
393 | 0 | IOUtils.closeQuietly(fout); |
394 | 0 | } |
395 | 0 | } else { |
396 | 0 | File outputFile = getStoreLocation(); |
397 | 0 | if (outputFile != null) { |
398 | // Save the length of the file | |
399 | 0 | size = outputFile.length(); |
400 | /* | |
401 | * The uploaded file is being stored on disk | |
402 | * in a temporary location so move it to the | |
403 | * desired file. | |
404 | */ | |
405 | 0 | FileUtils.moveFile(outputFile, file); |
406 | } else { | |
407 | /* | |
408 | * For whatever reason we cannot write the | |
409 | * file to disk. | |
410 | */ | |
411 | 0 | throw new FileUploadException( |
412 | "Cannot write uploaded file to disk!"); | |
413 | } | |
414 | } | |
415 | 0 | } |
416 | ||
417 | /** | |
418 | * Deletes the underlying storage for a file item, including deleting any | |
419 | * associated temporary disk file. Although this storage will be deleted | |
420 | * automatically when the <code>FileItem</code> instance is garbage | |
421 | * collected, this method can be used to ensure that this is done at an | |
422 | * earlier time, thus preserving system resources. | |
423 | */ | |
424 | @Override | |
425 | public void delete() { | |
426 | 707 | cachedContent = null; |
427 | 707 | File outputFile = getStoreLocation(); |
428 | 707 | if (outputFile != null && !isInMemory() && outputFile.exists()) { |
429 | 265 | outputFile.delete(); |
430 | } | |
431 | 707 | } |
432 | ||
433 | /** | |
434 | * Returns the name of the field in the multipart form corresponding to | |
435 | * this file item. | |
436 | * | |
437 | * @return The name of the form field. | |
438 | * | |
439 | * @see #setFieldName(java.lang.String) | |
440 | * | |
441 | */ | |
442 | @Override | |
443 | public String getFieldName() { | |
444 | 1445 | return fieldName; |
445 | } | |
446 | ||
447 | /** | |
448 | * Sets the field name used to reference this file item. | |
449 | * | |
450 | * @param fieldName The name of the form field. | |
451 | * | |
452 | * @see #getFieldName() | |
453 | * | |
454 | */ | |
455 | @Override | |
456 | public void setFieldName(String fieldName) { | |
457 | 0 | this.fieldName = fieldName; |
458 | 0 | } |
459 | ||
460 | /** | |
461 | * Determines whether or not a <code>FileItem</code> instance represents | |
462 | * a simple form field. | |
463 | * | |
464 | * @return <code>true</code> if the instance represents a simple form | |
465 | * field; <code>false</code> if it represents an uploaded file. | |
466 | * | |
467 | * @see #setFormField(boolean) | |
468 | * | |
469 | */ | |
470 | @Override | |
471 | public boolean isFormField() { | |
472 | 32 | return isFormField; |
473 | } | |
474 | ||
475 | /** | |
476 | * Specifies whether or not a <code>FileItem</code> instance represents | |
477 | * a simple form field. | |
478 | * | |
479 | * @param state <code>true</code> if the instance represents a simple form | |
480 | * field; <code>false</code> if it represents an uploaded file. | |
481 | * | |
482 | * @see #isFormField() | |
483 | * | |
484 | */ | |
485 | @Override | |
486 | public void setFormField(boolean state) { | |
487 | 0 | isFormField = state; |
488 | 0 | } |
489 | ||
490 | /** | |
491 | * Returns an {@link java.io.OutputStream OutputStream} that can | |
492 | * be used for storing the contents of the file. | |
493 | * | |
494 | * @return An {@link java.io.OutputStream OutputStream} that can be used | |
495 | * for storing the contents of the file. | |
496 | * | |
497 | * @throws IOException if an error occurs. | |
498 | */ | |
499 | @Override | |
500 | public OutputStream getOutputStream() | |
501 | throws IOException { | |
502 | 2172 | if (dfos == null) { |
503 | 2172 | File outputFile = getTempFile(); |
504 | 2172 | dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); |
505 | } | |
506 | 2172 | return dfos; |
507 | } | |
508 | ||
509 | // --------------------------------------------------------- Public methods | |
510 | ||
511 | /** | |
512 | * Returns the {@link java.io.File} object for the <code>FileItem</code>'s | |
513 | * data's temporary location on the disk. Note that for | |
514 | * <code>FileItem</code>s that have their data stored in memory, | |
515 | * this method will return <code>null</code>. When handling large | |
516 | * files, you can use {@link java.io.File#renameTo(java.io.File)} to | |
517 | * move the file to new location without copying the data, if the | |
518 | * source and destination locations reside within the same logical | |
519 | * volume. | |
520 | * | |
521 | * @return The data file, or <code>null</code> if the data is stored in | |
522 | * memory. | |
523 | */ | |
524 | public File getStoreLocation() { | |
525 | 709 | if (dfos == null) { |
526 | 0 | return null; |
527 | } | |
528 | 709 | if (isInMemory()) { |
529 | 442 | return null; |
530 | } | |
531 | 267 | return dfos.getFile(); |
532 | } | |
533 | ||
534 | // ------------------------------------------------------ Protected methods | |
535 | ||
536 | /** | |
537 | * Removes the file contents from the temporary storage. | |
538 | */ | |
539 | @Override | |
540 | protected void finalize() { | |
541 | 1417 | if (dfos == null || dfos.isInMemory()) { |
542 | 892 | return; |
543 | } | |
544 | 525 | File outputFile = dfos.getFile(); |
545 | ||
546 | 525 | if (outputFile != null && outputFile.exists()) { |
547 | 262 | outputFile.delete(); |
548 | } | |
549 | 525 | } |
550 | ||
551 | /** | |
552 | * Creates and returns a {@link java.io.File File} representing a uniquely | |
553 | * named temporary file in the configured repository path. The lifetime of | |
554 | * the file is tied to the lifetime of the <code>FileItem</code> instance; | |
555 | * the file will be deleted when the instance is garbage collected. | |
556 | * <p> | |
557 | * <b>Note: Subclasses that override this method must ensure that they return the | |
558 | * same File each time.</b> | |
559 | * | |
560 | * @return The {@link java.io.File File} to be used for temporary storage. | |
561 | */ | |
562 | protected File getTempFile() { | |
563 | 2172 | if (tempFile == null) { |
564 | 2172 | File tempDir = repository; |
565 | 2172 | if (tempDir == null) { |
566 | 2165 | tempDir = new File(System.getProperty("java.io.tmpdir")); |
567 | } | |
568 | ||
569 | 2172 | String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId()); |
570 | ||
571 | 2172 | tempFile = new File(tempDir, tempFileName); |
572 | } | |
573 | 2172 | return tempFile; |
574 | } | |
575 | ||
576 | // -------------------------------------------------------- Private methods | |
577 | ||
578 | /** | |
579 | * Returns an identifier that is unique within the class loader used to | |
580 | * load this class, but does not have random-like appearance. | |
581 | * | |
582 | * @return A String with the non-random looking instance identifier. | |
583 | */ | |
584 | private static String getUniqueId() { | |
585 | 2172 | final int limit = 100000000; |
586 | 2172 | int current = COUNTER.getAndIncrement(); |
587 | 2172 | String id = Integer.toString(current); |
588 | ||
589 | // If you manage to get more than 100 million of ids, you'll | |
590 | // start getting ids longer than 8 characters. | |
591 | 2172 | if (current < limit) { |
592 | 2172 | id = ("00000000" + id).substring(id.length()); |
593 | } | |
594 | 2172 | return id; |
595 | } | |
596 | ||
597 | /** | |
598 | * Returns a string representation of this object. | |
599 | * | |
600 | * @return a string representation of this object. | |
601 | */ | |
602 | @Override | |
603 | public String toString() { | |
604 | 0 | return format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s", |
605 | 0 | getName(), getStoreLocation(), Long.valueOf(getSize()), |
606 | 0 | Boolean.valueOf(isFormField()), getFieldName()); |
607 | } | |
608 | ||
609 | /** | |
610 | * Returns the file item headers. | |
611 | * @return The file items headers. | |
612 | */ | |
613 | @Override | |
614 | public FileItemHeaders getHeaders() { | |
615 | 32 | return headers; |
616 | } | |
617 | ||
618 | /** | |
619 | * Sets the file item headers. | |
620 | * @param pHeaders The file items headers. | |
621 | */ | |
622 | @Override | |
623 | public void setHeaders(FileItemHeaders pHeaders) { | |
624 | 2160 | headers = pHeaders; |
625 | 2160 | } |
626 | ||
627 | /** | |
628 | * Returns the default charset for use when no explicit charset | |
629 | * parameter is provided by the sender. | |
630 | * @return the default charset | |
631 | */ | |
632 | public String getDefaultCharset() { | |
633 | 0 | return defaultCharset; |
634 | } | |
635 | ||
636 | /** | |
637 | * Sets the default charset for use when no explicit charset | |
638 | * parameter is provided by the sender. | |
639 | * @param charset the default charset | |
640 | */ | |
641 | public void setDefaultCharset(String charset) { | |
642 | 2169 | defaultCharset = charset; |
643 | 2169 | } |
644 | } |