1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.struts.flow.core.source.impl;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileNotFoundException;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.net.MalformedURLException;
27 import java.net.URLConnection;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.ConcurrentModificationException;
31
32 import org.apache.struts.flow.core.source.ModifiableSource;
33 import org.apache.struts.flow.core.source.ModifiableTraversableSource;
34 import org.apache.struts.flow.core.source.MoveableSource;
35 import org.apache.struts.flow.core.source.Source;
36 import org.apache.struts.flow.core.source.SourceException;
37 import org.apache.struts.flow.core.source.SourceNotFoundException;
38 import org.apache.struts.flow.core.source.SourceUtil;
39 import org.apache.struts.flow.core.source.SourceValidity;
40 import org.apache.struts.flow.core.source.impl.validity.FileTimeStampValidity;
41
42 /***
43 * A {@link ModifiableTraversableSource} for filesystem objects.
44 *
45 * @author <a href="mailto:dev@avalon.apache.org">Avalon Development Team</a>
46 * @version $Id: FileSource.java,v 1.5 2004/02/28 11:47:24 cziegeler Exp $
47 */
48
49 public class FileSource implements ModifiableTraversableSource, MoveableSource
50 {
51
52 /*** The file */
53 private File m_file;
54
55 /*** The scheme */
56 private String m_scheme;
57
58 /*** The URI of this source */
59 private String m_uri;
60
61 /***
62 * Builds a FileSource given an URI, which doesn't necessarily have to start with "file:"
63 * @param uri
64 * @throws SourceException
65 * @throws MalformedURLException
66 */
67 public FileSource(String uri) throws SourceException, MalformedURLException
68 {
69 int pos = SourceUtil.indexOfSchemeColon(uri);
70 if (pos == -1)
71 {
72 throw new MalformedURLException("Invalid URI : " + uri);
73 }
74
75 String scheme = uri.substring(0, pos);
76 String fileName = uri.substring(pos + 1);
77 fileName = SourceUtil.decodePath(fileName);
78 init(scheme, new File(fileName));
79 }
80
81 /***
82 * Builds a FileSource, given an URI scheme and a File.
83 *
84 * @param scheme
85 * @param file
86 * @throws SourceException
87 */
88 public FileSource(String scheme, File file) throws SourceException
89 {
90 init(scheme, file);
91 }
92
93 private void init(String scheme, File file) throws SourceException
94 {
95 m_scheme = scheme;
96
97 String uri;
98 try
99 {
100 uri = file.toURL().toExternalForm();
101 }
102 catch (MalformedURLException mue)
103 {
104
105 throw new SourceException("Failed to get URL for file " + file, mue);
106 }
107
108 if (!uri.startsWith(scheme))
109 {
110
111 uri = scheme + ':' + uri.substring(uri.indexOf(':') + 1);
112 }
113
114 m_uri = uri;
115
116 m_file = file;
117 }
118
119 /***
120 * Get the associated file
121 */
122 public File getFile()
123 {
124 return m_file;
125 }
126
127
128
129
130
131 /***
132 * @see org.apache.struts.flow.core.source.Source#getContentLength()
133 */
134 public long getContentLength()
135 {
136 return m_file.length();
137 }
138
139 /***
140 * @see org.apache.struts.flow.core.source.Source#getInputStream()
141 */
142 public InputStream getInputStream() throws IOException, SourceNotFoundException
143 {
144 try
145 {
146 return new FileInputStream(m_file);
147 }
148 catch (FileNotFoundException fnfe)
149 {
150 throw new SourceNotFoundException(m_uri + " doesn't exist.", fnfe);
151 }
152 }
153
154 /***
155 * @see org.apache.struts.flow.core.source.Source#getLastModified()
156 */
157 public long getLastModified()
158 {
159 return m_file.lastModified();
160 }
161
162 /***
163 * @see org.apache.struts.flow.core.source.Source#getMimeType()
164 */
165 public String getMimeType()
166 {
167 return URLConnection.getFileNameMap().getContentTypeFor(m_file.getName());
168 }
169
170
171
172
173 public String getScheme()
174 {
175 return m_scheme;
176
177 }
178
179
180
181
182 public String getURI()
183 {
184 return m_uri;
185 }
186
187 /***
188 * Return a validity object based on the file's modification date.
189 *
190 * @see org.apache.struts.flow.core.source.Source#getValidity()
191 */
192 public SourceValidity getValidity()
193 {
194 if (m_file.exists())
195 {
196 return new FileTimeStampValidity(m_file);
197 }
198 else
199 {
200 return null;
201 }
202 }
203
204 /***
205 * @see org.apache.struts.flow.core.source.Source#refresh()
206 */
207 public void refresh()
208 {
209
210 }
211
212 /***
213 * Does this source actually exist ?
214 *
215 * @return true if the resource exists.
216 */
217 public boolean exists()
218 {
219 return getFile().exists();
220 }
221
222
223
224
225
226 /***
227 * @see org.apache.struts.flow.core.source.TraversableSource#getChild(java.lang.String)
228 */
229 public Source getChild(String name) throws SourceException
230 {
231 if (!m_file.isDirectory())
232 {
233 throw new SourceException(getURI() + " is not a directory");
234 }
235
236 return new FileSource(this.getScheme(), new File(m_file, name));
237
238 }
239
240 /***
241 * @see org.apache.struts.flow.core.source.TraversableSource#getChildren()
242 */
243 public Collection getChildren() throws SourceException
244 {
245
246 if (!m_file.isDirectory())
247 {
248 throw new SourceException(getURI() + " is not a directory");
249 }
250
251
252 File[] files = m_file.listFiles();
253
254 FileSource[] children = new FileSource[files.length];
255 for (int i = 0; i < files.length; i++)
256 {
257 children[i] = new FileSource(this.getScheme(), files[i]);
258 }
259
260
261 return Arrays.asList(children);
262 }
263
264 /***
265 * @see org.apache.struts.flow.core.source.TraversableSource#getName()
266 */
267 public String getName()
268 {
269 return m_file.getName();
270 }
271
272 /***
273 * @see org.apache.struts.flow.core.source.TraversableSource#getParent()
274 */
275 public Source getParent() throws SourceException
276 {
277 return new FileSource(getScheme(), m_file.getParentFile());
278 }
279
280 /***
281 * @see org.apache.struts.flow.core.source.TraversableSource#isCollection()
282 */
283 public boolean isCollection()
284 {
285 return m_file.isDirectory();
286 }
287
288
289
290
291
292 /***
293 * Get an <code>InputStream</code> where raw bytes can be written to.
294 * The signification of these bytes is implementation-dependent and
295 * is not restricted to a serialized XML document.
296 *
297 * The output stream returned actually writes to a temp file that replaces
298 * the real one on close. This temp file is used as lock to forbid multiple
299 * simultaneous writes. The real file is updated atomically when the output
300 * stream is closed.
301 *
302 * The returned stream must be closed or cancelled by the calling code.
303 *
304 * @return a stream to write to
305 * @throws ConcurrentModificationException if another thread is currently
306 * writing to this file.
307 */
308 public OutputStream getOutputStream() throws IOException
309 {
310
311
312 File tmpFile = new File(getFile().getPath() + ".tmp");
313
314
315 tmpFile.getParentFile().mkdirs();
316
317
318 if (getFile().exists() && !getFile().canWrite())
319 {
320 throw new IOException("Cannot write to file " + getFile().getPath());
321 }
322
323
324 if (!tmpFile.createNewFile())
325 {
326 throw new ConcurrentModificationException(
327 "File " + getFile().getPath() + " is already being written by another thread");
328 }
329
330
331 return new FileSourceOutputStream(tmpFile, this);
332 }
333
334 /***
335 * Can the data sent to an <code>OutputStream</code> returned by
336 * {@link #getOutputStream()} be cancelled ?
337 *
338 * @return true if the stream can be cancelled
339 */
340 public boolean canCancel(OutputStream stream)
341 {
342 if (stream instanceof FileSourceOutputStream)
343 {
344 FileSourceOutputStream fsos = (FileSourceOutputStream) stream;
345 if (fsos.getSource() == this)
346 {
347 return fsos.canCancel();
348 }
349 }
350
351
352 throw new IllegalArgumentException("The stream is not associated to this source");
353 }
354
355 /***
356 * Cancel the data sent to an <code>OutputStream</code> returned by
357 * {@link #getOutputStream()}.
358 * <p>
359 * After cancel, the stream should no more be used.
360 */
361 public void cancel(OutputStream stream) throws SourceException
362 {
363 if (stream instanceof FileSourceOutputStream)
364 {
365 FileSourceOutputStream fsos = (FileSourceOutputStream) stream;
366 if (fsos.getSource() == this)
367 {
368 try
369 {
370 fsos.cancel();
371 }
372 catch (Exception e)
373 {
374 throw new SourceException("Exception during cancel.", e);
375 }
376 return;
377 }
378 }
379
380
381 throw new IllegalArgumentException("The stream is not associated to this source");
382 }
383
384 /***
385 * Delete the source.
386 */
387 public void delete() throws SourceException
388 {
389 if (!m_file.exists())
390 {
391 throw new SourceNotFoundException("Cannot delete non-existing file " + m_file.toString());
392 }
393
394 if (!m_file.delete())
395 {
396 throw new SourceException("Could not delete " + m_file.toString() + " (unknown reason)");
397 }
398 }
399
400
401
402
403
404 /***
405 * @see org.apache.struts.flow.core.source.ModifiableTraversableSource#makeCollection()
406 */
407 public void makeCollection() throws SourceException
408 {
409 m_file.mkdirs();
410 }
411
412
413
414
415
416 /***
417 * @see org.apache.struts.flow.core.source.MoveableSource#copyTo(org.apache.struts.flow.core.source.Source)
418 */
419 public void copyTo(Source destination) throws SourceException
420 {
421 try
422 {
423 SourceUtil.copy(this.getInputStream(), ((ModifiableSource) destination).getOutputStream());
424 }
425 catch (IOException ioe)
426 {
427 throw new SourceException("Couldn't copy " + getURI() + " to " + destination.getURI(), ioe);
428 }
429 }
430
431 /***
432 * @see org.apache.struts.flow.core.source.MoveableSource#moveTo(org.apache.struts.flow.core.source.Source)
433 */
434 public void moveTo(Source destination) throws SourceException
435 {
436 if (destination instanceof FileSource)
437 {
438 final File dest = ((FileSource) destination).getFile();
439 final File parent = dest.getParentFile();
440
441 if (parent != null)
442 {
443 parent.mkdirs();
444 }
445
446 if (!m_file.renameTo(dest))
447 {
448 throw new SourceException("Couldn't move " + getURI() + " to " + destination.getURI());
449 }
450 }
451 else
452 {
453 SourceUtil.move(this, destination);
454 }
455
456 }
457
458
459
460
461
462 /***
463 * A file outputStream that will rename the temp file to the destination file upon close()
464 * and discard the temp file upon cancel().
465 */
466 private static class FileSourceOutputStream extends FileOutputStream
467 {
468
469 private File m_tmpFile;
470 private boolean m_isClosed = false;
471 private FileSource m_source;
472
473 public FileSourceOutputStream(File tmpFile, FileSource source) throws IOException
474 {
475 super(tmpFile);
476 m_tmpFile = tmpFile;
477 m_source = source;
478 }
479
480 public void close() throws IOException
481 {
482 if (!m_isClosed)
483 {
484 super.close();
485 try
486 {
487
488 if (m_source.getFile().exists())
489 {
490 m_source.getFile().delete();
491 }
492
493 if (!m_tmpFile.renameTo(m_source.getFile()))
494 {
495 throw new IOException("Could not rename " +
496 m_tmpFile.getAbsolutePath() +
497 " to " + m_source.getFile().getAbsolutePath());
498 }
499
500 }
501 finally
502 {
503
504
505 if (m_tmpFile.exists())
506 {
507 m_tmpFile.delete();
508 }
509 m_isClosed = true;
510 }
511 }
512
513 }
514
515 public boolean canCancel()
516 {
517 return !m_isClosed;
518 }
519
520 public void cancel() throws Exception
521 {
522 if (m_isClosed)
523 {
524 throw new IllegalStateException("Cannot cancel : outputstrem is already closed");
525 }
526
527 m_isClosed = true;
528 super.close();
529 m_tmpFile.delete();
530 }
531
532 public void finalize()
533 {
534 if (!m_isClosed && m_tmpFile.exists())
535 {
536
537 m_tmpFile.delete();
538 }
539 }
540
541 public FileSource getSource()
542 {
543 return m_source;
544 }
545 }
546 }