3
\$\begingroup\$

Doing IO in Java is a big pain. It got better with Guava and Java 7 and now about everything is there, but I never know exactly where.

Most of the time I need to slurp a file. Nearly as often I need to write a file, while making sure, I don't end up with some garbage in case of an IO error (atomicity). Leaving .tmp files around in such a case is intended. Overwriting (and then deleting) a xxx.tmp when xxx gets written is a non-issue for me.

Sometimes the location is given like clazz.getResource(name). Sometimes, this resource is a file (when working in an IDE with sources and class files; I sometimes generate some data files in the source tree), sometimes it's not. Reading must work in both cases, writing only in the first.

The encoding is UTF-8 by default, but can be changed. Concerning platform default encoding, I don't give a damn, as the the files must be portable by simply binary copying them from one system to another. It's all rather hacky, but it does exactly what I want.

The imports are omitted for brevity and can be found here. Feel free to ignore one-liners containing conditions, as I'm not gonna change this.

I'll probably rewrite it to be Android-compatible, so no try with resources (there'll be a problem with java.nio.file, but on Linux files moves are atomic, so it's not needed; the challenge will be to get atomicity on both Windows with Java 7+ and Android with Java 6).

@RequiredArgsConstructor(access=AccessLevel.PRIVATE) public class MyIoResource {
    public MyIoResource(File file) {
        this(UTF8, checkNotNull(file), null, null);
    }

    public MyIoResource(Class<?> clazz, String name) {
        this(UTF8, null, checkNotNull(clazz), checkNotNull(name));
    }

    public MyIoResource asAnsi() {
        return withCharset(ANSI);
    }

    public byte[] readBytes() throws IOException {
        if (file!=null) return Files.readAllBytes(file.toPath());
        final URL url = newUrl(clazz, name);
        final InputStream in = url.openStream();
        try {
            final ByteArrayOutputStream result = new ByteArrayOutputStream();
            ByteStreams.copy(in, result);
            return result.toByteArray();
        } finally {
            in.close();
        }
    }

    public String readString() throws IOException {
        return new String(readBytes(), charset);
    }

    public void writeAtomic(byte[] data) throws IOException {
        checkNotNull(data);
        final File file = this.file!=null ? this.file : newFile();
        final File tmpFile = new File(file.getParent(), file.getName() + ".tmp");
        final StandardCopyOption[] options = {StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING};
        Files.write(tmpFile.toPath(), data);
        Files.move(tmpFile.toPath(), file.toPath(), options);
    }

    public void writeAtomic(String data) throws IOException {
        writeAtomic(data.getBytes(charset));
    }

    public void writeAtomic(CharSequence data) throws IOException {
        writeAtomic(data.toString());
    }

    public void writeAtomic(Iterable<String> data) throws IOException {
        if (Iterables.isEmpty(data)) {
            writeAtomic(new byte[0]);
        } else {
            writeAtomic(Joiner.on('\n').appendTo(new StringBuilder(), data).append('\n'));
        }
    }

    private File newFile() throws IOException {
        final URL dirUrl = newUrl(clazz, ".");
        final URL url = new URL(dirUrl, name);
        try {
            return new File(url.toURI());
        } catch (final URISyntaxException e) {
            throw new IOException(e); // Don't pollute everything with URISyntaxException.
        }
    }

    private static URL newUrl(Class<?> clazz, String name) throws NoSuchFileException {
        final URL url = clazz.getResource(name);
        if (url!=null) return url;
        throw new NoSuchFileException("No such resource: " + clazz.getName() + " " + name);
    }

    private static final Charset UTF8 = Charsets.UTF_8;
    private static final Charset ANSI = Charsets.ISO_8859_1;

    @Wither private final Charset charset;
    private final File file;
    private final Class<?> clazz;
    private final String name;
}
\$\endgroup\$
1

1 Answer 1

3
\$\begingroup\$

Sometimes the location is given like clazz.getResource(name). Sometimes, this resource is a file (when working in an IDE with sources and class files; I sometimes generate some data files in the source tree), sometimes it's not. Reading must work in both cases, writing only in the first.

It sounds like this one class should really be two classes. Another indicator of that is when you have constructors used like this:

    this(charset, file, null, null);
    this(charset, null, clazz, name);

That's really two implementations masquerading as one class.

How about splitting to smaller pieces:

  • IoResource: common interface that file-based and location-based resources can implement
  • AbstractIoResource: skeletal implementation of common logic
  • FileIoResource: file-based implementation, with appropriate constructor
  • ClassPathIoResource: location-based implementation, with appropriate constructor, and write operations throwing UnsupportedOperationException

If you don't want to force clients to know the right class names for the appropriate types, you can provide factory methods in one common class. For inspiration, think of Collections and its various methods, such as Collections.synchronizedList and Collections.unmodifiableList: the actual implementation types are not visible to clients. If you go with this approach, then the FileIoResource and ClassPathIoResource implementation can be even private.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ +1 I was so happy that I could unify the access that I didn't think about split implementations. I guess, I'll create the hierarchy (but probably without the interface as I can easily add it later if ever needed). The UOE is a good idea as well. \$\endgroup\$
    – maaartinus
    Nov 28, 2014 at 20:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.