Platinum Partner
java

Using Files in Your Interfaces is Not a Good Idea

I've been using some proprietary frameworks and libraries, that are using File objects, and only File objects, everywhere in the interfaces.

It's not that is bad to have methods accepting File objects, but it's not wise to accept only that. For example the DOM/SAX XML parser in the JDK, accepts File objects but includes also other sources like InputStream, URLs, etc. The Properties class doesn't even accept File objects. In fact, the File class is not very popular in the interfaces of good Java libraries around, including the JRE.
The reason is simple: InputStream is much more abstract than File.

When you specify, for instance[1], an interface like this:

public void sendMail(String email , String message, File attachment);

You have chained the user to specify the path of an existing File on the local disk.

Now, imagine that your application has to send an email about a critical error happening in production to notify the system administrators, and you want to include the stacktrace of the Exception to help tracing the problem. The only option you have is to print the stacktrace into a temporary file, call the method, and then delete the File.

That's because an attachment is a "content" not a File. Also the configuration of an application is "content" not a File. An XML file and your application icons as well, are not just Files, they are data and resources. And data, resources, and contents, they may not just come from Files, but from remote Systems (URLs), from databases, from a jar in the classpath, or other systems you may not even yet imagine.

The example above would have a much better interface if it was defined like this:

public void sendMail(String email, String message, InputStream attachment);

Now... if you want to attach a stacktrace you can do something like:

try {
    ...
} catch (Exception ex) {
    sendMail(
        "foo@bar.com",
        "something bad happened!",
        new ThrowableInputStream(ex));
}

How hard is to transform an exception to an InputStream? that's it:

public class ThrowableInputStream extends InputStream {
	private byte[] bytes;
	private int i = 0;

	public ThrowableInputStream(Throwable throwable) {
		this.bytes = toByteArray(throwable);
	}

	private byte[] toByteArray(Throwable throwable) {
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		PrintStream printer = new PrintStream(output);
		try {
			throwable.printStackTrace(printer);
			printer.flush();
			return output.toByteArray();
		} finally {
			printer.close();
		}
	}

	@Override
	public int read() throws IOException {
		if (i < bytes.length)
			return bytes[i++];
		else
			return -1;
	}
}

Is the above easier or harder to print the exception to a temporary file? And is the above less or more efficient? If you prefer the temporary file... at least remember to remove the file. And don't forget to check if the filesystem is writable... and to verify that there is enough space left on the disk... etc. etc. etc...

You want to attach some XML content that you have received in String format? Again, instead of printing the String to a temporary file, etc.etc... you can write a class that converts Strings to InputStream:

public class StringInputStream extends InputStream {
	private String string;
	private int i;

	public StringInputStream(String string) {
		this.string = string;
	}

	@Override
	public int read() throws IOException {
		if (i < string.length())
			return string.charAt(i++);
		else
			return -1;
	}
}

Just 16 lines of code.

The incredible power of URLs

Also the configuration of your application is part of the interface with the users... and also here Files have their weakness.

But let's go much further with a complex use case: you need to parse some XML data deployed inside a zip file on a remote web server.
You have two options:

  1. You may write the url of the file on the webserver into your configuration. Your program will get the URL from the configuration, access the webserver with an http client, read the zipped file, save it somewhere, unpack it, then parse the xml and manage the data. And finally clean up the mess.
  2. Or, you may specify in your configuration a URL like "jar:http://www.partnerwebsite.com/zippedfile.zip!path/to/data.xml", and pass that URL to the XML Parser. Done.

Now... if you choosed the second option and your application requirement change, and needs to access the same XML file, zipped or not, from a web server, from an ftp server, from the local filesystem, etc. you just need to change the URL in the configuration. And, if you need to access the resource in a protocol that is not supported natively by URL, you can implement an URLStreamHandler.
Yes, URL are very powerful and flexible in Java. And they return you an InputStream. Not a File! It's not a casualty.

How to "fix" a library with such flaws?

Ok... you are the unlucky guy - like me - who has to use bad libraries, more or less frequently. How do you fix an interface like the first sendMail method? (the one with the file)
It's not uncommon that such bad designed libraries, are REALLY bad. So you may think to write your own (object oriented) interface over it: a wrapper or something like that. Your interface will use an InputStream, of course. So you need something that converts your InputStream to a File that can be digested by the bad library.
Here is an example:

public class FileDump {
        private static final Random random = new Random();      

        public static File dump(InputStream input, String fileName) {
                File path = createTempDir();
                return dump(input, path, fileName);
        }

        private static File createTempDir() {
                String tmpDirName =
                                System.getProperty("java.io.tmpdir");
                File tmpDir = new File(tmpDirName);
                File newTempDir = new File(tmpDir,
                                String.valueOf(random.nextInt()));
                newTempDir.mkdirs();
                return newTempDir;
        }

        private static File dump(InputStream input, File path,
                                             String fileName) {
                File file = new File(path, fileName);
                try {
                        OutputStream output =
                                        new FileOutputStream(file);
                        try {
                                int data;
                                while ((data = input.read()) != -1)
                                        output.write(data);
                        } finally {
                                output.close();
                        }
                        return file;
                } catch (IOException ex) {
                        throw new RuntimeException(ex);
                }
        }
}

The above class is an example. You can improve it checking that there is space on disk, that the target directory is writable, etc. Then remember to remove the file after the bad library used the file's content.

Conclusions

  1. Next time you're defining an interface of a library and, for any reason, you find yourself importing java.io.File... think about using (also) an InputStream[2] please.
  2. If you are tempted to write in your configuration a "file path" of a resource that you need to read, think about the opportunity to use an URL with file:/path/to/file.ext protocol. Take in mind by the way, that unfortunately you cannot write to a Java URL, independently from the protocol, but you can still obtain the file path from a URL using the "file" URI scheme.

Notes

[1] The above example is not a good interface of a mailer object. It's just for the purpose to explain why to specify a File as attachment is not a good idea.
[2] The above discussion is valid not only for InputStream and OutputStream but as well for Reader and Writer. You should use Reader/Writer to deal with textual data, while InputStream/OutputStream to deal with binary data.

 

From http://en.newinstance.it/2010/09/10/using-files-in-your-interfaces-is-not-a-good-idea

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}