Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catch UncheckedIOException in Java 8 stream

EDIT: This does not seem to be possible, see https://bugs.openjdk.java.net/browse/JDK-8039910.

I have a helper class that provides a Stream<Path>. This code just wraps Files.walk and sorts the output:

public Stream<Path> getPaths(Path path) {
    return Files.walk(path, FOLLOW_LINKS).sorted();
}

As symlinks are followed, in case of loops in the filesystem (e.g. a symlink x -> .) the code used in Files.walk throws an UncheckedIOException wrapping an instance of FileSystemLoopException.

In my code I would like to catch such exceptions and, for example, just log a helpful message. The resulting stream could/should just stop providing entries as soon as this happens.

I tried adding .map(this::catchException) and .peek(this::catchException) to my code, but the exception is not caught in this stage.

Path checkException(Path path) {
    try {
        logger.info("path.toString() {}", path.toString());
        return path;
    } catch (UncheckedIOException exception) {
        logger.error("YEAH");
        return null;
    }
}

How, if at all, can I catch an UncheckedIOException in my code giving out a Stream<Path>, so that consumers of the path do not encounter this exception?

As an example, the following code should never encounter the exception:

List<Path> paths = getPaths().collect(toList());

Right now, the exception is triggered by code invoking collect (and I could catch the exception there):

java.io.UncheckedIOException: java.nio.file.FileSystemLoopException: /tmp/junit5844257414812733938/selfloop

    at java.nio.file.FileTreeIterator.fetchNextIfNeeded(FileTreeIterator.java:88)
    at java.nio.file.FileTreeIterator.hasNext(FileTreeIterator.java:104)
    at java.util.Iterator.forEachRemaining(Iterator.java:115)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at ...

EDIT: I provided a simple JUnit test class. In this question I ask you to fix the test by just modifying the code in provideStream.

package somewhere;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.nio.file.FileVisitOption.FOLLOW_LINKS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.fail;

public class StreamTest {
    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Test
    public void test() throws Exception {
        Path rootPath = Paths.get(temporaryFolder.getRoot().getPath());
        createSelfloop();

        Stream<Path> stream = provideStream(rootPath);

        assertThat(stream.collect(Collectors.toList()), is(not(nullValue())));
    }

    private Stream<Path> provideStream(Path rootPath) throws IOException {
        return Files.walk(rootPath, FOLLOW_LINKS).sorted();
    }

    private void createSelfloop() throws IOException {
        String root = temporaryFolder.getRoot().getPath();
        try {
            Path symlink = Paths.get(root, "selfloop");
            Path target = Paths.get(root);
            Files.createSymbolicLink(symlink, target);
        } catch (UnsupportedOperationException x) {
            // Some file systems do not support symbolic links
            fail();
        }
    }
}
like image 723
C-Otto Avatar asked Oct 29 '22 20:10

C-Otto


1 Answers

You can make your own walking stream factory:

public class FileTree {
    public static Stream<Path> walk(Path p) {
        Stream<Path> s=Stream.of(p);
        if(Files.isDirectory(p)) try {
            DirectoryStream<Path> ds = Files.newDirectoryStream(p);
            s=Stream.concat(s, StreamSupport.stream(ds.spliterator(), false)
                .flatMap(FileTree::walk)
                .onClose(()->{ try { ds.close(); } catch(IOException ex) {} }));
        } catch(IOException ex) {}
        return s;
    }
    // in case you don’t want to ignore exceprions silently
    public static Stream<Path> walk(Path p, BiConsumer<Path,IOException> handler) {
        Stream<Path> s=Stream.of(p);
        if(Files.isDirectory(p)) try {
            DirectoryStream<Path> ds = Files.newDirectoryStream(p);
            s=Stream.concat(s, StreamSupport.stream(ds.spliterator(), false)
                .flatMap(sub -> walk(sub, handler))
                .onClose(()->{ try { ds.close(); }
                               catch(IOException ex) { handler.accept(p, ex); } }));
        } catch(IOException ex) { handler.accept(p, ex); }
        return s;
    }
    // and with depth limit
    public static Stream<Path> walk(
                  Path p, int maxDepth, BiConsumer<Path,IOException> handler) {
        Stream<Path> s=Stream.of(p);
        if(maxDepth>0 && Files.isDirectory(p)) try {
            DirectoryStream<Path> ds = Files.newDirectoryStream(p);
            s=Stream.concat(s, StreamSupport.stream(ds.spliterator(), false)
                .flatMap(sub -> walk(sub, maxDepth-1, handler))
                .onClose(()->{ try { ds.close(); }
                               catch(IOException ex) { handler.accept(p, ex); } }));
        } catch(IOException ex) { handler.accept(p, ex); }
        return s;
    }
}
like image 94
Holger Avatar answered Nov 13 '22 01:11

Holger