Luigi is a passionate software developer working in Java since 2000. In 2001 he co-founded the Java User Group Torino, the first local JUG in Italy. Quickly after that, many other JUGs have born in Italy following JUG Torino's example. Since some years now, he is working as free lance software consultant between Italy, Germany, UK and Switzerland, on several industries. His personal interests include software, science fiction books and movies, traveling, photography and good food. Luigi is a DZone MVB and is not an employee of DZone and has posted 19 posts at DZone. You can read more from them at their website. View Full User Profile

Using Files in Your Interfaces is Not a Good Idea

09.11.2010
| 12283 views |
  • submit to reddit

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

Published at DZone with permission of Luigi Viggiano, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Fab Mars replied on Sat, 2010/09/11 - 7:17am

Using Files in Your Interfaces is Not a Good Idea: totally agree.

RIchard replied on Sat, 2010/09/11 - 11:58am

Awesome, I've been working with files for the last two weeks. I cant reiterate enough how tired I am of matching streams, or reading a file into a buffer or vise versa ... If there's one thing that Java needs, it a good pruning. drop enumeration, Xin/out streams, file etc get homogenized and present a hub and spoke API where any type can be transformed into any other type via the hub.

Alexander Radzin replied on Sat, 2010/09/11 - 1:18pm

Absolutely agree with your approach.

As a guy that deals with bad interfaces too I can say that I saught about other trick that can help to abuse File. 

File does not implement IO operations itself. It is done in FileInputStream and FileOutputStream that do almost nothing too using FileDescriptor internaly. If you manage to change file descriptor of FileInputStream that wraps your file you can make the rest of the application to work with "file" that is actually not a file at all.This can be implemented using reflection.

This is instead creating temp file that can be problematic in some cases due to security restrictions. But I have never try to implement this...

Luigi Viggiano replied on Sat, 2010/09/11 - 2:37pm

«hub and spoke API». Richard: where I can read more about this approach? an example somewhere? 

Personally I don't dislike Streams, I find them quite flexible and handy.

Luigi.

Roger Voss replied on Sun, 2010/09/12 - 2:45pm

Refactoring subpar interfaces on libraries that come from an external source might be a situation that AspectJ could be put to good use - refactor that bad interface into one that is desired - all the while not touching the original source code of the library (just need it's jar file and perhaps the opportunity to at least inspect its source code).


In the example bad interface discussed here, it is likely that the library will internally use the File object to create a FileInputStream. There may even be a private or protected helper method in the library that accepts either FileInputStream or InputStream and then proceeds with the desired i/o operation.

At any rate, if we can look at the source code (or perhaps a dump of the class information from the .class file), we can write an AspectJ aspect that provides a new method in the interface accepting InputStream. We either call the helper function (which this aspect will be able to do because after byte code weaving into the target class, it's an actual method of the class and can access private and protected class members), or we lift the source code of the existing File-based method and re-implement it in an aspect-based new method to where it operates with just an InputStream as an input argument).

Once we have a core method that accepts an InputStream, we can then use aspects to weave in other methods taking URLs and the like, and which eventually call the InputStream-based method.

We wind up with an interface to the library that is ideally designed to the way we prefer.

By using compile time weaving of the AspectJ aspects, we can use just the distribution .jar file of the library as input at build time. The AspectJ build-time output will be a new version of the .jar that sports the refactored interface that we consider to be ideal for the library. This then becomes the .jar file of the library that our overall project proceeds to make use of.

While only relying on the .jar of a third-party library (thus not having to mess with being able to rebuild said library - which can sometimes be a complex endeavor), and perhaps the ability to inspect its source code, we can utilize AspectJ and its compile-time weaving option as a technique to address bugs and/or refactor interfaces. We can always send our fixes and enhancements to the library author(s) and hope they eventually incorporate them, but in the meantime, AspectJ enables us to immediately proceed with our own project work.

Jason Wang replied on Sun, 2010/09/12 - 6:06pm

Some very good points on why using stream is way better than using file in interfaces. But just one thing I disagree on: the StringInputStream looks abit odd to me. Since you are dealing with Strings, I think Reader family is a far more suitable than Stream. Plus there is alreay a StringReader class available in the JDK. JasonW

Matthew Sandoz replied on Sun, 2010/09/12 - 9:01pm

thanks - nice post. badly designed apis are a special pet peeve of mine. another point against using files is that they are final and thus harder to mock with test code. when in doubt, look at the way the unix utilities are designed - most of them work on streams.

Luigi Viggiano replied on Mon, 2010/09/13 - 3:35am in response to: Jason Wang

True ( see note [2] ). But, for instance, take SAXPArser 's parse method: it doesn't accept a Reader. And from a Reader you can't get an InputStream (while the opposite is true). From an InputStream you can get a reader. So if your XML is stored in a String, you can use my StringInputStream to pass it to the SAXParser, but not the StringReader.

For the purpose of using a String as binary attachment, I believe that the above example could fit. Of course, to deal with textual data Reader/Writer are the correct classes to use.

Luigi Viggiano replied on Mon, 2010/09/13 - 3:21am in response to: Matthew Sandoz

Matthew > another point against using files is that they are final and thus harder to mock with test code.

Have a look at mockito. In the context I explained, I had to write some tests mocking File objects:

File path = mock(File.class);
when(path.exists()).thenReturn(false);
when(path.mkdirs()).thenReturn(true);
...//something using path.mkdir()
verify(path).mkdirs();

 

it worked.

BTW, I just checked and java.io.File is not final.

Andrea Polci replied on Mon, 2010/09/13 - 11:05am in response to: Luigi Viggiano

Nice post, I just want to add something to the StringInputStream part.

The reason why there is a StringReader but not a StringInputStream in the java library is that while Strings and Readers work with chars (unicode characters) Streams work with bytes. Your code for StringInputStream is dependant on the default charset and in the content of the string. It will usually work if the String contains only plain ascii characters, but youl'll get in trouble otherwise.

A better way to get an InputStream from a String is through the ByteArrayInputStream:

InputStream is = new ByteArrayInputStream(source.getBytes());

This is less efficient because the String.getBytes() has to create a new byte[] and to encode all the characters in the String according to the default charset.

Alternatively is possible to provide an implementation of StringInputStream that manage the conversion between char and bytes but it will be more complicated cause a single char can result in multiple bytes (and the number of bytes can be different for every char, depending on the charset).

Andrea Polci replied on Mon, 2010/09/13 - 11:41am in response to: RIchard

The problem with File is not the design of the library but it's usage (or overuse).

The File is meant to represent ... a file (you guessed?). Not it's content but exactly what it is (an entry in a directory in the filesystem).

Unfortunatly many use a File when what they really need is something to access the content.

King Sam replied on Fri, 2012/02/24 - 9:31am

You’re right in applying the Interface Segregation Principle: code should not depend on abstractions that it does not use (like a File with read/writing capability when it only needs an InputStream or OutputStream). I only wish converting between the two and String would be simpler in Java…

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.