Sameen
Sameen

Reputation: 734

How do I determine progress when reading a file

I'm using JavaFX Task to read a text file and parse it into a HashMap<String, String> (using aMapper<String, String object). My code is fully functioning, but I wish to show read progress to user as the input file contains over 9000 rows of data.

@Override
protected Mapper<String, String> call() throws Exception {
    // Read and parse contents of source mapping file.

    /* Format:
     * Ignore first line of file, then:
     * 
     * <old_name>       <new_name>
     *  NAME_A              NAME_X              
     *  NAME_B              NAME_Y               
     *  NAME_C              NAME_Z
     *
     * Where <old_name> = K, <new_name> = V in Mapper object.
     */

    int i=beginReadingFileFromLine; // Ignore first line of file
    String line;
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    updateMessage("Loading data...");
    while ( (line=FileUtils.readLine(sourceMappingFilePath, i)) != null ) {
        // Parse line into String[] split by delim char.
        String[] parsedLine = line.split(fileDelimChar);
        keys.add(parsedLine[0]);
        values.add(parsedLine[1]);
        i++;
    }
    updateProgress(i, i);
    updateMessage("Data loaded!");
    return new Mapper<String, String>(keys, values);
}

In the existing state of this code, I can't think of a way to determine how much of the input file I've read. The method FileUtils.readLine(sourceMappingFilePath, i) above is a custom implementation:

 /**
 * Reads a single line, according to supplied line number from specified file.
 * @param filepath
 * @param lineNumber zero based index.
 * @return  Line from file without linefeed character. NULL if at EoF.
 * @throws IOException
 */
public static String readLine(String filepath, int lineNumber) throws IOException {
    FileReader fReader = new FileReader(filepath);
    LineNumberReader lineNumberReader = new LineNumberReader(fReader);

    String desiredLine = null; 

    int i=0;
    String line;
    while( (line=lineNumberReader.readLine()) != null) {

        if(lineNumber==i) {
            desiredLine=line;
        }
        i++;
    }
    lineNumberReader.close();
    return desiredLine;
}

Any suggestions? The simpler to implement, the better - thanks for your time.

Upvotes: 0

Views: 1415

Answers (1)

James_D
James_D

Reputation: 209674

A simple approach is to use an underlying input stream that tracks how many bytes you have read. The Files.size() method will give you the total number of bytes in a file, so this gives you enough information to compute the overall progress. You can do something like

public class CountingInputStream extends InputStream implements AutoCloseable {

    private long bytesRead = 0 ;

    private final InputStream stream ;

    public CountingInputStream(InputStream stream) {
        this.stream = stream ;
    }

    @Override
    public int read() throws IOException {
        int result = stream.read() ;
        if (result != -1) {
            bytesRead++;
        }
        return result ;
    }

    @Override
    public void close() throws IOException {
        super.close();
        stream.close();
    }

    public long getBytesRead() {
        return bytesRead ;
    }
}

Note that if you wrap this in a BufferedReader, getBytesRead() will return the number of bytes read from the underlying stream, including those still stored in the buffer. This is probably good enough for displaying a progress bar (since it's very fast to read from the buffer), but isn't going to be technically 100% accurate.

Here's a SSCCE. On my system, you need to load a file with ~100,000 lines to see the progress bar. You can create one if you don't have one available (the SSCCE allows you to create a file first).

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Collectors;

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class ReadFileWithProgress extends Application {


    public static class CountingInputStream extends InputStream implements AutoCloseable {

        private long bytesRead = 0 ;

        private final InputStream stream ;

        public CountingInputStream(InputStream stream) {
            this.stream = stream ;
        }

        @Override
        public int read() throws IOException {
            int result = stream.read() ;
            if (result != -1) {
                bytesRead++;
            }
            return result ;
        }

        @Override
        public void close() throws IOException {
            stream.close();
        }

        public long getBytesRead() {
            return bytesRead ;
        }
    }

    @Override
    public void start(Stage primaryStage) {
        GridPane root = new GridPane();
        TextField numLinesField = new TextField();

        FileChooser chooser = new FileChooser();

        Button create = new Button("Create File...");
        create.setOnAction(e -> {
            int numLines = Integer.parseInt(numLinesField.getText());
            File file = chooser.showSaveDialog(primaryStage);
            if (file != null) {
                try {
                    createFile(file.toPath(), numLines);
                } catch (Exception exc) {
                    exc.printStackTrace();
                }
            }
        });

        Button loadFile = new Button("Load file");
        ProgressBar progress = new ProgressBar(0);
        loadFile.setOnAction(e -> {
            File file = chooser.showOpenDialog(primaryStage);
            if (file != null) {
                Task<Map<String, String>> task = readFileTask(file.toPath());
                progress.progressProperty().bind(task.progressProperty());
                task.setOnSucceeded(evt -> new Alert(AlertType.INFORMATION, "File loaded", ButtonType.OK).showAndWait());
                task.setOnFailed(evt -> new Alert(AlertType.ERROR, "File could not be loaded", ButtonType.OK).showAndWait());
                new Thread(task).start();
            }
        });

        root.addRow(0, new Label("Number of lines:"), numLinesField, create);
        root.add(loadFile, 0, 1, 3, 1);
        root.add(progress, 0, 2, 3, 1);

        GridPane.setFillWidth(progress, true);
        GridPane.setHalignment(progress, HPos.CENTER);
        GridPane.setFillWidth(loadFile, true);
        GridPane.setHalignment(loadFile, HPos.CENTER);

        root.setPadding(new Insets(20));
        root.setHgap(5);
        root.setVgap(10);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    private Task<Map<String, String>> readFileTask(Path path) {

        return new Task<Map<String, String>>() {

            @Override
            protected Map<String, String> call() throws IOException {
                try (
                        CountingInputStream input = new CountingInputStream(Files.newInputStream(path));
                        BufferedReader in = new BufferedReader(new InputStreamReader(input));
                    ) {

                        long totalBytes = Files.size(path);

                        return in.lines()
                                .peek(line -> updateProgress(input.getBytesRead(), totalBytes))
                                .map(line -> line.split("\t"))
                                .collect(Collectors.toMap(tokens -> tokens[0], tokens -> tokens[1]));
                    }

            }

        };

    }

    private void createFile(Path path, int numEntries) throws IOException {
        try (BufferedWriter out = Files.newBufferedWriter(path)) {
            for (int i = 1; i <= numEntries ; i++) {
                out.write(String.format("key %d\tvalue %d%n", i, i));
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 2

Related Questions