Input and Output - Java 8 Recipes, 2th Edition (2014)

Java 8 Recipes, 2th Edition (2014)

CHAPTER 8. Input and Output

Oftentimes in applications, there is a requirement to obtain and manipulate the I/O terminals. In today’s operating systems, that usually means file access and network connectivity. In previous releases, Java was slow to adopt a good file and network framework in order to maintain universal compatibility. Standing true to its roots of write once, read everywhere, a lot of the original file I/O and network connectivity needed to be simple and universal. Since the release of Java 7, developers have been taking advantage of much better I/O APIs.

The file and network I/O has evolved over the years into a much better framework for handling files, network scalability, and ease of use,. As of the network input output version 2 API (NIO.2), Java has the capability of monitoring folders, accessing OS-dependent methods, and create scalable asynchronous network sockets. This is in addition to the already robust library for handling input and output streams, and serializing (and deserializing) object information.

In this chapter, we cover recipes that demonstrate different input and output processes. You learn about serialization of files, sending files over the network, file manipulation, and much more. After reading the recipes in this chapter, you will be armed with the capability to develop applications containing sophisticated input and output tasks.

STREAMS AND THE DECORATOR PATTERN

I/O streams are the foundation of most of the Java I/O and include a plethora of ready-made streams for any occasion, but they are very confusing to use if some context is not provided. A stream (like a river) represents an inflow/outflow of data. Think about it this way. When you type, you create a stream of characters that the system receives (input stream). When the system produces sounds, it sends them to the speaker (output stream). The system could be receiving keystrokes and sending sound all day long, and thus the streams can be either processing data or waiting for more data.

When a stream doesn’t receive any data, it waits (nothing else to do, right?). As soon as data comes in, the stream starts processing this data. The stream then stops and waits for the next data item to come. This keeps going until this proverbial river becomes dry (the stream is closed).

Like a river, streams can be connected to each other (this is the decorator pattern). For the content of this chapter, there are mainly two input streams that you care about. One of them is the file input stream, and the other is the network socket input stream. These two streams are a source of data for your I/O programs. There are also their corresponding output streams: file output stream and the network socket output streams (how creative isn’t it?). Like a plumber, you can hook them together and create something new. For example, you could weld together a file input stream to a network output stream to send the contents of the file through a network socket. Or you could do the opposite and connect a network input stream (data coming in) to a file output stream (data being written to disk). In I/O parlance, the input streams are calledsources, while the output streams are called sinks.

There are other input and output streams that can be glued together. For example, there is a BufferedInputStream, which allows you to read the data in chunks (it’s more efficient than reading it byte by byte), and DataOutputStream allows you to write Java primitives to an output stream (instead of just writing bytes). One of the most useful streams is the ObjectInputStream and ObjectOutputStream pair, which will allow you to serialize/deserialize object (there is a recipe for that here).

The decorator pattern allows you to keep plucking streams together to get many different effects. The beauty of this design is that you can actually create a stream that will take any input and produce any output, and then can be thrown together with every other stream.

8-1. Serializing Java Objects

Problem

You need to serialize a class (save the contents of the class) so that you can restore it at a later time.

Solution

Java implements a built-in serialization mechanism. You access that mechanism via the ObjectOutputStream class. In the following example, the method saveSettings() uses an ObjectOutputStream to serialize the settings object in preparation for writing the object to disk:

public class Ch_8_1_SerializeExample {
public static void main(String[] args) {
Ch_8_1_SerializeExample example = new Ch_8_1_SerializeExample();
example.start();
}

private void start() {
ProgramSettings settings = new ProgramSettings( new Point(10,10),
new Dimension(300,200), Color.blue,
"The title of the application" );
saveSettings(settings,"settings.bin");
ProgramSettings loadedSettings = loadSettings("settings.bin");
System.out.println("Are settings are equal? :"+loadedSettings.equals(settings));

}

private void saveSettings(ProgramSettings settings, String filename) {
try {
FileOutputStream fos = new FileOutputStream(filename);
try (ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(settings);
}
} catch (IOException e) {
e.printStackTrace();
}
}

private ProgramSettings loadSettings(String filename) {
try {
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
return (ProgramSettings) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

How It Works

Java supports serialization, which is the capability of taking an object and creating a byte representation that can be used to restore the object at a later time. By using an internal serialization mechanism, most of the setup to serialize objects is taken care of. Java will transform the properties of an object into a byte stream, which can then be saved to a file or transmitted over the wire.

Image Note The original Java Serialization framework uses reflection to serialize the objects, so it might be an issue if serializing/deserializing heavily. There are plenty of open source frameworks that offer different trade-offs depending on your need (speed versus size versus ease of use). Seehttps://github.com/eishay/jvm-serializers/wiki/.

For a class to be serializable, it needs to implement the Serializable interface, which is a Marker interface: it doesn’t have any methods, but instead tells the serialization mechanism that you have allowed the ability of your class to be serialized. While not evident from the onset, serialization exposes all the internal workings of your class (including protected and private members), so if you want to keep secret the authorization code for a nuclear launch, you might want to make any class that contains such information nonserializable.

It is also necessary that all properties (a.k.a. members, variables, or fields) of the class are serializable (and/or transient, which we will get to in a minute). All primitives—int, long, double, and float (plus their wrapper classes)—and the String class, are serializable by design. Other Java classes are serializable on a case-by-case basis. For example, you can’t serialize any Swing components (like JButton or JSpinner), and you can’t serialize File objects, but you can serialize the Color class (awt.color, to be more precise).

As a design principle you don’t want to serialize your main classes, but instead you want to create classes that contain only the properties that you want to serialize. It will save a lot of headache in debugging because serialization becomes very pervasive. If you mark a major class as serializable (implements Serializable), and this class contains many other properties, you need to declare those classes as serializable as well. If your Java class inherits from another class, the parent class must also be serializable. In that way, you will find marking classes serializable when they really shouldn’t be.

If you want to mark a property as nonserializable, you may mark it as transient. Transient properties tell the Java compiler that you are not interested in saving/loading the property value, so it will be ignored. Some properties are good candidates for being transient, like cached calculations, or a date formatter that you always instantiate to the same value.

By the virtue of the Serialization framework, static properties are not serializable; neither are static classes. The reason is that a static class cannot be instantiated. Therefore, if you save and then load the static class at the same time, you will have loaded another copy of thestatic class, throwing the JVM for a loop.

The Java serialization mechanism works behind the scenes to convert and traverse every object within the class that is marked as Serializable. If an application contains objects within objects, and even perhaps contains cross-referenced objects, the Serialization framework will resolve those objects, and store only one copy of any object. Each property then gets translated to a byte[] representation. The format of the byte array includes the actual class name (for example: com.somewhere.over.the.rainbow.preferences.UserPreferences), followed by the encoding of the properties (which in turn may encode another object class, with its properties, etc., etc., ad infinitum).

For the curious, if you look at the file generated (even in a text editor), you can see the class name as almost the first part of the file.

Image Note Serialization is very brittle. By default, the Serialization framework generates a Stream Unique Identifier (SUID) that captures information about what fields are presented in the class, what kind they are (public/protected), and what is transient, among other things. Even a perceived slight modification of the class (for example, changing an int to a long property) will generate a new SUID. A class that has been saved with a prior SUID cannot be deserialized on the new SUID. This is done to protect the serialization/deserialization mechanism, while also protecting the designers.

You can actually tell the Java class to use a specific SUID. This will allow you to serialize classes, modify them, and then deserialize the original classes while implementing some backward compatibility. The danger you run into is that the deserialization must be backward-compatible. Renaming or removing fields will generate an exception as the class is being deserialized. If you are specifying your own serial Serializable on your Serializable class, be sure to have some unit tests for backward-compatibility every time you change the class. In general, the changes that can be made on a class to keep it backward-compatible are found here: http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html.

Due to the nature of serialization, don’t expect constructors to be called when an object is deserialized. If you have initialization code in constructors that is required for your object to function properly, you may need to refactor the code out of the constructor to allow proper execution after construction. The reason is that in the deserialization process, the deserialized objects are “restored” internally (not created) and don’t invoke constructors.

8-2. Serializing Java Objects More Efficiently

Problem

You want to serialize a class, but want to make the output more efficient, or smaller in size, than the product generated via the built-in serialization method.

Solution

By making the object implement the Externalizable interface, you instruct the Java Virtual Machine to use a custom serialization/deserialization mechanism, as provided by the readExternal/writeExternal methods in the following example.

public class ExternalizableProgramSettings implements Externalizable {
private Point locationOnScreen;
private Dimension frameSize;
private Color defaultFontColor;
private String title;

// Empty constructor, required for Externalizable implementors
public ExternalizableProgramSettings() {

}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(locationOnScreen.x);
out.writeInt(locationOnScreen.y);
out.writeInt(frameSize.width);
out.writeInt(frameSize.height);
out.writeInt(defaultFontColor.getRGB());
out.writeUTF(title);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
locationOnScreen = new Point(in.readInt(), in.readInt());
frameSize = new Dimension(in.readInt(), in.readInt());
defaultFontColor = new Color(in.readInt());
title = in.readUTF();
}
// getters and setters omitted for brevity
}

How It Works

The Java Serialization framework provides the ability for you to specify the implementation for serializing an object. As such, it requires implementing the Externalizable interface in lieu of the Serializable interface. The Externalizable interface contains two methods:writeExternal(ObjectOutput out) and readExternal(ObjectInput in). By implementing these methods, you are telling the framework how to encode/decode your object.

The writeExternal() method will pass in as a parameter an ObjectOutput object. This object will then let you write your own encoding for the serialization. The ObjectOutput contains the methods listed in Table 8-1.

Table 8-1. ObjectOutput Methods

ObjectOutput

ObjectInput

Description

writeBoolean (boolean v)

booleanreadBoolean ()

Read/writes the Boolean primitive.

writeByte(int v)

intreadByte()

Read/writes a byte.

Note: Java doesn’t have a byte primitive, so an int is used as a parameter, but only the least-significant byte will be written.

writeShort(int v)

intreadShort()

Read/writes two bytes.

Note: Only the two least-significant bytes will be written.

writeChar(int v)

intreadChar()

Read/writes two bytes as a char (reverse order than writeShort).

writeInt (int v)

intreadInt()

Read/writes an integer.

writeLong (long v)

intreadLong()

Read/writes a long.

writeDouble (double v)

double readDouble

Read/writes a double.

One reason you may choose to implement the Externalizable interface instead of the Serializable interface is because Java’s default serialization is very inefficient. Because the Java Serialization framework needs to ensure that every object (and dependent object) is serialized, it will write even objects that have default values or that might be empty and/or null. Implementing the Externalizable interface also provides for finer-grained control on how your class is being serialized. In our example, the Serializable version created a setting of 439 bytes, compared with the Externalizable version of only 103 bytes!

Image Note Classes that implement the Externalizable interface must contain an empty (no-arg) constructor.

8-3. Serializing Java Objects as XML

Problem

Although you love the Serialization framework, you want to create something that is at least cross-language-compatible (or human readable). You would like to save and load your objects using XML.

Solution

In this example, the XMLEncoder object is used to encode the Settings object, which contains program settings information and writes it to the settings.xml file. The XMLDecoder takes the settings.xml file and reads it as a stream, decoding the Settings object. AFileSystem is used to gain access to the machine’s file system; FileOutputStream is used to write a file to the system; and FileInputStream is used to obtain input bytes from a file within the file system. In this example, these three file objects are used to create new XML files, as well as read them for processing.

//Encoding
FileSystem fileSystem = FileSystems.getDefault();
try (FileOutputStream fos = new FileOutputStream("settings.xml"); XMLEncoder encoder =
new XMLEncoder(fos)) {
encoder.setExceptionListener((Exception e) -> {
System.out.println("Exception! :"+e.toString());
});
encoder.writeObject(settings);
}

// Decoding
try (FileInputStream fis = new FileInputStream("settings.xml"); XMLDecoder decoder =
new XMLDecoder(fis)) {
ProgramSettings decodedSettings = (ProgramSettings) decoder.readObject();
System.out.println("Is same? "+settings.equals(decodedSettings));
}

Path file= fileSystem.getPath("settings.xml");
List<String> xmlLines = Files.readAllLines(file, Charset.defaultCharset());
xmlLines.stream().forEach((line) -> {
System.out.println(line);
});

How It Works

XMLEncoder and XMLDecoder, like the Serialization framework, use reflection to determine which fields are to be written, but instead of writing the fields as binary, they are written as XML. Objects that are to be encoded do not need to be serializable, but they do need to follow the Java Beans specification.

Java Bean is the name of any object that conforms to the following contract:

· The object contains a public empty (no-arg) constructor.

· The object contains public getters and setters for each protected/private property that takes the name of get{Property}() and set{Property}().

The XMLEncoder and XMLDecoder will encode/decode only the properties of the Bean that have public accessors (get{property}, set{property}), so any properties that are private and do not have accessors will not be encoded/decoded.

Image Tip It is a good idea to register an Exception Listener when encoding/decoding.

The XmlEncoder creates a new instance of the class that being serialized (remember that they need to be Java Beans, so they must have an empty no-arg constructor), and then figures out which properties are accessible (via get{property}, set{property}). And if a property of the newly instantiated class contains the same value as the property of the original class (i.e., has the same default value), the XmlEncoder doesn’t write that property. In other words, if the default value of a property hasn’t changed, the XmlEncoder will not write it out. This provides the flexibility of changing what a “default” value is between versions. For example, if the default value of a property is 2 when an object is encoded, and later decoded after the default property changed from 2 to 4, the decoded object will contain the new default property of 4 (which might not be correct).

The XMLEncoder also keeps track of references. If an object appears more than once when being persisted in the object graph (for example, an object is inside a Map from the main class, but is also as the DefaultValue property), then the XMLEncoder will only encode it once, and link up a reference by putting a link in the xml. The XMLEncoder/XMLDecoder is much more forgiving than the Serialization framework. When decoding, if a property type is changed, or if it was deleted/added/moved/renamed, the decoding will decode “as much as it can” while skipping the properties that it couldn’t decode.

The recommendation is to not persist your main classes (even though the XMLEncoder is more forgiving), but to create special objects that are simple, hold the basic information, and do not perform many tasks by themselves.

8-4. Creating a Socket Connection and Sending Serializable Objects Across the Wire

Problem

You need to open a network connection, and send/receive objects from it.

Solution

Use Java’s New Input Output API version 2 (NIO.2) to send and receive objects. The following solution utilizes the NIO.2 features of nonblocking sockets (by using Future tasks):

// Server Side
hostAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 2583);

Future<AsynchronousSocketChannel> serverFuture = null;
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(hostAddress);
serverFuture = serverSocketChannel.accept();
final AsynchronousSocketChannel clientSocket = serverFuture.get(2000, TimeUnit.MILLISECONDS);
System.out.println("Connected!");
if ((clientSocket != null) && (clientSocket.isOpen())) {
InputStream connectionInputStream = Channels.newInputStream(clientSocket);
ObjectInputStream ois = null;
ois = new ObjectInputStream(connectionInputStream);
while (true) {
Object object = ois.readObject();
if (object.equals("EOF")) {
connectionCount.decrementAndGet();
clientSocket.close();
break;
}
System.out.println("Received :" + object);
}
ois.close();
connectionInputStream.close();
}

// Client Side
try (AsynchronousSocketChannel clientSocketChannel = AsynchronousSocketChannel.open()) {
Future<Void> connectFuture = clientSocketChannel.connect(hostAddress);
connectFuture.get(); // Wait until connection is done.
OutputStream os = Channels.newOutputStream(clientSocketChannel);
try (ObjectOutputStream oos = new ObjectOutputStream(os)) {
for (int i = 0; i < 5; i++) {
oos.writeObject("Look at me " + i);
Thread.sleep(1000);
}
oos.writeObject("EOF");
}
}

How It Works

At its basic level, sockets require a type, IP address, and port. While sockets literature has consumed whole books, the main idea is pretty straightforward. Like the post office, socket communication relies on addresses. These addresses are used to deliver data. In this example, we picked the loopback (the same computer where the program is running) address (127.0.0.1), and chose a random port number (2583).

The advantage of the new NIO.2 is that it is asynchronous in nature. By using asynchronous calls, you can scale your application without creating thousands of threads for each connection. In our example, we take the asynchronous calls and wait for a connection, effectively making it single-threaded for the sake of the example, but don’t let that stop you for enhancing this example with more asynchronous calls. (Check the recipes on the multithreaded section of this book.)

For a client to connect, it requires a socket channel. The NIO.2 API allows creation of asynchronous socket channels. Once a socket channel is created, it will need an address to connect to. The socketChannel.connect() operation does not block; instead it returns a Futureobject (this is a different from traditional NIO, where calling socketChannel.connect() will block until a connection is established). The Future object allows a Java program to continue what it is doing and simply query the status of the submitted task. To take the analogy further, instead of waiting at the front door for your mail to arrive, you go do other stuff, and “check” periodically to see whether the mail has arrived. Future objects have methods like isDone() and isCancelled() that let you know if the task is done or cancelled. It also has the get()method, which allows you to actually wait for the task to finish. In our example, we use the Future.get() to wait for the client connection to be established.

Once the connection is established, we use Channels.newOutputStream() to create an output stream to send information. Using the decorator pattern, we decorate the outputStream with our ObjectOutputStream to finally send objects through the socket.

The server code is a little more elaborate. Server socket connections allow more than one connection to occur, thus they are used to monitor or receive connections instead of initiating a connection. For this reason, the server is usually waiting for a connection asynchronously.

The server begins by establishing the address it listens to (127.0.0.1:2583) and accepting connections. The call to serverSocketChannel.accept() returns another Future object that will give you the flexibility of how to deal with incoming connections. In our example, the server connection simply calls future.get(), which will block (stop the execution of the program) until a connection is accepted.

After the server acquires a socket channel, it creates an inputStream by calling Channels.newInputStream(socket) and then wrapping that input stream with an ObjectInputStream. The server then proceeds to loop and read each object coming from theObjectInputStream. If the object received’s toString() method equals EOF, the server stops looping and the connection is closed.

Image Note Using an ObjectOutputStream and ObjectInputStream to send and receive a lot of objects can lead to memory leaks. ObjectOutputStream keeps a copy of the sent object for efficiency. If you were to send the same object again, ObjectOutputStream andObjectInputStream will not send the same object again, but instead send a previously sent Object ID. This behavior or just sending the Object ID instead of the whole object raises two issues.

The first issue is that objects that are changed in-place (mutable) will not get the change reflected in the receiving client when sent through the wire. The reason is that because the object was sent once, the ObjectOutputStream believes that the object is already transmitted and will only send the ID, negating any changes to the object that have happened since it was sent. To avoid this, don’t make changes to objects that were sent down the wire. This rule also applies to sub-objects from the object graph.

The second issue is that because ObjectOutputStream maintains a list of sent objects and their Object IDs, if you send a lot of objects the dictionary of sent objects to keys grows indefinitely, causing memory starvation on a long-running program. To alleviate this issue, you can callObjectOutputStream.reset(), which will clear the dictionary of sent objects. Alternatively, you can invoke ObjectOutputStream.writeUnshared() to not cache the object in the ObjectOutputStream dictionary.

8-5. Obtaining the Java Execution Path

Problem

You want to get the path where the Java program is running.

Solution

Invoke the System class’s getProperty method. For example:

String path = System.getProperty("user.dir");

How It Works

When a Java program starts, the JRE updates the user.dir system property to record where the JRE was invoked. The solution example passes the property name "user.dir" to the getProperty method, which returns the value.

8-6. Copying a File

Problem

You need to copy a file from one folder to another.

Solution

From the default FileSystem, you create the “to” and “from” paths where the files/folders exist and then use the Files.copy static method to copy files between the created paths:

FileSystem fileSystem = FileSystems.getDefault();
Path sourcePath = fileSystem.getPath("file.log");
Path targetPath = fileSystem.getPath("file2.log");
System.out.println("Copy from "+sourcePath.toAbsolutePath().toString()+
" to "+targetPath.toAbsolutePath().toString());
try {
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}

How It Works

In the new NIO.2 libraries, Java works with an abstraction level that allows for more direct manipulation of file attributes belonging to the underlying operating system.

FileSystem.getDefaults() gets the usable abstract system that you can do file operations on. For example, running this example in Windows will get you a WindowsFileSystem; if you were running this example in Linux, a LinuxFileSystem object would be returned.AllFileSystems supports basic operations; in addition, each concrete FileSystem provides access to the unique features offered for that operating system.

After getting the default FileSystem object, you can query for file objects. In the NIO.2 file, folders and links are all called paths. Once you get a path, you can perform operations with it. In this example, Files.copy is called with the source and destination paths. The last parameter refers to the different copy options. The different copy options are file-system dependent so make sure that the one that you choose is compatible with the operating system you intend to run the application in.

8-7. Moving a File

Problem

You need to move a file from one file system location to another.

Solution

As in Recipe 8-6, you use the default FileSystem to create the “to” and “from” paths, and invoke the Files.move() static method:

FileSystem fileSystem = FileSystems.getDefault();
Path sourcePath = fileSystem.getPath("file.log");
Path targetPath = fileSystem.getPath("file2.log");
System.out.println("Copy from "+sourcePath.toAbsolutePath().toString()+Image
" to "+targetPath.toAbsolutePath().toString());
try {
Files.move(sourcePath, targetPath);
} catch (IOException e) {
e.printStackTrace();
}

How It Works

In the same manner as copying a file, create the path of source and destination. After having the source and destination paths, Files.move will take care of moving the file from one location to another for you. Other methods provided by the Files object are the following:

· Delete (path): Deletes a file (or a folder, if it’s empty).

· Exists (path): Checks whether a file/folder exists.

· isDirectory (path): Checks whether the path created points to a directory.

· isExecutable (path): Checks whether the file is an executable.

· isHidden (path): Checks whether the file is visible or hidden in the operating system.

8-8. Creating a Directory

Problem

You need to create a directory from your Java application.

Solution 1

By using the default FileSystem, you instantiate a path pointing to the new directory; then invoke the Files.createDirectory() static method, which creates the directory specified in the path.

FileSystem fileSystem = FileSystems.getDefault();
Path directory= fileSystem.getPath("./newDirectory");
try {
Files.createDirectory(directory);
} catch (IOException e) {
e.printStackTrace();
}

Solution 2

If using a *nix operating system, you can specify the folder attributes by invoking the PosixFilePermission() method, which lets you set access at the owner, group, and world levels. For example:

FileSystem fileSystem = FileSystems.getDefault();
Path directory= fileSystem.getPath("./newDirectoryWPermissions");
try {
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
Files.createDirectory(directory, attr);

} catch (IOException e) {
e.printStackTrace();
}

How It Works

The Files.createDirectory() method takes a path as a parameter and then creates the directory. By default, the directory created will inherit the default permissions. If you wanted to specify specific permissions in Linux, you can use the PosixAttributes as an extra parameter in the createDirectory() method.

8-9. Iterating Over Files in a Directory

Problem

You need to scan files from a directory. There are possibly subdirectories with more files. You want to include those in your scan.

Solution

Using the NIO.2, create a FileVisitor object and perform a desired implementation within its visitFile method. Next, obtain the default FileSystem object and grab a reference to the Path that you’d like to scan via the getPath() method. Lastly, invoke theFiles.walkFileTree() method, passing the Path and the FileVisitor that you created. The following code demonstrates how to perform these tasks.

FileVisitor<Path> myFileVisitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
System.out.println("Visited File: "+file.toString());
return FileVisitResult.CONTINUE;
}
};

FileSystem fileSystem = FileSystems.getDefault();
Path directory= fileSystem.getPath(".");
try {
Files.walkFileTree(directory, myFileVisitor);
} catch (IOException e) {
e.printStackTrace();
}

How It Works

Before NIO.2, trying to traverse a directory tree involved recursion, and depending on the implementation, it could be very brittle. The calls to get files within a folder were synchronous and required the scanning of the whole directory before returning; generating what would appear to be an unresponsive method call to an application user. With the new NIO.2, one can specify which folder to start traversing on, and the NIO.2 calls will handle the recursion details. The only item that you provide to the NIO.2 API is a class that tells it what to do when a file/folder is found (SimpleFileVisitor implementation). NIO.2 uses a Visitor pattern, so it isn’t required to prescan the entire folder, but instead processes files as they are being iterated over.

The implementation of the SimpleFileVisitor class as an anonymous inner class includes overriding the visitFile(Path file, BasicFileAttributesattrs() method. When you override this method, you can specify the tasks to perform when a file is encountered.

The visitFile method returns a FileVisitReturn enum. This enum then tells the FileVisitor which action to take:

· CONTINUE: Continues with the traversing of the directory tree.

· TERMINATE: Stops the traversing.

· SKIP_SUBTREE: Stops going deeper from the current tree level (useful only if this enum is returned on the preVisitDirectory() method).

· SKIP_SIBLINGS: Skips the other directories at the same tree level as the current.

The SimpleFileVisitor class, aside from the visitFile() method, also contains the following:

· preVisitDirectory: Called before entering a directory to be traversed.

· postVisitDirectory: Called after finished traversing a directory.

· visitFile: Called as it visits the file, as in the example code.

· visitFileFailed: Called if the file cannot be visited; for example, on an I/O error.

8-10. Querying (and Setting) File Metadata

Problem

You need to get information about a particular file, such as file size, whether it is a directory, and so on. Also, you might want to mark a file as archived in the Windows operating system or grant specific POSIX file permissions in the *nix operating system (refer to Recipe 8-8).

Solution

Using Java NIO.2 you can obtain any file information by simply invoking methods on the java.nio.file.Files utility class, passing the path for which you’d like to obtain the metadata. You can obtain attribute information by calling the Files.getFileAttributeView()method, passing the specific implementation for the attribute view that you would like to use. The following code demonstrates these techniques for obtaining metadata.

Path path = FileSystems.getDefault().getPath("./file2.log");
try {
// General file attributes, supported by all Java systems
System.out.println("File Size:"+Files.size(path));
System.out.println("Is Directory:"+Files.isDirectory(path));
System.out.println("Is Regular File:"+Files.isRegularFile(path));
System.out.println("Is Symbolic Link:"+Files.isSymbolicLink(path));
System.out.println("Is Hidden:"+Files.isHidden(path));
System.out.println("Last Modified Time:"+Files.getLastModifiedTime(path));
System.out.println("Owner:"+Files.getOwner(path));

// Specific attribute views.
DosFileAttributeView view = Files.getFileAttributeView(path,
DosFileAttributeView.class);
System.out.println("DOS File Attributes\n”);
System.out.println(“------------------------------------\n");
System.out.println("Archive :"+view.readAttributes().isArchive());
System.out.println("Hidden :"+view.readAttributes().isHidden());
System.out.println("Read-only:"+view.readAttributes().isReadOnly());
System.out.println("System :"+view.readAttributes().isSystem());

view.setArchive(false);

} catch (IOException e) {
e.printStackTrace();
}

How It Works

Java NIO.2 allows much more flexibility in getting and setting file attributes than older I/O techniques. NIO.2 abstracts the different operating system attributes into both a “Common” set of attributes and an “OS Specific” set of attributes. The standard attributes are the following:

· isDirectory: True if it’s a directory.

· isRegularFile: Returns false if the file isn’t considered a regular file, the file doesn’t exist, or it can’t be determined whether it’s a regular file.

· isSymbolicLink: True if the link is symbolic (most prevalent in Unix systems).

· isHidden: True if the file is considered to be hidden in the operating system.

· LastModifiedTime: The time the file was last updated.

· Owner: The file’s owner per the operating system.

Also, NIO.2 allows entering the specific attributes of the underlying operating system. To do so, you first need to get a view that represents the operating system’s file attributes (in this example, it is a DosFileAttributeView). Once you get the view, you can query and change the OS-specific attributes.

Image Note The AttributeView will only work for the operating system that is intended (you cannot use the DosFileAttributeView in a *nix machine).

8-11. Monitoring a Directory for Changes

Problem

You need to keep track when a directory’s content has changed (for example, a file was added, changed, or deleted) and act upon those changes.

Solution

By using a WatchService, you can subscribe to be notified about events occurring within a folder. In the following example, we subscribe for ENTRY_CREATE, ENTRY_MODIFY, and ENTRY_DELETE events:

try {
System.out.println("Watch Event, press q<Enter> to exit");
FileSystem fileSystem = FileSystems.getDefault();
WatchService service = fileSystem.newWatchService();
Path path = fileSystem.getPath(".");
System.out.println("Watching :"+path.toAbsolutePath());
path.register(service, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
boolean shouldContinue = true;
while(shouldContinue) {
WatchKey key = service.poll(250, TimeUnit.MILLISECONDS);

// Code to stop the program
while (System.in.available() > 0) {
int readChar = System.in.read();
if ((readChar == 'q') || (readChar == 'Q')) {
shouldContinue = false;
break;
}
}
if (key == null) continue;
key.pollEvents().stream()
.filter((event) -> !(event.kind() == StandardWatchEventKinds.OVERFLOW))
.map((event) -> (WatchEvent<Path>)event).forEach((ev) -> {
Path filename = ev.context();
System.out.println("Event detected :"+filename.toString()+" "+ev.kind());
});
boolean valid = key.reset();
if (!valid) {
break;
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}

How It Works

NIO.2 includes a built-in polling mechanism to monitor for changes in the FileSystem. Using a poll mechanism allows you to wait for events and poll for updates at a specified interval. Once an event occurs, you can process and consume it. A consumed event tells the NIO.2 framework that you are ready to handle a new event.

To start monitoring a folder, create a WatchService that you can use to poll for changes. After the WatchService has been created, register the WatchService with a path. A path symbolizes a folder in the file system. When the WatchService is registered with the path, you define the kinds of events you want to monitor (see Table 8-2).

Table 8-2. Types of watchEvents

WatchEvent

Description

OVERFLOW

An event that has overflown (ignore)

ENTRY_CREATE

A directory or file was created

ENTRY_DELETE

A directory or file has been deleted

ENTRY_MODIFY

A directory or file has been modified

After registering the WatchService with the path, you can then “poll” the WatchService for event occurrences. By calling the watchService.poll() method, you will wait for a file/folder event to occur on that path. Using the watchService.poll(int timeout, Timeunit timeUnit) will wait until the specified timeout is reached before continuing. If the watchService receives an event, or if the allowed time has passed, then it will continue execution. If there were no events and the timeout was reached, the WatchKey object returned by the watchService.poll(int timeout) will be null; otherwise, the WatchKey object returned will contain the relevant information for the event that has occurred.

Because many events can occur at the same time (say, for example, moving an entire folder or pasting a bunch of files into a folder), the WatchKey might contain more than one event. You can use the watchKey to obtain all the events that are associated with that key by calling thewatchKey.pollEvents() method.

The watchKey.pollEvents() call will return a list of watchEvents that can be iterated over. Each watchEvent contains information on the actual file or folder to which the event refers (for example, an entire subfolder could have been moved or deleted), and the event type (add, edit, delete). Only those events that were registered on the WatchService will be processed. The event types you can register are listed in Table 8-2.

Once an event has been processed, it is important to call the EventKey.reset(). The reset will return a Boolean value determining whether the WatchKey is still valid. A WatchKey becomes invalid if it is cancelled or if its originating WatchService is closed. If the eventKeyreturns false, you should break from the watch loop.

8-12. Reading Property Files

Problem

You want to establish some configurational settings for your application, and you want to have the ability to modify the settings manually or programmatically.

Solution

Create a properties file to store the application configurations. Using the Properties object, load properties stored within the properties file for application processing. Properties can also be updated and modified within the properties file. The following example demonstrates how to read a properties file named properties.conf, load the values for application use, and finally set a property and write it to the file.

File file = new File("properties.conf");
try {
if (!file.exists()) file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}

Properties properties = new Properties();
try {
properties.load(new FileInputStream("properties.conf"));
} catch (IOException e) {
e.printStackTrace();
}

boolean shouldWakeUp = false;
int startCounter = 100;

String shouldWakeUpProperty = properties.getProperty("ShouldWakeup");
shouldWakeUp = (shouldWakeUpProperty == null) ? false Image Boolean.parseBoolean(shouldWakeUpProperty.trim().toLowerCase());

String startCounterProperty = properties.getProperty("StartCounter");
try {
startCounter = Integer.parseInt(startCounterProperty);
} catch (Exception e) {
System.out.println("Couldn't read startCounter, defaulting to "+startCounter);
}
String dateFormatStringProperty = properties.getProperty("DateFormatString",
"MMM dd yy");

System.out.println("Should Wake up? "+shouldWakeUp);
System.out.println("Start Counter: "+startCounter);
System.out.println("Date Format String:"+dateFormatStringProperty);

//setting property
properties.setProperty("StartCounter","250");
try {
properties.store(new FileOutputStream("properties.conf"),"Properties Description");
} catch (IOException e) {
e.printStackTrace();
}
properties.list(System.out);

How It Works

The Java Properties class helps you manage program properties. It allows you to manage the properties either via external modification (someone editing a property file) or internally by using the Properties.store() method.

The Properties object can be instantiated either without a file or with a preloaded file. The files that the Properties object read are in the form of [name]=[value] and are textually represented. If you need to store values in other formats, you need to write to and read from a string.

If you are expecting the files to be modified outside the program (the user directly opens a text editor and changes the values), be sure to sanitize the inputs; like trimming the values for extra spaces and ignoring case if need be.

To query the different properties programmatically, you call the getProperty(String) method, passing the string-based name of the property whose value you want to retrieve. The method will return null if the property is not found. Alternatively, you can invoke thegetProperty (String,String) method, on which if the property is not found in the Properties object, it will return the second parameter as its value. It is a good practice to specify default values in case the file doesn’t have an entry for a particular key.

Upon looking at a generated property file, you will notice that the first two lines indicate the description of the file and the date when it was modified. These two lines start with #, which in Java property files is the equivalent of a comment. The Properties object will skip any line starting with # when processing the file.

Image Note If you allow users to modify your configuration files directly, it is important to have validation in place when retrieving properties from the Properties object. One of the most common issues encountered in the value of properties is leading and/or trailing spaces. If specifying a Boolean or integer property, be sure that they can be parsed from a string. At a minimum, catch an exception when trying to parse to survive an unconventional value (and log the offending value).

8-13. Uncompressing Compressed Files

Problem

Your application has the requirement to decompress and extract files from a compressed .zip file.

Solution

Using the Java.util.zip package, you can open a .zip file and iterate through its entries. While traversing the entries, directories can be created for directory entries. Similarly, when a file entry is encountered, write the decompressed file to the file .unzipped. The following lines of code demonstrate how to perform the decompress and file iteration technique, as described.

ZipFile file = null;
try {
file = new ZipFile("file.zip");
FileSystem fileSystem = FileSystems.getDefault();
Enumeration<? extends ZipEntry> entries = file.entries();
String uncompressedDirectory = "uncompressed/";
Files.createDirectory(fileSystem.getPath(uncompressedDirectory));
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
System.out.println("Creating Directory:" + uncompressedDirectory + entry.getName());
Files.createDirectories(fileSystem.getPath(uncompressedDirectory +
entry.getName()));
} else {
InputStream is = file.getInputStream(entry);
System.out.println("File :" + entry.getName());
BufferedInputStream bis = new BufferedInputStream(is);

String uncompressedFileName = uncompressedDirectory + entry.getName();
Path uncompressedFilePath = fileSystem.getPath(uncompressedFileName);
Files.createFile(uncompressedFilePath);
try (FileOutputStream fileOutput = new FileOutputStream(uncompressedFileName)) {
while (bis.available() > 0) {
fileOutput.write(bis.read());
}
}
System.out.println("Written :" + entry.getName());
}
}
} catch (IOException e) {
e.printStackTrace();
}

How It Works

To work with the contents of a .Zip archive, create a ZipFile object. A ZipFile object can be instantiated, passing the name of a .zip archive to the constructor. After creating the object, you gain access to the specified.zip file information. Each ZipFile object will contain a collection of entries that represent the directories and files contained within the archive, and by iterating through the entries you can obtain information on each of the compressed files. Each ZipEntry instance will have the compressed and uncompressed size, the name, and the input stream of the uncompressed bytes.

The uncompressed bytes can be read into a byte buffer by generating an InputStream, and later (in our case) written to a file. Using the FileStream, it is possible to determine how many bytes can be read without blocking the process. Once the determined number bytes has been read, then those bytes are written to the output file. This process continues until the total number of bytes has been read.

Image Note Reading the entire file into memory may not be a good idea if the file is extremely large. If you need to work with a large file, it’s best to first write it in an uncompressed format to disk (as in the example) and then open it and load it in chunks. If the file that you are working on is not large (you can limit the size by checking the getSize() method), you can probably load it in memory.

Summary

This chapter demonstrated several examples for working with file and network I/O in Java 8. You learned how to serialize files so that they could be stored to disk, and also how to manipulate a host’s file system with the Java APIs. The chapter also covered how to read and write property files, and perform file compression.