Docs and easy font loading. Added interfaces for PreloadResource's

This commit is contained in:
lieght
2025-10-01 18:09:14 +02:00
parent 8ae7510219
commit 744bef2ddf
14 changed files with 423 additions and 39 deletions

View File

@@ -4,6 +4,8 @@ import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.text.Font;
import org.toop.framework.asset.resources.FontAsset;
import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.framework.asset.resources.LocalizationAsset;
import java.util.Locale; import java.util.Locale;
@@ -12,10 +14,11 @@ import org.toop.framework.asset.resources.CssAsset;
import org.toop.framework.asset.resources.ImageAsset; import org.toop.framework.asset.resources.ImageAsset;
public final class MainMenu extends Menu { public final class MainMenu extends Menu {
private Locale currentLocale = Locale.of("nl"); private final Locale currentLocale = Locale.of("nl");
private LocalizationAsset loc = AssetManager.get("localization.properties"); private final LocalizationAsset loc = AssetManager.get("localization.properties");
public MainMenu() { public MainMenu() {
final Button tictactoe = createButton(loc.getString("mainMenuSelectTicTacToe", currentLocale), () -> {}); final Button tictactoe = createButton(loc.getString("mainMenuSelectTicTacToe", currentLocale), () -> {});
final Button reversi = createButton(loc.getString("mainMenuSelectReversi", currentLocale), () -> {}); final Button reversi = createButton(loc.getString("mainMenuSelectReversi", currentLocale), () -> {});
final Button sudoku = createButton(loc.getString("mainMenuSelectSudoku", currentLocale), () -> {}); final Button sudoku = createButton(loc.getString("mainMenuSelectSudoku", currentLocale), () -> {});

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,5 @@
.main-button { .main-button {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-background-image: url("card-default.jpg"); /* fallback image */
-fx-background-size: cover; -fx-background-size: cover;
-fx-background-position: center; -fx-background-position: center;
-fx-pref-width: 250px; -fx-pref-width: 250px;

View File

@@ -13,7 +13,7 @@
-fx-fill: white; -fx-fill: white;
-fx-font-size: 28px; -fx-font-size: 28px;
-fx-font-weight: 600; -fx-font-weight: 600;
-fx-font-family: "Segoe UI", sans-serif; -fx-font-family: "Groovy Maniac Demo", sans-serif;
} }
.quit-button { .quit-button {

View File

@@ -5,7 +5,38 @@ import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
/**
* A thread-safe, distributed unique ID generator following the Snowflake pattern.
* <p>
* Each generated 64-bit ID encodes:
* <ul>
* <li>41-bit timestamp (milliseconds since custom epoch)</li>
* <li>10-bit machine identifier</li>
* <li>12-bit sequence number for IDs generated in the same millisecond</li>
* </ul>
* </p>
*
* <p>This implementation ensures:
* <ul>
* <li>IDs are unique per machine.</li>
* <li>Monotonicity within the same machine.</li>
* <li>Safe concurrent generation via synchronized {@link #nextId()}.</li>
* </ul>
* </p>
*
* <p>Custom epoch is set to {@code 2025-01-01T00:00:00Z}.</p>
*
* <p>Usage example:</p>
* <pre>{@code
* SnowflakeGenerator generator = new SnowflakeGenerator();
* long id = generator.nextId();
* }</pre>
*/
public class SnowflakeGenerator { public class SnowflakeGenerator {
/**
* Custom epoch in milliseconds (2025-01-01T00:00:00Z).
*/
private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli(); private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
// Bit allocations // Bit allocations
@@ -13,19 +44,27 @@ public class SnowflakeGenerator {
private static final long MACHINE_BITS = 10; private static final long MACHINE_BITS = 10;
private static final long SEQUENCE_BITS = 12; private static final long SEQUENCE_BITS = 12;
// Max values // Maximum values for each component
private static final long MAX_MACHINE_ID = (1L << MACHINE_BITS) - 1; private static final long MAX_MACHINE_ID = (1L << MACHINE_BITS) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_BITS) - 1; private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_BITS) - 1;
// Bit shifts // Bit shifts for composing the ID
private static final long MACHINE_SHIFT = SEQUENCE_BITS; private static final long MACHINE_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS; private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
/**
* Unique machine identifier derived from network interfaces (10 bits).
*/
private static final long machineId = SnowflakeGenerator.genMachineId(); private static final long machineId = SnowflakeGenerator.genMachineId();
private final AtomicLong lastTimestamp = new AtomicLong(-1L); private final AtomicLong lastTimestamp = new AtomicLong(-1L);
private long sequence = 0L; private long sequence = 0L;
/**
* Generates a 10-bit machine identifier based on MAC addresses of network interfaces.
* Falls back to a random value if MAC cannot be determined.
*/
private static long genMachineId() { private static long genMachineId() {
try { try {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -35,17 +74,25 @@ public class SnowflakeGenerator {
for (byte b : mac) sb.append(String.format("%02X", b)); for (byte b : mac) sb.append(String.format("%02X", b));
} }
} }
// limit to 10 bits (01023) return sb.toString().hashCode() & 0x3FF; // limit to 10 bits
return sb.toString().hashCode() & 0x3FF;
} catch (Exception e) { } catch (Exception e) {
return (long) (Math.random() * 1024); // fallback return (long) (Math.random() * 1024); // fallback
} }
} }
/**
* For testing: manually set the last generated timestamp.
* @param l timestamp in milliseconds
*/
void setTime(long l) { void setTime(long l) {
this.lastTimestamp.set(l); this.lastTimestamp.set(l);
} }
/**
* Constructs a SnowflakeGenerator.
* Validates that the machine ID is within allowed range.
* @throws IllegalArgumentException if machine ID is invalid
*/
public SnowflakeGenerator() { public SnowflakeGenerator() {
if (machineId < 0 || machineId > MAX_MACHINE_ID) { if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
@@ -53,6 +100,16 @@ public class SnowflakeGenerator {
} }
} }
/**
* Generates the next unique ID.
* <p>
* If multiple IDs are generated in the same millisecond, a sequence number
* is incremented. If the sequence overflows, waits until the next millisecond.
* </p>
*
* @return a unique 64-bit ID
* @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit
*/
public synchronized long nextId() { public synchronized long nextId() {
long currentTimestamp = timestamp(); long currentTimestamp = timestamp();
@@ -67,7 +124,6 @@ public class SnowflakeGenerator {
if (currentTimestamp == lastTimestamp.get()) { if (currentTimestamp == lastTimestamp.get()) {
sequence = (sequence + 1) & MAX_SEQUENCE; sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) { if (sequence == 0) {
// Sequence overflow, wait for next millisecond
currentTimestamp = waitNextMillis(currentTimestamp); currentTimestamp = waitNextMillis(currentTimestamp);
} }
} else { } else {
@@ -81,6 +137,11 @@ public class SnowflakeGenerator {
| sequence; | sequence;
} }
/**
* Waits until the next millisecond if sequence overflows.
* @param lastTimestamp previous timestamp
* @return new timestamp
*/
private long waitNextMillis(long lastTimestamp) { private long waitNextMillis(long lastTimestamp) {
long ts = timestamp(); long ts = timestamp();
while (ts <= lastTimestamp) { while (ts <= lastTimestamp) {
@@ -89,6 +150,9 @@ public class SnowflakeGenerator {
return ts; return ts;
} }
/**
* Returns current system timestamp in milliseconds.
*/
private long timestamp() { private long timestamp() {
return System.currentTimeMillis(); return System.currentTimeMillis();
} }

View File

@@ -2,7 +2,7 @@ package org.toop.framework.asset;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.asset.events.AssetEvents; import org.toop.framework.asset.events.AssetLoaderEvents;
import org.toop.framework.asset.resources.*; import org.toop.framework.asset.resources.*;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.reflections.Reflections; import org.reflections.Reflections;
@@ -14,6 +14,35 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; import java.util.function.Function;
/**
* Responsible for loading assets from a file system directory into memory.
* <p>
* The {@code AssetLoader} scans a root folder recursively, identifies files,
* and maps them to registered resource types based on file extensions and
* {@link FileExtension} annotations.
* It supports multiple resource types including {@link PreloadResource} (automatically loaded)
* and {@link BundledResource} (merged across multiple files).
* </p>
*
* <p>Assets are stored in a static, thread-safe list and can be retrieved
* through {@link AssetManager}.</p>
*
* <p>Features:</p>
* <ul>
* <li>Recursive directory scanning for assets.</li>
* <li>Automatic registration of resource classes via reflection.</li>
* <li>Bundled resource support: multiple files merged into a single resource instance.</li>
* <li>Preload resources automatically invoke {@link PreloadResource#load()}.</li>
* <li>Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.</li>
* </ul>
*
* <p>Usage example:</p>
* <pre>{@code
* AssetLoader loader = new AssetLoader("assets");
* double progress = loader.getProgress();
* List<Asset<? extends BaseResource>> loadedAssets = loader.getAssets();
* }</pre>
*/
public class AssetLoader { public class AssetLoader {
private static final Logger logger = LogManager.getLogger(AssetLoader.class); private static final Logger logger = LogManager.getLogger(AssetLoader.class);
private static final List<Asset<? extends BaseResource>> assets = new CopyOnWriteArrayList<>(); private static final List<Asset<? extends BaseResource>> assets = new CopyOnWriteArrayList<>();
@@ -22,6 +51,10 @@ public class AssetLoader {
private final AtomicInteger loadedCount = new AtomicInteger(0); private final AtomicInteger loadedCount = new AtomicInteger(0);
private int totalCount = 0; private int totalCount = 0;
/**
* Constructs an AssetLoader and loads assets from the given root folder.
* @param rootFolder the folder containing asset files
*/
public AssetLoader(File rootFolder) { public AssetLoader(File rootFolder) {
autoRegisterResources(); autoRegisterResources();
List<File> foundFiles = new ArrayList<>(); List<File> foundFiles = new ArrayList<>();
@@ -30,30 +63,59 @@ public class AssetLoader {
loader(foundFiles); loader(foundFiles);
} }
/**
* Constructs an AssetLoader from a folder path.
* @param rootFolder the folder path containing assets
*/
public AssetLoader(String rootFolder) { public AssetLoader(String rootFolder) {
this(new File(rootFolder)); this(new File(rootFolder));
} }
/**
* Returns the current progress of loading assets (0.0 to 1.0).
* @return progress as a double
*/
public double getProgress() { public double getProgress() {
return (totalCount == 0) ? 1.0 : (loadedCount.get() / (double) totalCount); return (totalCount == 0) ? 1.0 : (loadedCount.get() / (double) totalCount);
} }
/**
* Returns the number of assets loaded so far.
* @return loaded count
*/
public int getLoadedCount() { public int getLoadedCount() {
return loadedCount.get(); return loadedCount.get();
} }
/**
* Returns the total number of files found to load.
* @return total asset count
*/
public int getTotalCount() { public int getTotalCount() {
return totalCount; return totalCount;
} }
/**
* Returns a snapshot list of all assets loaded by this loader.
* @return list of loaded assets
*/
public List<Asset<? extends BaseResource>> getAssets() { public List<Asset<? extends BaseResource>> getAssets() {
return new ArrayList<>(assets); return new ArrayList<>(assets);
} }
/**
* Registers a factory for a specific file extension.
* @param extension the file extension (without dot)
* @param factory a function mapping a File to a resource instance
* @param <T> the type of resource
*/
public <T extends BaseResource> void register(String extension, Function<File, T> factory) { public <T extends BaseResource> void register(String extension, Function<File, T> factory) {
this.registry.put(extension, factory); this.registry.put(extension, factory);
} }
/**
* Maps a file to a resource instance based on its extension and registered factories.
*/
private <T extends BaseResource> T resourceMapper(File file, Class<T> type) { private <T extends BaseResource> T resourceMapper(File file, Class<T> type) {
String ext = getExtension(file.getName()); String ext = getExtension(file.getName());
Function<File, ? extends BaseResource> factory = registry.get(ext); Function<File, ? extends BaseResource> factory = registry.get(ext);
@@ -69,6 +131,9 @@ public class AssetLoader {
return type.cast(resource); return type.cast(resource);
} }
/**
* Loads the given list of files into assets, handling bundled and preload resources.
*/
private void loader(List<File> files) { private void loader(List<File> files) {
Map<String, BundledResource> bundledResources = new HashMap<>(); Map<String, BundledResource> bundledResources = new HashMap<>();
@@ -88,7 +153,7 @@ public class AssetLoader {
bundledResources.put(key, br); bundledResources.put(key, br);
} }
} }
case FontAsset fontAsset -> fontAsset.load(); case PreloadResource pr -> pr.load();
default -> { default -> {
} }
} }
@@ -103,12 +168,14 @@ public class AssetLoader {
logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath()); logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath());
loadedCount.incrementAndGet(); loadedCount.incrementAndGet();
new EventFlow() new EventFlow()
.addPostEvent(new AssetEvents.LoadingProgressUpdate(loadedCount.get(), totalCount)) .addPostEvent(new AssetLoaderEvents.LoadingProgressUpdate(loadedCount.get(), totalCount))
.postEvent(); .postEvent();
} }
} }
/**
* Recursively searches a folder and adds all files to the foundFiles list.
*/
private void fileSearcher(final File folder, List<File> foundFiles) { private void fileSearcher(final File folder, List<File> foundFiles) {
for (File fileEntry : Objects.requireNonNull(folder.listFiles())) { for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
if (fileEntry.isDirectory()) { if (fileEntry.isDirectory()) {
@@ -119,6 +186,10 @@ public class AssetLoader {
} }
} }
/**
* Uses reflection to automatically register all {@link BaseResource} subclasses
* annotated with {@link FileExtension}.
*/
private void autoRegisterResources() { private void autoRegisterResources() {
Reflections reflections = new Reflections("org.toop.framework.asset.resources"); Reflections reflections = new Reflections("org.toop.framework.asset.resources");
Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class); Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class);
@@ -138,6 +209,9 @@ public class AssetLoader {
} }
} }
/**
* Extracts the base name from a file name, used for bundling multiple files.
*/
private static String getBaseName(String fileName) { private static String getBaseName(String fileName) {
int underscoreIndex = fileName.indexOf('_'); int underscoreIndex = fileName.indexOf('_');
int dotIndex = fileName.lastIndexOf('.'); int dotIndex = fileName.lastIndexOf('.');
@@ -145,6 +219,9 @@ public class AssetLoader {
return fileName.substring(0, dotIndex); return fileName.substring(0, dotIndex);
} }
/**
* Returns the file extension of a given file name (without dot).
*/
public static String getExtension(String name) { public static String getExtension(String name) {
int i = name.lastIndexOf('.'); int i = name.lastIndexOf('.');
return (i > 0) ? name.substring(i + 1) : ""; return (i > 0) ? name.substring(i + 1) : "";

View File

@@ -5,22 +5,79 @@ import org.toop.framework.asset.resources.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* Centralized manager for all loaded assets in the application.
* <p>
* {@code AssetManager} maintains a thread-safe registry of {@link Asset} objects
* and provides utility methods to retrieve assets by name, ID, or type.
* It works together with {@link AssetLoader} to register assets automatically
* when they are loaded from the file system.
* </p>
*
* <p>Key responsibilities:</p>
* <ul>
* <li>Storing all loaded assets in a concurrent map.</li>
* <li>Providing typed access to asset resources.</li>
* <li>Allowing lookup by asset name or ID.</li>
* <li>Supporting retrieval of all assets of a specific {@link BaseResource} subclass.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@code
* // Load assets from a loader
* AssetLoader loader = new AssetLoader(new File("RootFolder"));
* AssetManager.loadAssets(loader);
*
* // Retrieve a single resource
* ImageAsset background = AssetManager.get("background.jpg");
*
* // Retrieve all fonts
* List<Asset<FontAsset>> fonts = AssetManager.getAllOfType(FontAsset.class);
*
* // Retrieve by asset name or optional lookup
* Optional<Asset<? extends BaseResource>> maybeAsset = AssetManager.findByName("menu.css");
* }</pre>
*
* <p>Notes:</p>
* <ul>
* <li>All retrieval methods are static and thread-safe.</li>
* <li>The {@link #get(String)} method may require casting if the asset type is not known at compile time.</li>
* <li>Assets should be loaded via {@link AssetLoader} before retrieval.</li>
* </ul>
*/
public class AssetManager { public class AssetManager {
private static final AssetManager INSTANCE = new AssetManager(); private static final AssetManager INSTANCE = new AssetManager();
private static final Map<String, Asset<? extends BaseResource>> assets = new ConcurrentHashMap<>(); private static final Map<String, Asset<? extends BaseResource>> assets = new ConcurrentHashMap<>();
private AssetManager() {} private AssetManager() {}
/**
* Returns the singleton instance of {@code AssetManager}.
*
* @return the shared instance
*/
public static AssetManager getInstance() { public static AssetManager getInstance() {
return INSTANCE; return INSTANCE;
} }
/**
* Loads all assets from a given {@link AssetLoader} into the manager.
*
* @param loader the loader that has already loaded assets
*/
public synchronized static void loadAssets(AssetLoader loader) { public synchronized static void loadAssets(AssetLoader loader) {
for (var asset : loader.getAssets()) { for (var asset : loader.getAssets()) {
assets.put(asset.getName(), asset); assets.put(asset.getName(), asset);
} }
} }
/**
* Retrieve the resource of a given name, cast to the expected type.
*
* @param name the asset name
* @param <T> the expected resource type
* @return the resource, or null if not found
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <T extends BaseResource> T get(String name) { public static <T extends BaseResource> T get(String name) {
Asset<T> asset = (Asset<T>) assets.get(name); Asset<T> asset = (Asset<T>) assets.get(name);
@@ -28,9 +85,16 @@ public class AssetManager {
return asset.getResource(); return asset.getResource();
} }
/**
* Retrieve all assets of a specific resource type.
*
* @param type the class type to filter
* @param <T> the resource type
* @return a list of assets matching the type
*/
public static <T extends BaseResource> ArrayList<Asset<T>> getAllOfType(Class<T> type) { public static <T extends BaseResource> ArrayList<Asset<T>> getAllOfType(Class<T> type) {
ArrayList<Asset<T>> list = new ArrayList<>(); ArrayList<Asset<T>> list = new ArrayList<>();
for (Asset<? extends BaseResource> asset : assets.values()) { // <-- use .values() for (Asset<? extends BaseResource> asset : assets.values()) {
if (type.isInstance(asset.getResource())) { if (type.isInstance(asset.getResource())) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Asset<T> typed = (Asset<T>) asset; Asset<T> typed = (Asset<T>) asset;
@@ -40,6 +104,12 @@ public class AssetManager {
return list; return list;
} }
/**
* Retrieve an asset by its unique ID.
*
* @param id the asset ID
* @return the asset, or null if not found
*/
public static Asset<? extends BaseResource> getById(String id) { public static Asset<? extends BaseResource> getById(String id) {
for (Asset<? extends BaseResource> asset : assets.values()) { for (Asset<? extends BaseResource> asset : assets.values()) {
if (asset.getId().toString().equals(id)) { if (asset.getId().toString().equals(id)) {
@@ -49,34 +119,32 @@ public class AssetManager {
return null; return null;
} }
/**
* Retrieve an asset by its name.
*
* @param name the asset name
* @return the asset, or null if not found
*/
public static Asset<? extends BaseResource> getByName(String name) { public static Asset<? extends BaseResource> getByName(String name) {
return assets.get(name); return assets.get(name);
} }
/**
* Attempt to find an asset by name, returning an {@link Optional}.
*
* @param name the asset name
* @return an Optional containing the asset if found
*/
public static Optional<Asset<? extends BaseResource>> findByName(String name) { public static Optional<Asset<? extends BaseResource>> findByName(String name) {
return Optional.ofNullable(assets.get(name)); return Optional.ofNullable(assets.get(name));
} }
/**
* Add a new asset to the manager.
*
* @param asset the asset to add
*/
public static void addAsset(Asset<? extends BaseResource> asset) { public static void addAsset(Asset<? extends BaseResource> asset) {
assets.put(asset.getName(), asset); assets.put(asset.getName(), asset);
} }
}
// public static LocalizationAsset getLocalization(Locale locale) {
// for (Asset<? extends BaseResource> asset : assets.values()) {
// if (asset.getResource() instanceof LocalizationAsset locAsset) {
// if (!locAsset.isLoaded()) locAsset.load();
// if (locAsset.hasLocale(locale)) return locAsset;
// }
// }
// // fallback NL
// Locale fallback = Locale.forLanguageTag("nl");
// for (Asset<? extends BaseResource> asset : assets.values()) {
// if (asset.getResource() instanceof LocalizationAsset locAsset) {
// if (!locAsset.isLoaded()) locAsset.load();
// if (locAsset.hasLocale(fallback)) return locAsset;
// }
// }
// throw new NoSuchElementException("No localization asset available for locale: " + locale + " or fallback: nl");
// }
}

View File

@@ -2,6 +2,6 @@ package org.toop.framework.asset.events;
import org.toop.framework.eventbus.events.EventWithoutSnowflake; import org.toop.framework.eventbus.events.EventWithoutSnowflake;
public class AssetEvents { public class AssetLoaderEvents {
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount) implements EventWithoutSnowflake {} public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount) implements EventWithoutSnowflake {}
} }

View File

@@ -2,14 +2,66 @@ package org.toop.framework.asset.resources;
import java.io.File; import java.io.File;
/**
* Represents a resource that can be composed of multiple files, or "bundled" together
* under a common base name.
*
* <p>Implementing classes allow an {@link org.toop.framework.asset.AssetLoader}
* to automatically merge multiple related files into a single resource instance.</p>
*
* <p>Typical use cases include:</p>
* <ul>
* <li>Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
* `messages_nl.properties`) are grouped under the same logical resource.</li>
* <li>Sprite sheets, tile sets, or other multi-file resources that logically belong together.</li>
* </ul>
*
* <p>Implementing classes must provide:</p>
* <ul>
* <li>{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.</li>
* <li>{@link #getBaseName()}: A consistent base name used to group multiple files into this resource.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@code
* public class LocalizationAsset extends BaseResource implements BundledResource {
* private final String baseName;
*
* public LocalizationAsset(File file) {
* super(file);
* this.baseName = extractBaseName(file.getName());
* loadFile(file);
* }
*
* @Override
* public void loadFile(File file) {
* // merge file into existing bundles
* }
*
* @Override
* public String getBaseName() {
* return baseName;
* }
* }
* }</pre>
*
* <p>When used with an asset loader, all files sharing the same base name are
* automatically merged into a single resource instance.</p>
*/
public interface BundledResource { public interface BundledResource {
/** /**
* Load or merge an additional file into this resource. * Load or merge an additional file into this resource.
*
* @param file the file to load or merge
*/ */
void loadFile(File file); void loadFile(File file);
/** /**
* Return a base name for grouping multiple files into this single resource. * Return a base name for grouping multiple files into this single resource.
* Files with the same base name are automatically merged by the loader.
*
* @return the base name used to identify this bundled resource
*/ */
String getBaseName(); String getBaseName();
} }

View File

@@ -5,8 +5,37 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
/**
* Annotation to declare which file extensions a {@link BaseResource} subclass
* can handle.
*
* <p>This annotation is processed by the {@link org.toop.framework.asset.AssetLoader}
* to automatically register resource types for specific file extensions.
* Each extension listed will be mapped to the annotated resource class,
* allowing the loader to instantiate the correct type when scanning files.</p>
*
* <p>Usage example:</p>
* <pre>{@code
* @FileExtension({"png", "jpg"})
* public class ImageAsset extends BaseResource implements LoadableResource {
* ...
* }
* }</pre>
*
* <p>Key points:</p>
* <ul>
* <li>The annotation is retained at runtime for reflection-based registration.</li>
* <li>Can only be applied to types (classes) that extend {@link BaseResource}.</li>
* <li>Multiple extensions can be specified in the {@code value()} array.</li>
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface FileExtension { public @interface FileExtension {
/**
* The list of file extensions (without leading dot) that the annotated resource class can handle.
*
* @return array of file extensions
*/
String[] value(); String[] value();
} }

View File

@@ -6,7 +6,7 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@FileExtension({"ttf", "otf"}) @FileExtension({"ttf", "otf"})
public class FontAsset extends BaseResource implements LoadableResource { public class FontAsset extends BaseResource implements PreloadResource {
private String family; private String family;
public FontAsset(final File fontFile) { public FontAsset(final File fontFile) {

View File

@@ -1,9 +1,64 @@
package org.toop.framework.asset.resources; package org.toop.framework.asset.resources;
import java.io.FileNotFoundException; /**
* Represents a resource that can be explicitly loaded and unloaded.
* <p>
* Any class implementing {@code LoadableResource} is responsible for managing its own
* loading and unloading logic, such as reading files, initializing data structures,
* or allocating external resources.
* </p>
*
* <p>Implementing classes must define the following behaviors:</p>
* <ul>
* <li>{@link #load()}: Load the resource into memory or perform necessary initialization.</li>
* <li>{@link #unload()}: Release any held resources or memory when the resource is no longer needed.</li>
* <li>{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and is ready for use, {@code false} otherwise.</li>
* </ul>
*
* <p>Typical usage:</p>
* <pre>{@code
* public class MyFontAsset extends BaseResource implements LoadableResource {
* private boolean loaded = false;
*
* @Override
* public void load() {
* // Load font file into memory
* loaded = true;
* }
*
* @Override
* public void unload() {
* // Release resources if needed
* loaded = false;
* }
*
* @Override
* public boolean isLoaded() {
* return loaded;
* }
* }
* }</pre>
*
* <p>This interface is commonly used with {@link PreloadResource} to allow automatic
* loading by an {@link org.toop.framework.asset.AssetLoader} if desired.</p>
*/
public interface LoadableResource { public interface LoadableResource {
/**
* Load the resource into memory or initialize it.
* This method may throw runtime exceptions if loading fails.
*/
void load(); void load();
/**
* Unload the resource and free any associated resources.
* After this call, {@link #isLoaded()} should return false.
*/
void unload(); void unload();
/**
* Check whether the resource has been successfully loaded.
*
* @return true if the resource is loaded and ready for use, false otherwise
*/
boolean isLoaded(); boolean isLoaded();
} }

View File

@@ -0,0 +1,37 @@
package org.toop.framework.asset.resources;
/**
* Marker interface for resources that should be **automatically loaded** by the {@link org.toop.framework.asset.AssetLoader}.
*
* <p>Extends {@link LoadableResource}, so any implementing class must provide the standard
* {@link LoadableResource#load()} and {@link LoadableResource#unload()} methods, as well as the
* {@link LoadableResource#isLoaded()} check.</p>
*
* <p>When a resource implements {@code PreloadResource}, the {@code AssetLoader} will invoke
* {@link LoadableResource#load()} automatically after the resource is discovered and instantiated,
* without requiring manual loading by the user.</p>
*
* <p>Typical usage:</p>
* <pre>{@code
* public class MyFontAsset extends BaseResource implements PreloadResource {
* @Override
* public void load() {
* // load the font into memory
* }
*
* @Override
* public void unload() {
* // release resources if needed
* }
*
* @Override
* public boolean isLoaded() {
* return loaded;
* }
* }
* }</pre>
*
* <p>Note: Only use this interface for resources that are safe to load at startup, as it may
* increase memory usage or startup time.</p>
*/
public interface PreloadResource extends LoadableResource {}