Add support for disabling classpath scanning

This commit however does not implement support for Substrate VM/native-image,
which I've already tested. Sad.
This commit is contained in:
JeremyStar™ 2024-09-21 17:20:14 +02:00
parent ebbc1778ae
commit 0fbfe8f4e3
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
10 changed files with 553 additions and 119 deletions

View file

@ -20,15 +20,13 @@
package de.staropensource.sosengine.base; package de.staropensource.sosengine.base;
import de.staropensource.sosengine.base.annotation.EngineSubsystem; import de.staropensource.sosengine.base.annotation.EngineSubsystem;
import de.staropensource.sosengine.base.implementable.ShutdownHandler;
import de.staropensource.sosengine.base.implementable.SubsystemClass;
import de.staropensource.sosengine.base.implementable.helper.EventHelper;
import de.staropensource.sosengine.base.utility.information.EngineInformation;
import de.staropensource.sosengine.base.utility.information.JvmInformation;
import de.staropensource.sosengine.base.implementation.versioning.StarOpenSourceVersioningSystem;
import de.staropensource.sosengine.base.event.*; import de.staropensource.sosengine.base.event.*;
import de.staropensource.sosengine.base.exception.IllegalAccessException; import de.staropensource.sosengine.base.exception.IllegalAccessException;
import de.staropensource.sosengine.base.exception.dependency.UnmetDependenciesException; import de.staropensource.sosengine.base.exception.dependency.UnmetDependenciesException;
import de.staropensource.sosengine.base.implementable.ShutdownHandler;
import de.staropensource.sosengine.base.implementable.SubsystemClass;
import de.staropensource.sosengine.base.implementable.helper.EventHelper;
import de.staropensource.sosengine.base.implementation.versioning.StarOpenSourceVersioningSystem;
import de.staropensource.sosengine.base.internal.event.InternalEngineShutdownEvent; import de.staropensource.sosengine.base.internal.event.InternalEngineShutdownEvent;
import de.staropensource.sosengine.base.internal.type.DependencySubsystemVector; import de.staropensource.sosengine.base.internal.type.DependencySubsystemVector;
import de.staropensource.sosengine.base.logging.*; import de.staropensource.sosengine.base.logging.*;
@ -38,6 +36,8 @@ import de.staropensource.sosengine.base.type.immutable.ImmutableLinkedList;
import de.staropensource.sosengine.base.utility.DependencyResolver; import de.staropensource.sosengine.base.utility.DependencyResolver;
import de.staropensource.sosengine.base.utility.Miscellaneous; import de.staropensource.sosengine.base.utility.Miscellaneous;
import de.staropensource.sosengine.base.utility.PlaceholderEngine; import de.staropensource.sosengine.base.utility.PlaceholderEngine;
import de.staropensource.sosengine.base.utility.information.EngineInformation;
import de.staropensource.sosengine.base.utility.information.JvmInformation;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -249,12 +249,34 @@ public final class Engine extends SubsystemClass {
private boolean checkEnvironment() { private boolean checkEnvironment() {
logger.diag("Checking environment"); logger.diag("Checking environment");
// Warn about potential Java incompatibilities
if (JvmInformation.getJavaVersion() > EngineInformation.getJavaSource()) if (JvmInformation.getJavaVersion() > EngineInformation.getJavaSource())
logger.warn("The StarOpenSource Engine is running on an untested Java version.\nThings may not work as expected or features which can improve performance, stability, compatibility or ease of use may be missing.\nIf you encounter issues, try running a JVM implementing Java " + EngineInformation.getJavaSource()); logger.warn("The StarOpenSource Engine is running on an untested Java version.\nThings may not work as expected or features which can improve performance, stability, compatibility or ease of use may be missing.\nIf you encounter issues, try running a JVM implementing Java " + EngineInformation.getJavaSource());
// Check if reflective classpath scanning is supported
if (checkClasspathScanningSupport()) {
logger.warn("Running in an classpath scanning-unfriendly environment, disabling.");
logger.warn("If shit doesn't work and is expected to be discovered by annotations, you'll need to");
logger.warn("either register it first or have to place classes in some package.");
logger.warn("Please consult sos!engine's documentation for more information about this issue.");
EngineInternals.getInstance().overrideReflectiveClasspathScanning(false);
}
return false; return false;
} }
/**
* Returns whether scanning the classpath is supported.
*
* @return test results
* @since v1-alpha5
*/
private boolean checkClasspathScanningSupport() {
// This may be expanded in the future
return EngineConfiguration.getInstance().isInitialForceDisableClasspathScanning();
}
/** /**
* Ensures the execution safety of the environment. * Ensures the execution safety of the environment.
* *
@ -354,21 +376,11 @@ public final class Engine extends SubsystemClass {
*/ */
private void collectSubsystems() { private void collectSubsystems() {
ArrayList<@NotNull DependencySubsystemVector> subsystemsMutable = new ArrayList<>(); ArrayList<@NotNull DependencySubsystemVector> subsystemsMutable = new ArrayList<>();
// Scan entire classpath using the Reflections library
Reflections reflections = new Reflections(
new ConfigurationBuilder()
.setUrls(ClasspathHelper.forJavaClassPath())
.setScanners(Scanners.TypesAnnotated)
);
// Get annotated methods
Set<@NotNull Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(EngineSubsystem.class);
// Initialize classes, get dependency vector and add to 'subsystemsMutable'
Object initializedClassRaw; Object initializedClassRaw;
SubsystemClass initializedClass; SubsystemClass initializedClass;
for (Class<?> clazz : annotatedClasses) {
// Check and initialize all classes, get dependency vector and check version, then add to 'subsystemsMutable'
for (Class<?> clazz : getRawSubsystemClasses())
try { try {
// Create new instance // Create new instance
initializedClassRaw = clazz.getDeclaredConstructor().newInstance(); initializedClassRaw = clazz.getDeclaredConstructor().newInstance();
@ -387,12 +399,43 @@ public final class Engine extends SubsystemClass {
logger.crash("Failed to initialize subsystem " + clazz.getName() + ": Invalid version string: " + exception.getMessage().replace("The version string is invalid: ", "")); logger.crash("Failed to initialize subsystem " + clazz.getName() + ": Invalid version string: " + exception.getMessage().replace("The version string is invalid: ", ""));
logger.crash("Failed to initialize subsystem " + clazz.getName() + ": Method invocation error", exception); logger.crash("Failed to initialize subsystem " + clazz.getName() + ": Method invocation error", exception);
} }
}
// Update 'subsystems' // Update 'subsystems'
subsystems = new ImmutableLinkedList<>(subsystemsMutable); subsystems = new ImmutableLinkedList<>(subsystemsMutable);
} }
/**
* Returns a list of classes which are potentially
* eligible for subsystem initialization.
*
* @return potential subsystem classes
* @since v1-alpha5
*/
private Set<@NotNull Class<?>> getRawSubsystemClasses() {
Set<@NotNull Class<?>> classes = new HashSet<>();
if (EngineInternals.getInstance().getReflectiveClasspathScanning()) {
// Scan entire classpath using the Reflections library
Reflections reflections = new Reflections(
new ConfigurationBuilder()
.setUrls(ClasspathHelper.forJavaClassPath())
.setScanners(Scanners.TypesAnnotated)
);
// Get annotated methods
classes = reflections.getTypesAnnotatedWith(EngineSubsystem.class);
} else
for (String path : EngineConfiguration.getInstance().getInitialIncludeSubsystemClasses())
try {
logger.diag("Resolving class " + path);
classes.add(Class.forName(path));
} catch (ClassNotFoundException exception) {
logger.error("Failed loading subsystem class " + path + ": Class not found");
}
return classes;
}
/** /**
* Initializes all subsystems. * Initializes all subsystems.
* *

View file

@ -32,7 +32,10 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
/** /**
* Provides the base engine configuration. * Provides the base engine configuration.
@ -119,6 +122,35 @@ public final class EngineConfiguration extends Configuration {
*/ */
private boolean debugShortcodeConverter; private boolean debugShortcodeConverter;
/**
* If enabled, will force-disable reflective classpath scanning.
*
* @see EngineInternals#getReflectiveClasspathScanning()
* @see EngineInternals#overrideReflectiveClasspathScanning(boolean)
* @since v1-alpha5
* -- GETTER --
* Gets the value for {@link #initialForceDisableClasspathScanning}.
*
* @return variable value
* @see #initialForceDisableClasspathScanning
* @since v1-alpha5
*/
private boolean initialForceDisableClasspathScanning;
/**
* Will try to load the specified classes as subsystems,
* if reflective classpath scanning is disabled.
*
* @since v1-alpha5
* -- GETTER --
* Gets the value for {@link #initialIncludeSubsystemClasses}.
*
* @return variable value
* @see #initialIncludeSubsystemClasses
* @since v1-alpha5
*/
private Set<@NotNull String> initialIncludeSubsystemClasses;
/** /**
* If enabled, invalid shortcodes will be logged by the {@link ShortcodeParser}. * If enabled, invalid shortcodes will be logged by the {@link ShortcodeParser}.
* The message will be printed as a {@link LogLevel#SILENT_WARNING}. * The message will be printed as a {@link LogLevel#SILENT_WARNING}.
@ -322,6 +354,12 @@ public final class EngineConfiguration extends Configuration {
case "debugEvents" -> debugEvents = parser.getBoolean(group + property); case "debugEvents" -> debugEvents = parser.getBoolean(group + property);
case "debugShortcodeConverter" -> debugShortcodeConverter = parser.getBoolean(group + property); case "debugShortcodeConverter" -> debugShortcodeConverter = parser.getBoolean(group + property);
case "initialForceDisableClasspathScanning" -> initialForceDisableClasspathScanning = parser.getBoolean(group + property);
case "initialIncludeSubsystemClasses" -> {
initialIncludeSubsystemClasses = new HashSet<>();
initialIncludeSubsystemClasses.addAll(Arrays.stream(parser.getString(group + property).split(",")).toList());
}
case "errorShortcodeConverter" -> errorShortcodeConverter = parser.getBoolean(group + property); case "errorShortcodeConverter" -> errorShortcodeConverter = parser.getBoolean(group + property);
case "optimizeLogging" -> { case "optimizeLogging" -> {
@ -369,6 +407,9 @@ public final class EngineConfiguration extends Configuration {
debugEvents = false; debugEvents = false;
debugShortcodeConverter = false; debugShortcodeConverter = false;
initialForceDisableClasspathScanning = false;
initialIncludeSubsystemClasses = new HashSet<>();
errorShortcodeConverter = true; errorShortcodeConverter = true;
optimizeLogging = true; optimizeLogging = true;
@ -393,6 +434,9 @@ public final class EngineConfiguration extends Configuration {
case "debugEvents" -> debugEvents; case "debugEvents" -> debugEvents;
case "debugShortcodeConverter" -> debugShortcodeConverter; case "debugShortcodeConverter" -> debugShortcodeConverter;
case "initialForceDisableClasspathScanning" -> initialForceDisableClasspathScanning;
case "initialIncludeSubsystemClasses" -> initialIncludeSubsystemClasses;
case "errorShortcodeConverter" -> errorShortcodeConverter; case "errorShortcodeConverter" -> errorShortcodeConverter;
case "optimizeLogging" -> optimizeLogging; case "optimizeLogging" -> optimizeLogging;

View file

@ -19,9 +19,12 @@
package de.staropensource.sosengine.base; package de.staropensource.sosengine.base;
import de.staropensource.sosengine.base.implementable.ShutdownHandler;
import de.staropensource.sosengine.base.exception.IllegalAccessException; import de.staropensource.sosengine.base.exception.IllegalAccessException;
import de.staropensource.sosengine.base.implementable.EventListenerCode;
import de.staropensource.sosengine.base.implementable.ShutdownHandler;
import de.staropensource.sosengine.base.implementable.helper.EventHelper;
import de.staropensource.sosengine.base.logging.LoggerInstance; import de.staropensource.sosengine.base.logging.LoggerInstance;
import de.staropensource.sosengine.base.type.EventPriority;
import de.staropensource.sosengine.base.type.InternalAccessArea; import de.staropensource.sosengine.base.type.InternalAccessArea;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -72,6 +75,21 @@ public final class EngineInternals {
@Getter @Getter
private final @NotNull List<@NotNull InternalAccessArea> restrictedAreas = new ArrayList<>(); private final @NotNull List<@NotNull InternalAccessArea> restrictedAreas = new ArrayList<>();
/**
* Contains whether the engine should reflectively
* search the classpath for events or other annotations.
* <p>
* If disabled, code will either have to manually call
* registration methods or certain classes have to
* be created in a certain package, depending on the
* use case and application.
*
* @see EventHelper#registerEvent(Class, EventListenerCode)
* @see EventHelper#registerEvent(Class, EventListenerCode, EventPriority)
* @since v1-alpha5
*/
private boolean reflectiveClasspathScanning = true;
/** /**
* Constructs this class. * Constructs this class.
* *
@ -110,10 +128,12 @@ public final class EngineInternals {
areas.remove(InternalAccessArea.ALL); areas.remove(InternalAccessArea.ALL);
areas.remove(InternalAccessArea.ALL_READ); areas.remove(InternalAccessArea.ALL_READ);
areas.remove(InternalAccessArea.ALL_WRITE); areas.remove(InternalAccessArea.ALL_WRITE);
areas.remove(InternalAccessArea.ALL_READ_ESSENTIAL);
restrictedAreas.addAll(areas); restrictedAreas.addAll(areas);
} }
case ALL_READ -> restrictedAreas.addAll(Arrays.stream(InternalAccessArea.valuesReadOnly()).toList());
case ALL_WRITE -> restrictedAreas.addAll(Arrays.stream(InternalAccessArea.valuesWriteOnly()).toList()); case ALL_WRITE -> restrictedAreas.addAll(Arrays.stream(InternalAccessArea.valuesWriteOnly()).toList());
case ALL_READ -> restrictedAreas.addAll(Arrays.stream(InternalAccessArea.valuesReadOnly()).toList());
case ALL_READ_ESSENTIAL -> restrictedAreas.addAll(Arrays.stream(InternalAccessArea.valuesEssentialReadOnly()).toList());
default -> restrictedAreas.add(area); default -> restrictedAreas.add(area);
} }
} }
@ -125,11 +145,11 @@ public final class EngineInternals {
* Highly recommended to keep enabled. * Highly recommended to keep enabled.
* *
* @param status {@code true} to install, {@code false} otherwise * @param status {@code true} to install, {@code false} otherwise
* @throws IllegalAccessException when restricted * @throws IllegalAccessException when restricted ({@link InternalAccessArea#SAFETY_SHUTDOWN_HOOK_UPDATE})
* @since v1-alpha4 * @since v1-alpha4
*/ */
public void installSafetyShutdownHook(boolean status) throws IllegalAccessException { public void installSafetyShutdownHook(boolean status) throws IllegalAccessException {
isRestricted(InternalAccessArea.SAFETY_SHUTDOWN_HOOK); isRestricted(InternalAccessArea.SAFETY_SHUTDOWN_HOOK_UPDATE);
try { try {
if (status) if (status)
@ -139,13 +159,27 @@ public final class EngineInternals {
} catch (IllegalArgumentException | IllegalStateException ignored) {} } catch (IllegalArgumentException | IllegalStateException ignored) {}
} }
/**
* Gets the engine's shutdown handler.
* The shutdown handler is responsible for
* shutting down the JVM safely.
*
* @return shutdown handler
* @throws IllegalAccessException when restricted ({@link InternalAccessArea#SHUTDOWN_HANDLER_GET})
* @since v1-alpha4
*/
public @NotNull ShutdownHandler getShutdownHandler() throws IllegalAccessException {
isRestricted(InternalAccessArea.SHUTDOWN_HANDLER_GET);
return Engine.getInstance().getShutdownHandler();
}
/** /**
* Sets the engine's shutdown handler. * Sets the engine's shutdown handler.
* The shutdown handler is responsible for * The shutdown handler is responsible for
* shutting down the JVM safely. * shutting down the JVM safely.
* *
* @param shutdownHandler new shutdown handler * @param shutdownHandler new shutdown handler
* @throws IllegalAccessException when restricted * @throws IllegalAccessException when restricted ({@link InternalAccessArea#SHUTDOWN_HANDLER_UPDATE})
* @since v1-alpha4 * @since v1-alpha4
*/ */
public void setShutdownHandler(@NotNull ShutdownHandler shutdownHandler) throws IllegalAccessException { public void setShutdownHandler(@NotNull ShutdownHandler shutdownHandler) throws IllegalAccessException {
@ -154,16 +188,47 @@ public final class EngineInternals {
} }
/** /**
* Gets the engine's shutdown handler. * Returns whether the engine should reflectively
* The shutdown handler is responsible for * search the classpath for events or other annotations.
* shutting down the JVM safely. * <p>
* If disabled, code will either have to manually call
* registration methods or certain classes have to
* be created in a certain package, depending on the
* use case and application.
* *
* @return shutdown handler * @return reflective classpath scanning flag state
* @throws IllegalAccessException when restricted * @throws IllegalAccessException when restricted ({@link InternalAccessArea#REFLECTIVE_CLASSPATH_SCANNING_GET})
* @see EventHelper#registerEvent(Class, EventListenerCode)
* @see EventHelper#registerEvent(Class, EventListenerCode, EventPriority)
* @since v1-alpha4 * @since v1-alpha4
*/ */
public @NotNull ShutdownHandler getShutdownHandler() throws IllegalAccessException { public boolean getReflectiveClasspathScanning() throws IllegalAccessException {
isRestricted(InternalAccessArea.SHUTDOWN_HANDLER_GET); isRestricted(InternalAccessArea.REFLECTIVE_CLASSPATH_SCANNING_GET);
return Engine.getInstance().getShutdownHandler(); return reflectiveClasspathScanning;
}
/**
* Overrides whether the engine should reflectively
* search the classpath for events or other annotations.
* <p>
* If disabled, code will either have to manually call
* registration methods or certain classes have to
* be created in a certain package, depending on the
* use case and application.
* <p>
* Enabling reflective classpath scanning in an unsupported
* environment may cause minor to extreme side effects,
* including but not limited to <b>bugs, exceptions, engine
* or even whole JVM crashes</b>. <i>You have been warned!</i>
*
* @param reflectiveClasspathScanning new reflective classpath scanning
* @throws IllegalAccessException when restricted ({@link InternalAccessArea#REFLECTIVE_CLASSPATH_SCANNING_OVERRIDE})
* @see EventHelper#registerEvent(Class, EventListenerCode)
* @see EventHelper#registerEvent(Class, EventListenerCode, EventPriority)
* @since v1-alpha0
*/
public void overrideReflectiveClasspathScanning(boolean reflectiveClasspathScanning) throws IllegalAccessException {
isRestricted(InternalAccessArea.REFLECTIVE_CLASSPATH_SCANNING_OVERRIDE);
this.reflectiveClasspathScanning = reflectiveClasspathScanning;
} }
} }

View file

@ -0,0 +1,51 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.sosengine.base.implementable;
import de.staropensource.sosengine.base.implementable.helper.EventHelper;
import de.staropensource.sosengine.base.type.EventPriority;
import org.jetbrains.annotations.NotNull;
/**
* Used by {@link EventHelper} to execute event listeners.
*
* @see Runnable
* @since v1-alpha5
*/
public abstract class EventListenerCode {
/**
* Contains the priority of this
* event listener.
* <p>
* Set automatically by {@link EventHelper},
* do not change this manually.
*
* @since v1-alpha5
*/
public @NotNull EventPriority priority = EventPriority.DEFAULT;
/**
* Invokes the event listener.
*
* @param arguments arguments passed along by the event
* @since v1-alpha5
*/
public abstract void run(Object... arguments) throws Exception;
}

View file

@ -20,6 +20,7 @@
package de.staropensource.sosengine.base.implementable.helper; package de.staropensource.sosengine.base.implementable.helper;
import de.staropensource.sosengine.base.EngineConfiguration; import de.staropensource.sosengine.base.EngineConfiguration;
import de.staropensource.sosengine.base.EngineInternals;
import de.staropensource.sosengine.base.annotation.EventListener; import de.staropensource.sosengine.base.annotation.EventListener;
import de.staropensource.sosengine.base.event.LogEvent; import de.staropensource.sosengine.base.event.LogEvent;
import de.staropensource.sosengine.base.exception.reflection.InstanceMethodFromStaticContextException; import de.staropensource.sosengine.base.exception.reflection.InstanceMethodFromStaticContextException;
@ -27,9 +28,10 @@ import de.staropensource.sosengine.base.exception.reflection.InvalidMethodSignat
import de.staropensource.sosengine.base.exception.reflection.NoAccessException; import de.staropensource.sosengine.base.exception.reflection.NoAccessException;
import de.staropensource.sosengine.base.exception.reflection.StaticInitializerException; import de.staropensource.sosengine.base.exception.reflection.StaticInitializerException;
import de.staropensource.sosengine.base.implementable.Event; import de.staropensource.sosengine.base.implementable.Event;
import de.staropensource.sosengine.base.implementable.EventListenerCode;
import de.staropensource.sosengine.base.internal.implementation.EventListenerMethod;
import de.staropensource.sosengine.base.logging.LoggerInstance; import de.staropensource.sosengine.base.logging.LoggerInstance;
import de.staropensource.sosengine.base.reflection.Reflect; import de.staropensource.sosengine.base.type.EventPriority;
import de.staropensource.sosengine.base.reflection.ReflectionMethod;
import de.staropensource.sosengine.base.utility.ListFormatter; import de.staropensource.sosengine.base.utility.ListFormatter;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -41,10 +43,7 @@ import org.reflections.util.ConfigurationBuilder;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Comparator; import java.util.*;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Set;
/** /**
* Simplifies event logging and calling. * Simplifies event logging and calling.
@ -53,14 +52,6 @@ import java.util.Set;
*/ */
@Getter @Getter
public final class EventHelper { public final class EventHelper {
/**
* Holds all cached events.
* Should not be modified manually.
*
* @since v1-alpha0
*/
private static final @NotNull HashMap<@NotNull Class<? extends Event>, LinkedList<@NotNull ReflectionMethod>> cachedEventListeners = new HashMap<>();
/** /**
* Contains the {@link LoggerInstance} for this instance. * Contains the {@link LoggerInstance} for this instance.
* *
@ -69,60 +60,111 @@ public final class EventHelper {
*/ */
private static final @NotNull LoggerInstance logger = new LoggerInstance.Builder().setClazz(EventHelper.class).setOrigin("ENGINE").build(); private static final @NotNull LoggerInstance logger = new LoggerInstance.Builder().setClazz(EventHelper.class).setOrigin("ENGINE").build();
/**
* Holds all cached events.
*
* @since v1-alpha5
*/
private static final @NotNull Map<@NotNull Class<? extends Event>, LinkedList<@NotNull EventListenerCode>> cachedEventListeners = new HashMap<>();
/** /**
* Constructs this class. * Constructs this class.
* *
* @since v1-alpha0 * @since v1-alpha0
*/ */
public EventHelper() {} private EventHelper() {}
/** /**
* Returns all {@link EventListener}s listening on some event. * Registers a new {@link Event}.
* The classpath will be scanned for listeners, unless cached results exist and {@code !forceScanning}. * <p>
* This method does nothing if classpath searching is disabled.
* *
* @param event event class * @param event {@link Event} to register for
* @param forceScanning forces scanning the classpath for listeners. not recommended due to a huge performance penalty * @param eventListener {@link EventListenerCode} to register
* @return list of event listeners * @param priority priority of the listener
* @see #cacheEvent(Class) * @see EngineInternals#getReflectiveClasspathScanning()
* @since v1-alpha0 * @since v1-alpha5
*/ */
public static @NotNull LinkedList<ReflectionMethod> getAnnotatedMethods(@NotNull Class<? extends Event> event, boolean forceScanning) { public static synchronized void registerEvent(@NotNull Class<? extends Event> event, @NotNull EventListenerCode eventListener, @NotNull EventPriority priority) {
LinkedList<ReflectionMethod> methods = new LinkedList<>(); if (EngineInternals.getInstance().getReflectiveClasspathScanning())
return;
if (forceScanning || !cachedEventListeners.containsKey(event)) { // Update 'eventListener' priority
Reflections reflections = new Reflections( eventListener.priority = priority;
new ConfigurationBuilder()
.setUrls(ClasspathHelper.forJavaClassPath())
.setScanners(Scanners.MethodsAnnotated)
);
// Get annotated methods // Check if event already exists in map
Set<@NotNull Method> annotatedMethods = reflections.getMethodsAnnotatedWith(EventListener.class); // If not, create entry with a LinkedList
if (cachedEventListeners.containsKey(event))
if (cachedEventListeners.get(event).contains(eventListener))
return;
else
cachedEventListeners.get(event).add(eventListener);
else {
LinkedList<@NotNull EventListenerCode> list = new LinkedList<>();
list.add(eventListener);
cachedEventListeners.put(event, list);
}
// Sort event listeners not listening for this event out logger.diag("Registered event listener " + eventListener + " for event " + event + " with priority " + priority.name());
for (Method method : annotatedMethods)
if (method.getAnnotation(EventListener.class).event() == event)
methods.add(Reflect.reflectOn(method));
// Sort 'methods' linked list after event priority
methods.sort(Comparator.comparing(method -> method.getAnnotation(EventListener.class).priority()));
} else
// Event listeners are cached and !forceScanning, return cached results
methods = cachedEventListeners.get(event);
return methods;
} }
/** /**
* Returns all {@link EventListener}s listening on some event. * Registers a new {@link Event}.
* The classpath will be scanned for listeners, unless cached results exist. * <p>
* This method does nothing if classpath searching is disabled.
* *
* @param event event class * @param event {@link Event} to register for
* @return list of event listeners * @param eventListener {@link EventListenerCode} to register
* @since v1-alpha0 * @see EngineInternals#getReflectiveClasspathScanning()
* @since v1-alpha5
*/ */
public static @NotNull LinkedList<ReflectionMethod> getAnnotatedMethods(@NotNull Class<? extends Event> event) { public static void registerEvent(@NotNull Class<? extends Event> event, @NotNull EventListenerCode eventListener) {
return getAnnotatedMethods(event, false); registerEvent(event, eventListener, EventPriority.DEFAULT);
}
/**
* (Re-)Caches all event listeners for some {@link Event}.
* <p>
* This method does nothing if classpath searching is enabled.
*
* @param event event to (re-)cache. Set to {@code null} to recompute all cached events
* @see EngineInternals#getReflectiveClasspathScanning()
* @since v1-alpha5
*/
public static synchronized void cacheEvent(@Nullable Class<? extends Event> event) {
if (!EngineInternals.getInstance().getReflectiveClasspathScanning())
return;
if (event == null)
for (Class<? extends Event> cachedEvent : cachedEventListeners.keySet())
cacheEvent(cachedEvent);
else {
LinkedList<@NotNull EventListenerCode> annotatedMethods = getAnnotatedMethods(event);
if (cachedEventListeners.containsKey(event))
cachedEventListeners.replace(event, annotatedMethods);
else
cachedEventListeners.put(event, annotatedMethods);
}
}
/**
* Removes an event from the event listener cache.
* <p>
* This method does nothing if classpath searching is enabled.
*
* @param event event to uncache. Set to {@code null} to clear the entire cache
* @see EngineInternals#getReflectiveClasspathScanning()
* @since v1-alpha5
*/
public static synchronized void uncacheEvent(@Nullable Class<? extends Event> event) {
if (!EngineInternals.getInstance().getReflectiveClasspathScanning())
return;
if (event == null)
cachedEventListeners.clear();
else
cachedEventListeners.remove(event);
} }
/** /**
@ -130,7 +172,7 @@ public final class EventHelper {
* *
* @param event event class * @param event event class
* @param arguments arguments to pass to event listeners * @param arguments arguments to pass to event listeners
* @since v1-alpha0 * @since v1-alpha5
*/ */
public static void invokeAnnotatedMethods(@NotNull Class<? extends Event> event, Object... arguments) { public static void invokeAnnotatedMethods(@NotNull Class<? extends Event> event, Object... arguments) {
if (event != LogEvent.class && EngineConfiguration.getInstance().isDebugEvents()) if (event != LogEvent.class && EngineConfiguration.getInstance().isDebugEvents())
@ -140,21 +182,21 @@ public final class EventHelper {
logger.diag("Event " + event.getName() + " was emitted, passing arguments " + ListFormatter.formatArray(arguments)); logger.diag("Event " + event.getName() + " was emitted, passing arguments " + ListFormatter.formatArray(arguments));
Runnable eventCode = () -> { Runnable eventCode = () -> {
for (ReflectionMethod method : getAnnotatedMethods(event)) { for (EventListenerCode eventListener : getAnnotatedMethods(event)) {
try { try {
method.invoke(arguments); eventListener.run(arguments);
} catch (NoAccessException exception) { } catch (NoAccessException exception) {
logger.warn("Event listener method " + method.getName() + " could not be called as the method could not be accessed"); logger.warn("Event listener " + eventListener + " could not be called as the method could not be accessed");
} catch (InvalidMethodSignatureException exception) { } catch (InvalidMethodSignatureException exception) {
logger.warn("Event listener method " + method.getName() + " has an invalid method signature"); logger.warn("Event listener " + eventListener + " has an invalid method signature");
} catch (InvocationTargetException exception) { } catch (InvocationTargetException exception) {
logger.crash("Event listener method " + method.getName() + " threw a throwable", exception.getTargetException(), true); logger.crash("Event listener " + eventListener + " threw a throwable", exception.getTargetException(), true);
} catch (InstanceMethodFromStaticContextException exception) { } catch (InstanceMethodFromStaticContextException exception) {
logger.warn("Event listener method " + method.getName() + " is not static. Event listener methods must be static for them to be called."); logger.warn("Event listener " + eventListener + " is not static. Event listener methods must be static for them to be called.");
} catch (StaticInitializerException exception) { } catch (StaticInitializerException exception) {
logger.crash("Event listener method " + method.getName() + " could not be called as the static initializer failed", exception.getThrowable(), true); logger.crash("Event listener " + eventListener + " could not be called as the static initializer failed", exception.getThrowable(), true);
} catch (Exception exception) { } catch (Exception exception) {
logger.crash("Event listener method " + method.getName() + " could not be called as an error occurred during reflection", exception, true); logger.crash("Event listener " + eventListener + " could not be called as an error occurred during reflection", exception, true);
} }
} }
}; };
@ -169,35 +211,54 @@ public final class EventHelper {
} }
/** /**
* (Re-)Caches all event listeners for some {@link Event}. * Returns all {@link EventListener}s listening on some event.
* The classpath will be scanned for listeners, unless cached results exist and {@code !forceScanning}.
* *
* @param event event to (re-)cache. Set to {@code null} to recompute all cached events * @param event event class
* @since v1-alpha0 * @param forceScanning forces scanning the classpath for listeners. not recommended due to a huge performance penalty
* @return list of event listeners
* @see #cacheEvent(Class)
* @since v1-alpha5
*/ */
public static synchronized void cacheEvent(@Nullable Class<? extends Event> event) { public static @NotNull LinkedList<EventListenerCode> getAnnotatedMethods(@NotNull Class<? extends Event> event, boolean forceScanning) {
if (event == null) LinkedList<EventListenerCode> eventListeners = new LinkedList<>();
for (Class<? extends Event> cachedEvent : cachedEventListeners.keySet())
cacheEvent(cachedEvent);
else {
LinkedList<@NotNull ReflectionMethod> annotatedMethods = getAnnotatedMethods(event);
if (cachedEventListeners.containsKey(event)) if (!EngineInternals.getInstance().getReflectiveClasspathScanning())
cachedEventListeners.replace(event, annotatedMethods); return Objects.requireNonNullElse(cachedEventListeners.get(event), eventListeners);
else
cachedEventListeners.put(event, annotatedMethods); if (forceScanning || !cachedEventListeners.containsKey(event)) {
} Reflections reflections = new Reflections(
new ConfigurationBuilder()
.setUrls(ClasspathHelper.forJavaClassPath())
.setScanners(Scanners.MethodsAnnotated)
);
// Get annotated methods
Set<@NotNull Method> annotatedMethods = reflections.getMethodsAnnotatedWith(EventListener.class);
// Sort event listeners not listening for the specified event out
for (Method method : annotatedMethods)
if (method.getAnnotation(EventListener.class).event() == event)
eventListeners.add(new EventListenerMethod(method));
// Sort list after event priority
eventListeners.sort(Comparator.comparing(method -> Objects.requireNonNull(((EventListenerMethod) method).getAnnotation(EventListener.class)).priority()));
} else
// Event listeners are cached and !forceScanning, return cached results
eventListeners = cachedEventListeners.get(event);
return eventListeners;
} }
/** /**
* Removes an event from the event listener cache. * Returns all {@link EventListener}s listening on some event.
* The classpath will be scanned for listeners, unless cached results exist.
* *
* @param event event to uncache. Set to {@code null} to clear the entire cache * @param event event class
* @since v1-alpha0 * @return list of event listeners
* @since v1-alpha5
*/ */
public static synchronized void uncacheEvent(@Nullable Class<? extends Event> event) { public static @NotNull LinkedList<EventListenerCode> getAnnotatedMethods(@NotNull Class<? extends Event> event) {
if (event == null) return getAnnotatedMethods(event, false);
cachedEventListeners.clear();
else
cachedEventListeners.remove(event);
} }
} }

View file

@ -0,0 +1,81 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.sosengine.base.internal.implementation;
import de.staropensource.sosengine.base.implementable.EventListenerCode;
import de.staropensource.sosengine.base.reflection.Reflect;
import de.staropensource.sosengine.base.reflection.ReflectionMethod;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* Interface specifically for executing event listener methods.
*
* @since v1-alpha0
*/
public final class EventListenerMethod extends EventListenerCode {
/**
* Contains the method to call and get.
*
* @since v1-alpha0
*/
private final @NotNull ReflectionMethod method;
/**
* Constructs this class.
*
* @since v1-alpha0
*/
public EventListenerMethod(@NotNull Method method) {
this.method = Reflect.reflectOn(method);
}
/** {@inheritDoc} */
@Override
public void run(Object[] arguments) throws Exception {
method.invoke(arguments);
}
/**
* Forwards {@link ReflectionMethod#getAnnotation(Class)}
* to the internal {@link ReflectionMethod} instance.
*
* @param annotation annotation to get
* @return annotation or {@code null} on error
* @see ReflectionMethod#getAnnotation(Class)
* @since v1-alpha0
*/
public <T extends Annotation> @Nullable T getAnnotation(@NotNull Class<T> annotation) {
try {
return method.getAnnotation(annotation);
} catch (NullPointerException exception) {
return null;
}
}
/** {@inheritDoc} */
@Override
public String toString() {
return "method " + method.getMethod().getDeclaringClass().getName() + "#" + method.getName();
}
}

View file

@ -0,0 +1,25 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Interfaces and abstract classes which can be used for implementing classes.
*
* @since v1-alpha0
*/
package de.staropensource.sosengine.base.internal.implementation;

View file

@ -20,6 +20,7 @@
package de.staropensource.sosengine.base.type; package de.staropensource.sosengine.base.type;
import de.staropensource.sosengine.base.EngineInternals; import de.staropensource.sosengine.base.EngineInternals;
import de.staropensource.sosengine.base.implementable.Event;
import de.staropensource.sosengine.base.implementable.ShutdownHandler; import de.staropensource.sosengine.base.implementable.ShutdownHandler;
/** /**
@ -51,13 +52,23 @@ public enum InternalAccessArea {
*/ */
ALL_READ, ALL_READ,
/**
* Refers to all essential read-only areas.
* <p>
* Essential read-only areas are IIAs which are
* very important and should not be restricted.
*
* @since v1-alpha5
*/
ALL_READ_ESSENTIAL,
/** /**
* Refers to the toggling of the JVM shutdown hook, which * Refers to the toggling of the JVM shutdown hook, which
* prevents JVM shutdowns without the engine first shutting down. * prevents JVM shutdowns without the engine first shutting down.
* *
* @since v1-alpha4 * @since v1-alpha4
*/ */
SAFETY_SHUTDOWN_HOOK, SAFETY_SHUTDOWN_HOOK_UPDATE,
/** /**
* Refers to the getting of the engine's shutdown handler. * Refers to the getting of the engine's shutdown handler.
@ -75,7 +86,23 @@ public enum InternalAccessArea {
* *
* @since v1-alpha4 * @since v1-alpha4
*/ */
SHUTDOWN_HANDLER_UPDATE; SHUTDOWN_HANDLER_UPDATE,
/**
* Refers to the getting of the flag controlling whether
* automatic {@link Event} classpath searching should be performed.
*
* @since v1-alpha5
*/
REFLECTIVE_CLASSPATH_SCANNING_GET,
/**
* Refers to the overriding of the flag controlling whether
* automatic {@link Event} classpath searching should be performed.
*
* @since v1-alpha5
*/
REFLECTIVE_CLASSPATH_SCANNING_OVERRIDE;
/** /**
* Returns all read-only areas. * Returns all read-only areas.
@ -89,6 +116,18 @@ public enum InternalAccessArea {
}; };
} }
/**
* Returns all essential read-only areas.
*
* @return array containing all essential read-only areas
* @since v1-alpha5
*/
public static InternalAccessArea[] valuesEssentialReadOnly() {
return new InternalAccessArea[]{
REFLECTIVE_CLASSPATH_SCANNING_GET,
};
}
/** /**
* Returns all write-only areas. * Returns all write-only areas.
* *
@ -97,8 +136,9 @@ public enum InternalAccessArea {
*/ */
public static InternalAccessArea[] valuesWriteOnly() { public static InternalAccessArea[] valuesWriteOnly() {
return new InternalAccessArea[]{ return new InternalAccessArea[]{
SAFETY_SHUTDOWN_HOOK, SAFETY_SHUTDOWN_HOOK_UPDATE,
SHUTDOWN_HANDLER_UPDATE, SHUTDOWN_HANDLER_UPDATE,
REFLECTIVE_CLASSPATH_SCANNING_OVERRIDE,
}; };
} }
} }

View file

@ -81,6 +81,10 @@ application {
// Force writing to standard output // Force writing to standard output
"-Dsosengine.base.loggerForceStandardOutput=true", "-Dsosengine.base.loggerForceStandardOutput=true",
// Pass classes which should be included if
// reflective sclasspath scanning is disabled.
"-Dsosengine.base.initialIncludeSubsystemClasses=de.staropensource.sosengine.ansi.AnsiSubsystem,de.staropensource.sosengine.slf4j_compat.Slf4jCompatSubsystem,de.staropensource.sosengine.windowing.glfw.GlfwSubsystem",
// Force Jansi to write escape sequences // Force Jansi to write escape sequences
"-Djansi.mode=force", "-Djansi.mode=force",
] ]
@ -121,7 +125,9 @@ tasks.register('runNativeImage', Exec) {
args( args(
"-Dsosengine.base.loggerLevel=diagnostic", "-Dsosengine.base.loggerLevel=diagnostic",
"-Dsosengine.base.loggerForceStandardOutput=true", "-Dsosengine.base.loggerForceStandardOutput=true",
"-Djansi.mode=force" "-Dsosengine.base.initialForceDisableClasspathScanning=true",
"-Dsosengine.base.initialIncludeSubsystemClasses=de.staropensource.sosengine.ansi.AnsiSubsystem,de.staropensource.sosengine.slf4j_compat.Slf4jCompatSubsystem,de.staropensource.sosengine.windowing.glfw.GlfwSubsystem",
"-Djansi.mode=force",
) )
executable("build/bin/sosengine-testapp") executable("build/bin/sosengine-testapp")
} }

View file

@ -21,6 +21,8 @@ package de.staropensource.sosengine.testapp;
import de.staropensource.sosengine.base.Engine; import de.staropensource.sosengine.base.Engine;
import de.staropensource.sosengine.base.annotation.EventListener; import de.staropensource.sosengine.base.annotation.EventListener;
import de.staropensource.sosengine.base.implementable.EventListenerCode;
import de.staropensource.sosengine.base.implementable.helper.EventHelper;
import de.staropensource.sosengine.base.logging.LoggerInstance; import de.staropensource.sosengine.base.logging.LoggerInstance;
import de.staropensource.sosengine.base.type.vector.Vec2i; import de.staropensource.sosengine.base.type.vector.Vec2i;
import de.staropensource.sosengine.base.utility.Miscellaneous; import de.staropensource.sosengine.base.utility.Miscellaneous;
@ -113,9 +115,25 @@ public final class Main {
@SneakyThrows @SneakyThrows
public void run() { public void run() {
try { try {
// Specify subsystems to load
System.setProperty(
"sosengine.base.initialIncludeSubsystemClasses", (
System.getProperty("sosengine.base.initialIncludeSubsystemClasses") == null
? "" : System.getProperty("sosengine.base.initialIncludeSubsystemClasses") + ","
) + "de.staropensource.sosengine.windowing.WindowingSubsystem"
);
// Initialize sos!engine // Initialize sos!engine
engine = new Engine(); engine = new Engine();
// Register events
EventHelper.registerEvent(InputEvent.class, new EventListenerCode() {
@Override
public void run(Object... arguments) {
onInput((Window) arguments[0], (Key) arguments[1], (KeyState) arguments[2]);
}
});
// Say hello to the world! // Say hello to the world!
logger.info("Hello world!"); logger.info("Hello world!");