001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.vfs.provider.ftp;
018:
019: import org.apache.commons.logging.Log;
020: import org.apache.commons.logging.LogFactory;
021: import org.apache.commons.net.ftp.FTPFile;
022: import org.apache.commons.vfs.FileName;
023: import org.apache.commons.vfs.FileObject;
024: import org.apache.commons.vfs.FileSystemException;
025: import org.apache.commons.vfs.FileType;
026: import org.apache.commons.vfs.RandomAccessContent;
027: import org.apache.commons.vfs.provider.AbstractFileObject;
028: import org.apache.commons.vfs.provider.UriParser;
029: import org.apache.commons.vfs.util.Messages;
030: import org.apache.commons.vfs.util.MonitorInputStream;
031: import org.apache.commons.vfs.util.MonitorOutputStream;
032: import org.apache.commons.vfs.util.RandomAccessMode;
033: import org.apache.commons.vfs.util.FileObjectUtils;
034:
035: import java.io.IOException;
036: import java.io.InputStream;
037: import java.io.OutputStream;
038: import java.util.Calendar;
039: import java.util.Collections;
040: import java.util.Iterator;
041: import java.util.Map;
042: import java.util.TreeMap;
043:
044: /**
045: * An FTP file.
046: *
047: * @author <a href="mailto:adammurdoch@apache.org">Adam Murdoch</a>
048: * @version $Revision: 520100 $ $Date: 2007-03-19 13:54:27 -0700 (Mon, 19 Mar 2007) $
049: */
050: public class FtpFileObject extends AbstractFileObject {
051: private Log log = LogFactory.getLog(FtpFileObject.class);
052:
053: private static final Map EMPTY_FTP_FILE_MAP = Collections
054: .unmodifiableMap(new TreeMap());
055:
056: private final FtpFileSystem ftpFs;
057: private final String relPath;
058:
059: // Cached info
060: private FTPFile fileInfo;
061: private Map children;
062: private FileObject linkDestination;
063:
064: private boolean inRefresh = false;
065:
066: protected FtpFileObject(final FileName name,
067: final FtpFileSystem fileSystem, final FileName rootName)
068: throws FileSystemException {
069: super (name, fileSystem);
070: ftpFs = fileSystem;
071: String relPath = UriParser.decode(rootName
072: .getRelativeName(name));
073: if (".".equals(relPath)) {
074: // do not use the "." as path against the ftp-server
075: // e.g. the uu.net ftp-server do a recursive listing then
076: // this.relPath = UriParser.decode(rootName.getPath());
077: // this.relPath = ".";
078: this .relPath = null;
079: } else {
080: this .relPath = relPath;
081: }
082: }
083:
084: /**
085: * Called by child file objects, to locate their ftp file info.
086: *
087: * @param name the filename in its native form ie. without uri stuff (%nn)
088: * @param flush recreate children cache
089: */
090: private FTPFile getChildFile(final String name, final boolean flush)
091: throws IOException {
092: /* If we should flush cached children, clear our children map unless
093: * we're in the middle of a refresh in which case we've just recently
094: * refreshed our children. No need to do it again when our children are
095: * refresh()ed, calling getChildFile() for themselves from within
096: * getInfo(). See getChildren(). */
097: if (flush && !inRefresh) {
098: children = null;
099: }
100:
101: // List the children of this file
102: doGetChildren();
103:
104: // Look for the requested child
105: FTPFile ftpFile = (FTPFile) children.get(name);
106: return ftpFile;
107: }
108:
109: /**
110: * Fetches the children of this file, if not already cached.
111: */
112: private void doGetChildren() throws IOException {
113: if (children != null) {
114: return;
115: }
116:
117: final FtpClient client = ftpFs.getClient();
118: try {
119: final FTPFile[] tmpChildren = client.listFiles(relPath);
120: if (tmpChildren == null || tmpChildren.length == 0) {
121: children = EMPTY_FTP_FILE_MAP;
122: } else {
123: children = new TreeMap();
124:
125: // Remove '.' and '..' elements
126: for (int i = 0; i < tmpChildren.length; i++) {
127: final FTPFile child = tmpChildren[i];
128: if (child == null) {
129: if (log.isDebugEnabled()) {
130: log
131: .debug(Messages
132: .getString(
133: "vfs.provider.ftp/invalid-directory-entry.debug",
134: new Object[] {
135: new Integer(
136: i),
137: relPath }));
138: }
139: continue;
140: }
141: if (!".".equals(child.getName())
142: && !"..".equals(child.getName())) {
143: children.put(child.getName(), child);
144: }
145: }
146: }
147: } finally {
148: ftpFs.putClient(client);
149: }
150: }
151:
152: /**
153: * Attaches this file object to its file resource.
154: */
155: protected void doAttach() throws IOException {
156: // Get the parent folder to find the info for this file
157: getInfo(false);
158: }
159:
160: /**
161: * Fetches the info for this file.
162: */
163: private void getInfo(boolean flush) throws IOException {
164: final FtpFileObject parent = (FtpFileObject) FileObjectUtils
165: .getAbstractFileObject(getParent());
166: FTPFile newFileInfo;
167: if (parent != null) {
168: newFileInfo = parent.getChildFile(UriParser
169: .decode(getName().getBaseName()), flush);
170: } else {
171: // Assume the root is a directory and exists
172: newFileInfo = new FTPFile();
173: newFileInfo.setType(FTPFile.DIRECTORY_TYPE);
174: }
175:
176: this .fileInfo = newFileInfo;
177: }
178:
179: /**
180: * @throws FileSystemException
181: */
182: public void refresh() throws FileSystemException {
183: if (!inRefresh) {
184: try {
185: inRefresh = true;
186: super .refresh();
187: try {
188: // this will tell the parent to recreate its children collection
189: getInfo(true);
190: } catch (IOException e) {
191: throw new FileSystemException(e);
192: }
193: } finally {
194: inRefresh = false;
195: }
196: }
197: }
198:
199: /**
200: * Detaches this file object from its file resource.
201: */
202: protected void doDetach() {
203: this .fileInfo = null;
204: children = null;
205: }
206:
207: /**
208: * Called when the children of this file change.
209: */
210: protected void onChildrenChanged(FileName child, FileType newType) {
211: if (children != null && newType.equals(FileType.IMAGINARY)) {
212: try {
213: children.remove(UriParser.decode(child.getBaseName()));
214: } catch (FileSystemException e) {
215: throw new RuntimeException(e.getMessage());
216: }
217: } else {
218: // if child was added we have to rescan the children
219: // TODO - get rid of this
220: children = null;
221: }
222: }
223:
224: /**
225: * Called when the type or content of this file changes.
226: */
227: protected void onChange() throws IOException {
228: children = null;
229:
230: if (getType().equals(FileType.IMAGINARY)) {
231: // file is deleted, avoid server lookup
232: this .fileInfo = null;
233: return;
234: }
235:
236: getInfo(true);
237: }
238:
239: /**
240: * Determines the type of the file, returns null if the file does not
241: * exist.
242: */
243: protected FileType doGetType() throws Exception {
244: if (this .fileInfo == null) {
245: return FileType.IMAGINARY;
246: } else if (this .fileInfo.isDirectory()) {
247: return FileType.FOLDER;
248: } else if (this .fileInfo.isFile()) {
249: return FileType.FILE;
250: } else if (this .fileInfo.isSymbolicLink()) {
251: return getLinkDestination().getType();
252: }
253:
254: throw new FileSystemException(
255: "vfs.provider.ftp/get-type.error", getName());
256: }
257:
258: private FileObject getLinkDestination() throws FileSystemException {
259: if (linkDestination == null) {
260: final String path = this .fileInfo.getLink();
261: FileName relativeTo = getName().getParent();
262: if (relativeTo == null) {
263: relativeTo = getName();
264: }
265: FileName linkDestinationName = getFileSystem()
266: .getFileSystemManager().resolveName(relativeTo,
267: path);
268: linkDestination = getFileSystem().resolveFile(
269: linkDestinationName);
270: }
271:
272: return linkDestination;
273: }
274:
275: protected FileObject[] doListChildrenResolved() throws Exception {
276: if (this .fileInfo.isSymbolicLink()) {
277: return getLinkDestination().getChildren();
278: }
279:
280: return null;
281: }
282:
283: /**
284: * Returns the file's list of children.
285: *
286: * @return The list of children
287: * @throws FileSystemException If there was a problem listing children
288: * @see AbstractFileObject#getChildren()
289: * @since 1.0
290: */
291: public FileObject[] getChildren() throws FileSystemException {
292: try {
293: /* Wrap our parent implementation, noting that we're refreshing so
294: * that we don't refresh() ourselves and each of our parents for
295: * each children. Note that refresh() will list children. Meaning,
296: * if if this file has C children, P parents, there will be (C * P)
297: * listings made with (C * (P + 1)) refreshes, when there should
298: * really only be 1 listing and C refreshes. */
299:
300: this .inRefresh = true;
301: return super .getChildren();
302: } finally {
303: this .inRefresh = false;
304: }
305: }
306:
307: /**
308: * Lists the children of the file.
309: */
310: protected String[] doListChildren() throws Exception {
311: // List the children of this file
312: doGetChildren();
313:
314: // TODO - get rid of this children stuff
315: final String[] childNames = new String[children.size()];
316: int childNum = -1;
317: Iterator iterChildren = children.values().iterator();
318: while (iterChildren.hasNext()) {
319: childNum++;
320: final FTPFile child = (FTPFile) iterChildren.next();
321: childNames[childNum] = child.getName();
322: }
323:
324: return UriParser.encode(childNames);
325: }
326:
327: /**
328: * Deletes the file.
329: */
330: protected void doDelete() throws Exception {
331: final boolean ok;
332: final FtpClient ftpClient = ftpFs.getClient();
333: try {
334: if (this .fileInfo.isDirectory()) {
335: ok = ftpClient.removeDirectory(relPath);
336: } else {
337: ok = ftpClient.deleteFile(relPath);
338: }
339: } finally {
340: ftpFs.putClient(ftpClient);
341: }
342:
343: if (!ok) {
344: throw new FileSystemException(
345: "vfs.provider.ftp/delete-file.error", getName());
346: }
347: this .fileInfo = null;
348: children = EMPTY_FTP_FILE_MAP;
349: }
350:
351: /**
352: * Renames the file
353: */
354: protected void doRename(FileObject newfile) throws Exception {
355: final boolean ok;
356: final FtpClient ftpClient = ftpFs.getClient();
357: try {
358: String oldName = getName().getPath();
359: String newName = newfile.getName().getPath();
360: ok = ftpClient.rename(oldName, newName);
361: } finally {
362: ftpFs.putClient(ftpClient);
363: }
364:
365: if (!ok) {
366: throw new FileSystemException(
367: "vfs.provider.ftp/rename-file.error", new Object[] {
368: getName().toString(), newfile });
369: }
370: this .fileInfo = null;
371: children = EMPTY_FTP_FILE_MAP;
372: }
373:
374: /**
375: * Creates this file as a folder.
376: */
377: protected void doCreateFolder() throws Exception {
378: final boolean ok;
379: final FtpClient client = ftpFs.getClient();
380: try {
381: ok = client.makeDirectory(relPath);
382: } finally {
383: ftpFs.putClient(client);
384: }
385:
386: if (!ok) {
387: throw new FileSystemException(
388: "vfs.provider.ftp/create-folder.error", getName());
389: }
390: }
391:
392: /**
393: * Returns the size of the file content (in bytes).
394: */
395: protected long doGetContentSize() throws Exception {
396: if (this .fileInfo.isSymbolicLink()) {
397: return getLinkDestination().getContent().getSize();
398: } else {
399: return this .fileInfo.getSize();
400: }
401: }
402:
403: /**
404: * get the last modified time on an ftp file
405: *
406: * @see org.apache.commons.vfs.provider.AbstractFileObject#doGetLastModifiedTime()
407: */
408: protected long doGetLastModifiedTime() throws Exception {
409: if (this .fileInfo.isSymbolicLink()) {
410: return getLinkDestination().getContent()
411: .getLastModifiedTime();
412: } else {
413: Calendar timestamp = this .fileInfo.getTimestamp();
414: if (timestamp == null) {
415: return 0L;
416: } else {
417: return (timestamp.getTime().getTime());
418: }
419: }
420: }
421:
422: /**
423: * Creates an input stream to read the file content from.
424: */
425: protected InputStream doGetInputStream() throws Exception {
426: final FtpClient client = ftpFs.getClient();
427: final InputStream instr = client.retrieveFileStream(relPath);
428: return new FtpInputStream(client, instr);
429: }
430:
431: protected RandomAccessContent doGetRandomAccessContent(
432: final RandomAccessMode mode) throws Exception {
433: return new FtpRandomAccessContent(this , mode);
434: }
435:
436: /**
437: * Creates an output stream to write the file content to.
438: */
439: protected OutputStream doGetOutputStream(boolean bAppend)
440: throws Exception {
441: final FtpClient client = ftpFs.getClient();
442: OutputStream out = null;
443: if (bAppend) {
444: out = client.appendFileStream(relPath);
445: } else {
446: out = client.storeFileStream(relPath);
447: }
448:
449: if (out == null) {
450: throw new FileSystemException(
451: "vfs.provider.ftp/output-error.debug",
452: new Object[] { this .getName(),
453: client.getReplyString() });
454: }
455:
456: return new FtpOutputStream(client, out);
457: }
458:
459: String getRelPath() {
460: return relPath;
461: }
462:
463: FtpInputStream getInputStream(long filePointer) throws IOException {
464: final FtpClient client = ftpFs.getClient();
465: final InputStream instr = client.retrieveFileStream(relPath,
466: filePointer);
467: if (instr == null) {
468: throw new FileSystemException(
469: "vfs.provider.ftp/input-error.debug", new Object[] {
470: this .getName(), client.getReplyString() });
471: }
472: return new FtpInputStream(client, instr);
473: }
474:
475: /**
476: * An InputStream that monitors for end-of-file.
477: */
478: class FtpInputStream extends MonitorInputStream {
479: private final FtpClient client;
480:
481: public FtpInputStream(final FtpClient client,
482: final InputStream in) {
483: super (in);
484: this .client = client;
485: }
486:
487: void abort() throws IOException {
488: client.abort();
489: close();
490: }
491:
492: /**
493: * Called after the stream has been closed.
494: */
495: protected void onClose() throws IOException {
496: final boolean ok;
497: try {
498: ok = client.completePendingCommand();
499: } finally {
500: ftpFs.putClient(client);
501: }
502:
503: if (!ok) {
504: throw new FileSystemException(
505: "vfs.provider.ftp/finish-get.error", getName());
506: }
507: }
508: }
509:
510: /**
511: * An OutputStream that monitors for end-of-file.
512: */
513: private class FtpOutputStream extends MonitorOutputStream {
514: private final FtpClient client;
515:
516: public FtpOutputStream(final FtpClient client,
517: final OutputStream outstr) {
518: super (outstr);
519: this .client = client;
520: }
521:
522: /**
523: * Called after this stream is closed.
524: */
525: protected void onClose() throws IOException {
526: final boolean ok;
527: try {
528: ok = client.completePendingCommand();
529: } finally {
530: ftpFs.putClient(client);
531: }
532:
533: if (!ok) {
534: throw new FileSystemException(
535: "vfs.provider.ftp/finish-put.error", getName());
536: }
537: }
538: }
539: }
|