3

I'm trying to set an JFX ImageView image from a resource folder, but can't seem to get an appropriate URL/String filepath that won't throw an exception.

var x = getRandomImageFromPackage("pictures").toString();
var y = getClass().getClassLoader().getResource("pictures/mindwave/active/Super Saiyan.gif").toString();
this.iconImageView.setImage(new Image(x));

x returns

/home/sarah/Desktop/Dropbox/School/Current/MAX MSP Utilities/MindWaveMobileDataServer/target/classes/pictures/0515e3b7cb30ac92ebfe729440870a5c.jpg

whereas y returns something that looks like:

file:/home/sarah/Desktop/Dropbox/School/Current/MAX%20MSP%20Utilities/MindWaveMobileDataServer/target/classes/pictures/mindwave/active/Super%20Saiyan.gif

In theory either of these would be acceptable, however, only x will throw an exception if it is placed in the below setImage(String) line.

Is there any way to get a list of images in the package so that I can select a random one and set the ImageView?

I know that there was a custom scanner option, but it appears rather dated (being over 11 years old and wasn't really supported at the time):

Routine:

/**
 * Gets a picture from the classpath pictures folder.
 *
 * @param packageName The string path (in package format) to the classpath
 * folder
 * @return The random picture
 */
private Path getRandomImageFromPackage(String packageName) {
    try {
        var list = Arrays.asList(new File(Thread.currentThread().getContextClassLoader().getResource(packageName)
                .toURI()).listFiles());
        var x = list.get(new Random().nextInt(list.size())).toString();
        return list.get(new Random().nextInt(list.size())).toPath();

    } catch (URISyntaxException ex) {
        throw new IllegalStateException("Encountered an error while trying to get a picture from the classpath"
                + "filesystem", ex);
    }
}

For reference, this is the resource folder:

enter image description here

jewelsea
  • 150,031
  • 14
  • 366
  • 406
Sarah Szabo
  • 10,345
  • 9
  • 37
  • 60
  • 2
    Look at [this answer](https://stackoverflow.com/a/36021165/2711488) for an up-to-date approach to list resources (or perform filesystem queries in general). It works at least with the default filesystem, as well as jar files and module images. Other deployed forms may work as well if their author cared enough to add a filesystem implementation. – Holger Feb 07 '22 at 08:54

2 Answers2

4

Issues with your approach

You don't have a well-formed url

new Image(String url) takes a url as a parameter.

A space is not a valid character for a URL:

which is why your x string is not a valid URL and cannot be used to construct an image.

You need to provide an input recognized by the Image constructor

Note, that it is slightly more complex because, from the Image javadoc, the url parameter can be somethings other than a straight url, but even still, none of them match what you are trying to lookup.

If a URL string is passed to a constructor, it be any of the following:

  1. the name of a resource that can be resolved by the context ClassLoader for this thread
  2. a file path that can be resolved by File
  3. a URL that can be resolved by URL and for which a protocol handler exists

The RFC 2397 "data" scheme for URLs is supported in addition to the protocol handlers that are registered for the application. If a URL uses the "data" scheme, the data must be base64-encoded and the MIME type must either be empty or a subtype of the image type.

You are assuming the resources are in a file system, but that won't always work

If you pack your resources into a jar, then this will not work:

Arrays.asList(
    new File(
        Thread.currentThread()
            .getContextClassLoader()
            .getResource(packageName)
            .toURI()
    ).listFiles()
);

This doesn't work because files in the jar are located using the jar: protocol rather than the file: protocol. So, you will be unable to create File objects from the jar: protocol URIs that will be returned by getResource.

Recommended Approach: Use Spring

Getting a list of resources from a jar is actually a pretty tricky thing. From the question you linked, the easiest solution is the one which uses

Unfortunately, that means requiring a dependency on the Spring framework to use it, which is total overkill for this task . . . however I don't know of any other simple robust solution. But at least you can just call the Spring utility class, you don't need to start up a whole spring dependency injection container to use it, so you don't really need to know any Spring at all or suffer any Spring overhead to do it this way.

So, you could write something like this (ResourceLister is a class I created, as well as the toURL method, see the example app):

public List<String> getResourceUrls(String locationPattern) throws IOException {
    ClassLoader classLoader = ResourceLister.class.getClassLoader();
    PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);

    Resource[] resources = resolver.getResources(locationPattern);

    return Arrays.stream(resources)
            .map(this::toURL)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
}

Executable Example

ResourceLister.java

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public class ResourceLister {
    // currently, only gets pngs, if needed, can add
    // other patterns and union the results to get 
    // multiple image types. 
    private static final String IMAGE_PATTERN =
            "classpath:/img/*.png";

    public List<String> getImageUrls() throws IOException {
        return getResourceUrls(IMAGE_PATTERN);
    }

    public List<String> getResourceUrls(String locationPattern) throws IOException {
        ClassLoader classLoader = ResourceLister.class.getClassLoader();
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);

        Resource[] resources = resolver.getResources(locationPattern);

        return Arrays.stream(resources)
                .map(this::toURL)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private String toURL(Resource r) {
        try {
            if (r == null) {
                return null;
            }

            return r.getURL().toExternalForm();
        } catch (IOException e) {
            return null;
        }
    }

    public static void main(String[] args) throws IOException {
        ResourceLister lister = new ResourceLister();
        System.out.println(lister.getImageUrls());
    }
}

AnimalApp.java

import javafx.application.Application;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public class AnimalApp extends Application {
    private static final double ANIMAL_SIZE = 512;

    // remove the magic seed if you want a different random sequence all the time.
    private final Random random = new Random(42);

    private final ResourceLister resourceLister = new ResourceLister();

    private List<Image> images;

    @Override
    public void init() {
        List<String> imageUrls = findImageUrls();
        images = imageUrls.stream()
                .map(Image::new)
                .collect(Collectors.toList());
    }

    @Override
    public void start(Stage stage) {
        ImageView animalView = new ImageView();
        animalView.setFitWidth(ANIMAL_SIZE);
        animalView.setFitHeight(ANIMAL_SIZE);
        animalView.setPreserveRatio(true);

        Button findAnimalButton = new Button("Find animal");
        findAnimalButton.setOnAction(e ->
                animalView.setImage(randomImage())
        );

        VBox layout = new VBox(10,
                findAnimalButton,
                animalView
        );
        layout.setPadding(new Insets(10));
        layout.setAlignment(Pos.CENTER);

        stage.setScene(new Scene(layout));
        stage.show();
    }

    private List<String> findImageUrls() {
        try {
            return resourceLister.getImageUrls();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return new ArrayList<>();
    }

    /**
     * Chooses a random image.
     *
     * Allows the next random image chosen to be the same as the previous image.
     *
     * @return a random image or null if no images were found.
     */
    private Image randomImage() {
        if (images == null || images.isEmpty()) {
            return null;
        }

        return images.get(random.nextInt(images.size()));
    }

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

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>resource-lister</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>resource-lister</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.7.1</junit.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>17.0.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>LATEST</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

images

Place in src/main/resources/img.

  • chicken.png
  • cow.png
  • pig.png
  • sheep.png

chicken cow pig sheep

execution command

Set the VM arguments for your JavaFX SDK installation:

-p C:\dev\javafx-sdk-17.0.2\lib --add-modules javafx.controls
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • I tried to use this code with "classpath*" prefix because my service couldn't find a file with Spring's ResourceLoader which was in resources when deployed on AWS (works locally), but all I got a list of dependencies jar:file:/app.jar!/ jar:file:/app.jar!/BOOT-INF/classes!/ jar:file:/app.jar!/BOOT-INF/lib/spring-boot-2.7.3.jar!/ jar:file:/app.jar!/BOOT-INF/lib/spring-context-5.3.22.jar!/ jar:file:/app.jar!/BOOT-INF/lib/spring-boot-autoconfigure-2.7.3.jar!/ (etc...) What could it mean? – Lutosław Apr 20 '23 at 11:10
  • @Lutosław ask a new question, you can refer to this one. In the new question, try to include enough information and explanation so that somebody can understand and replicate your issue (which I can't from your comment). As an alternative, you can always use [mipa's answer](https://stackoverflow.com/a/70996497/1155209). – jewelsea Apr 20 '23 at 12:55
2

There is no easy and reliable way to do that. Therefore I create and put an inventory file into my resources folder. So at runtime I can read that in and then have all the file names awailable that I need.

Here is a little test that shows how I create that file:

public class ListAppDefaultsInventory {

    @Test
    public void test() throws IOException {
        List<String> inventory = listFilteredFiles("src/main/resources/app-defaults", Integer.MAX_VALUE);
        assertFalse("Directory 'app-defaults' is empty.", inventory.isEmpty());
        System.out.println("# src/main/resources/app-defaults-inventory.txt");
        inventory.forEach(s -> System.out.println(s));
    }
    
    public List<String> listFilteredFiles(String dir, int depth) throws IOException {
        try (Stream<Path> stream = Files.walk(Paths.get(dir), depth)) {
            return stream
                .filter(file -> !Files.isDirectory(file))
                .filter(file -> !file.getFileName().toString().startsWith("."))
                .map(Path::toString)
                .map(s -> s.replaceFirst("src/main/resources/app-defaults/", ""))
                .collect(Collectors.toList());
        }
    }
    
}
mipa
  • 10,369
  • 2
  • 16
  • 35