Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package core.gui;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import core.gui.image.ImageProvider;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

import static org.apache.commons.lang3.StringUtils.EMPTY;

public class UrlImageLabel extends JLabel {
public class AsyncImageLabel extends JLabel {

public UrlImageLabel() {
private long loadCounter = 0;

public AsyncImageLabel() {
super(EMPTY, SwingConstants.CENTER);
setOpaque(true);
setBackground(new Color(245, 245, 245));
Expand All @@ -27,67 +24,38 @@ public UrlImageLabel() {
setMinimumSize(dimension);
}

/**
* Loads primary, then fallback, otherwise placeholder
*/
public void loadWithFallback(String primaryUrl, String fallbackUrl) {
final var fixedPrimaryUrl = toUrlWithHttps(primaryUrl);
final var fixedFallbackUrl = toUrlWithHttps(fallbackUrl);
public void load(ImageProvider imageProvider) {
final long currentLoad = ++loadCounter;

showLoading();

new SwingWorker<BufferedImage, Void>() {
@Override
protected BufferedImage doInBackground() {
BufferedImage img = tryLoad(fixedPrimaryUrl);
if (img != null) {
return img;
}

if (StringUtils.isNotBlank(fixedFallbackUrl)) {
img = tryLoad(fixedFallbackUrl);
}
return img; // may be null
return imageProvider.load().orElse(null);
}

@Override
protected void done() {
if (currentLoad != loadCounter) {
return;
}

try {
BufferedImage img = get();

if (img != null) {
showImage(img);
} else {
showPlaceholder("Image not available");
}
} catch (Exception ex) {
// If the worker fails unexpectedly
showPlaceholder("Error loading image");
}
}
}.execute();
}

private BufferedImage tryLoad(String urlString) {
if (StringUtils.isBlank(urlString)) {
return null;
}

try {
URL url = new URL(urlString);
URLConnection con = url.openConnection();
con.setRequestProperty("User-Agent", "Mozilla/5.0");
con.setConnectTimeout(4000);
con.setReadTimeout(8000);

try (InputStream in = con.getInputStream()) {
// ImageIO.read may return null if the input is not a supported image format
return ImageIO.read(in);
}
} catch (Exception e) {
return null;
}
}

private void showLoading() {
setIcon(null);
setText("Loading image …");
Expand All @@ -109,8 +77,4 @@ private void showPlaceholder(String text) {
revalidate();
repaint();
}

private static String toUrlWithHttps(String url) {
return Strings.CS.startsWith(url, "//") ? "https:" + url : url;
}
}
147 changes: 147 additions & 0 deletions src/main/java/core/gui/image/CachedUrlImageProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package core.gui.image;

import core.util.FileNameUtil;
import core.util.HOLogger;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

public class CachedUrlImageProvider implements ImageProvider {

private static final String IMAGE_FORMAT_NAME = "PNG";

private final Path cacheDirectory;
private final String primaryUrl;
private final String fallbackUrl;

public CachedUrlImageProvider(Path cacheDirectory, String primaryUrl, String fallbackUrl) {
this.cacheDirectory = cacheDirectory;
this.primaryUrl = toUrlWithHttps(primaryUrl);
this.fallbackUrl = toUrlWithHttps(fallbackUrl);
}

@Override
public Optional<BufferedImage> load() {
Optional<BufferedImage> primary = tryLoad(primaryUrl);

if (primary.isPresent()) {
return primary;
}

if (StringUtils.isNotBlank(fallbackUrl)) {
return tryLoad(fallbackUrl);
}

return Optional.empty();
}

private Optional<BufferedImage> tryLoad(String urlString) {
if (StringUtils.isBlank(urlString)) {
return Optional.empty();
}

Path fullPath = cacheDirectory
.toAbsolutePath()
.resolve(toCacheFilename(urlString));

return loadFromFile(fullPath)
.or(() -> loadFromUrlAndSave(urlString, fullPath));
}

private static Optional<BufferedImage> loadFromUrlAndSave(String urlString, Path fullPath) {
Optional<BufferedImage> fromUrl = loadFromUrl(urlString);
fromUrl.ifPresent(bufferedImage -> saveToFile(bufferedImage, fullPath));
return fromUrl;
}

private static Optional<BufferedImage> loadFromUrl(String urlString) {
try {
URL url = new URL(urlString);
URLConnection con = url.openConnection();

con.setRequestProperty("User-Agent", "Mozilla/5.0");
con.setConnectTimeout(4000);
con.setReadTimeout(8000);

try (InputStream in = con.getInputStream()) {
return Optional.ofNullable(ImageIO.read(in));
}
} catch (FileNotFoundException e) {
// Expected if the club has no custom stadium image.
return Optional.empty();
} catch (IOException e) {
HOLogger.instance().debug(CachedUrlImageProvider.class,
"Image could not be loaded from URL '%s': %s: %s".formatted(urlString, e.getClass().getSimpleName(), e.getMessage()));
return Optional.empty();
}
}

private static Optional<BufferedImage> loadFromFile(Path path) {
if (!Files.isRegularFile(path)) {
return Optional.empty();
}

try {
Optional<BufferedImage> image = Optional.ofNullable(ImageIO.read(path.toFile()));

if (image.isPresent()) {
HOLogger.instance().log(CachedUrlImageProvider.class,
"Image loaded from file '%s'.".formatted(path));
}

return image;
} catch (IOException e) {
HOLogger.instance().error(CachedUrlImageProvider.class,
"Failed to load image from file '%s': %s".formatted(path, e.getMessage()));
return Optional.empty();
}
}

private static void saveToFile(BufferedImage image, Path path) {
try {
Path parent = path.getParent();
if (parent != null) {
Files.createDirectories(parent);
}

boolean written = ImageIO.write(image, IMAGE_FORMAT_NAME, path.toFile());

if (written) {
HOLogger.instance().log(CachedUrlImageProvider.class,
"Saved image (format '%s') to file '%s'.".formatted(IMAGE_FORMAT_NAME, path));
} else {
HOLogger.instance().warning(CachedUrlImageProvider.class,
"Could not save image (format '%s') to file '%s'.".formatted(IMAGE_FORMAT_NAME, path));
}
} catch (IOException e) {
HOLogger.instance().error(CachedUrlImageProvider.class,
"Failed to save image (format '%s') to file '%s': %s"
.formatted(IMAGE_FORMAT_NAME, path, e.getMessage()));
}
}

private static String toCacheFilename(String fixedUrl) {
return "%s.%s".formatted(
FileNameUtil.fileNameFromUrl(fixedUrl),
IMAGE_FORMAT_NAME.toLowerCase()
);
}

private static String toUrlWithHttps(String url) {
if (StringUtils.isBlank(url)) {
return url;
}

return Strings.CS.startsWith(url, "//") ? "https:" + url : url;
}
}
10 changes: 10 additions & 0 deletions src/main/java/core/gui/image/ImageProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package core.gui.image;

import java.awt.image.BufferedImage;
import java.util.Optional;

@FunctionalInterface
public interface ImageProvider {

Optional<BufferedImage> load();
}
Loading