Thursday, February 3, 2011

How to read file without locking in Java

In java, we can read or write to a file without acquiring locks to it. These are mostly used for log files. Recently I was tying to mimic the tail command (of Linux) in java and wrote a simple program below.

Using java.io.RandomAccesFile we can read/write to a file without obtaining locks. Let me demonstrate with example step by step how it works.

RandomAccessFile accessFile = new RandomAccessFile(file, "r");

Create an instance of the RandomAccessFile by either passing java.io.File or a path of a file. The second constructor argument is the type of access needed. In this case it is read only "r". If you want to make it read/write pass "rw". Like Buffers of java.nio package, this class also works with the pointers. it reads the bytes and move the pointer as it progresses. The current pointer gives the information about how much of the file have bean read or has to be read.

public void print(OutputStream stream) throws IOException {
    long length = accessFile.length();
    while (accessFile.getFilePointer() < length) {
        stream.write(accessFile.read());
    }
    stream.flush();
}


In above snippet, file is read and the bytes are passed to an OutputStream. This could be any thing, pass System.out if you want it to be print to your console. accessFile.length() gives the length in bytes of complete file. accessFile.getFilePointer() returns the current pointer from where the reading will continue. So, as long as the file pointer reaches the end (length) we keep reading each bytes using accessFile.read(). If the file is changed after the print method is called, calling this again will only print the bytes added after the last call. For example, if file contains 100bytes and all the bytes are read the pointer will be at the last. When new lines are added to log file, length/size will increase and the pointer still pints to the last read location, hence reading from thereon fetches you the added information for the log file.

Below example class LogViewer.java also contains static methods to use the example over a thread. It keeps reading the file every configured seconds (3 seconds in this case) and whenever the file is changed. This Thread code should be in another class and could be improved depending on your usage.

I have also added Two more classes named View.java and TextAreaOutputStream.java

TextAreaOutputStream class is and extension to Swing JTextArea which behaves like an output stream. In other words an OutPutStream is attached to this JTextArea, whenever something is written to a stream, content in the JTextArea changes automatically. This is a handy class to be used as a front end View component backed with the stream, A data model class like LogViewer and TextAreaOutputStream are unknown to each other. View.java is the main controller class which binds both of them and opens a Swing application for the demo. I am not explaining those two class in detail now. Do share your comments or feedbacks.




LogViewer.java


package org.mbm.io.thread;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;


public class LogReader {
    private final File file;
    private RandomAccessFile accessFile;
    long lastModified, lastPrinted;

    public LogReader(File file) {
        this.file = file;
    }

    public void init() throws IOException {
        accessFile = new RandomAccessFile(file, "r");
        lastModified = file.lastModified();
    }

    public void destroy() throws IOException {
        accessFile.close();
    }

    public void print(OutputStream stream) throws IOException {
        lastPrinted = System.currentTimeMillis();
        long length = accessFile.length();
        while (accessFile.getFilePointer() < length) {
            stream.write(accessFile.read());
        }
        stream.flush();
    }

    public boolean changed() {
        System.out.println("Checkiing");
        lastModified = file.lastModified();
        return lastModified > lastPrinted;
    }

    // ------------ BELOW CODE SHOULD BELONG TO DIFERRENT CLASS ------------

    private static boolean closed = false;

    public static void markClose() {
        closed = true;
    }

    public static void execute(final String filePath, final OutputStream output) throws IOException {

        final LogReader data = new LogReader(new File(filePath));
        data.init();
        Thread thrd = new Thread() {

            @Override
            public void run() {
                try {
                    // pooling time set for 3 seconds.
                    final int pooling = 1000 * 3;
                    long lastChecked = System.currentTimeMillis();
                    while (!closed) {
                        long now = System.currentTimeMillis();
                        if (lastChecked + pooling > now)
                            continue;
                        lastChecked = now;
                        if (data.changed()) {
                            // data/file modified read and print.
                            data.print(output);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        data.destroy();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread close = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("Closing...");
                    data.destroy();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        };
        // If JVM terminates abruptly, close/release the resources by adding a shut down hook
        Runtime.getRuntime().addShutdownHook(close);
        thrd.start();
    }
}

TextAreaOutputStream.java

package org.mbm.io.thread;

import java.io.IOException;
import java.io.OutputStream;

import javax.swing.JTextArea;

public class TextAreaOutputStream extends OutputStream {

    private final JTextArea textArea;
    private StringBuilder buffer;

    public TextAreaOutputStream(JTextArea textArea) {
        this.textArea = textArea;
    }

    public void close() {
        buffer.setLength(0);
    }

    /**
     * Flush the data that is currently in the buffer.
     * 
     * @throws IOException
     */

    public void flush() throws IOException {
        textArea.append(getBuffer().toString());
        if (System.getProperty("java.version").startsWith("1.1")) {
            textArea.append("\n");
        }
        textArea.setCaretPosition(textArea.getDocument().getLength());
        buffer.setLength(0);
        buffer = null;
    }

    protected StringBuilder getBuffer() {
        if (buffer == null) {
            buffer = new StringBuilder();
        }
        return buffer;
    }

    @Override
    public void write(int b) throws IOException {
        getBuffer().append((char)b);
    }
}

View.java

package org.mbm.io.thread;

import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.filechooser.FileFilter;


public class View extends JFrame {
    JPanel container;
    String lastVisited = lastVisited();

    public View() {
        setTitle("Viewer");
        setSize(600, 500);
        setLocation(200, 100);
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }

        container = new JPanel();
        container.setLayout(new BorderLayout());
        // container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
        container.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));

        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                LogReader.markClose();
                System.exit(0);
            }
        });
    }

    private void init() {
        JPanel pnlTop = new JPanel();
        pnlTop.setLayout(new BoxLayout(pnlTop, BoxLayout.X_AXIS));
        pnlTop.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));

        // simple text field.
        final JTextField path = new JTextField(2);
        path.setText(lastVisited);
        pnlTop.add(path);

        final JFileChooser fileChooser = new JFileChooser();
        if (lastVisited != null)
            fileChooser.setCurrentDirectory(new File(lastVisited));
        fileChooser.setFileFilter(new FileFilter() {

            @Override
            public String getDescription() {
                return "Log files";
            }

            @Override
            public boolean accept(File f) {
                if (f.isDirectory())
                    return true;
                String name = f.getName().toLowerCase();
                if (name.endsWith(".log") || name.endsWith(".txt"))
                    return true;

                return false;
            }
        });
        JButton browse = new JButton("Browse");

        pnlTop.add(browse);
        browse.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                int result = fileChooser.showOpenDialog(container);
                if (JFileChooser.APPROVE_OPTION == result) {
                    File selFile = fileChooser.getSelectedFile();
                    if (selFile != null) {
                        String p = selFile.getAbsolutePath();
                        path.setText(p);
                        saveVisited(p);
                    }

                }

            }
        });
        pnlTop.add(Box.createRigidArea(new Dimension(5, 0)));

        // a button.
        JButton load = new JButton("Load");
        pnlTop.add(load);

        container.add(pnlTop, BorderLayout.NORTH);

        // text area
        final JTextArea textArea = new JTextArea();
        textArea.setDisabledTextColor(Color.GRAY);
        textArea.setEditable(false);

        // scroll pane attached to text area
        JScrollPane scrollPane = new JScrollPane(textArea, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_AS_NEEDED);
        container.add(scrollPane, BorderLayout.CENTER);

        final TextAreaOutputStream textStream = new TextAreaOutputStream(textArea);

        JPanel bottom = new JPanel();
        bottom.setLayout(new BorderLayout());

        // a button.
        JButton clear = new JButton("Clear");
        clear.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                textArea.setText("");
                textStream.close();
            }
        });
        bottom.add(clear, BorderLayout.EAST);

        container.add(bottom, BorderLayout.SOUTH);
        add(container);

        // model processing.

        load.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(e);
                if (path.getText() != null && path.getText().length() > 0) {
                    try {
                        LogReader.execute(path.getText(), textStream);
                    } catch (IOException e1) {
                        e1.printStackTrace();
                        JOptionPane.showMessageDialog(getParent(), e1.getMessage(), "ERROR", JOptionPane.ERROR_MESSAGE);
                    }
                }
            }
        });
    }

    private String lastVisited() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("history.dat"));
            return (String) ois.readObject();
        } catch (Exception e) {
            return null;
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                }
            }
        }
    }

    private void saveVisited(String path) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("history.dat"));
            oos.writeObject(path);
        } catch (Exception e) {
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                }
            }
        }
    }

    public static void main(String a[]) {
        View v = new View();
        v.init();
        v.setVisible(true);
    }
}


2 comments:

  1. It still locks the file. I used this to tail the catalina logs that Apache Tomcat produces. One lock is acquired by Tomcat and the other is the Application that you wrote.

    I'm using a Java 7 JRE. Maybe this is the issue? Any ideas? I'm still investigating the issue of reading from a file without acquiring locks. :)

    ReplyDelete
  2. edit - I am using Windows. Maybe this works on UNIX but not Windows. From my experience Windows is more aggressive with locks.

    ReplyDelete

Was this article useful?