3939import build .codemodel .foundation .naming .NonCachingNameProvider ;
4040import build .codemodel .injection .ConfigurationResolver ;
4141import build .codemodel .injection .Context ;
42- import build .codemodel .injection .InjectionException ;
4342import build .codemodel .injection .InjectionFramework ;
4443import build .codemodel .jdk .JDKCodeModel ;
4544import build .spawn .application .option .LaunchIdentity ;
4645
47- import java .io .BufferedReader ;
4846import java .io .IOException ;
49- import java .io .InputStreamReader ;
50- import java .io .LineNumberReader ;
5147import java .lang .reflect .Modifier ;
52- import java .net .URL ;
5348import java .util .ArrayDeque ;
54- import java .util .Collections ;
5549import java .util .LinkedHashMap ;
5650import java .util .LinkedList ;
57- import java .util .List ;
5851import java .util .Map ;
5952import java .util .Objects ;
60- import java .util .Optional ;
53+ import java .util .ServiceLoader ;
6154import java .util .concurrent .atomic .AtomicLong ;
6255import java .util .stream .Collectors ;
6356import java .util .stream .Stream ;
6457
6558/**
6659 * An abstract {@link TemplatedPlatform} that automatically detects the supported {@link Class}es of
67- * {@link Application}s and associated {@link Launcher}s by loading and parsing properties files
68- * (named using the concrete implementation of this {@link Class}) located in {@code META-INF/} folders on the
69- * classpath.
60+ * {@link Application}s and associated {@link Launcher}s via {@link java.util.ServiceLoader} discovery of
61+ * {@link LauncherRegistration} providers.
7062 * <p>
71- * The properties files define key-value pairs where each key is a fully-qualified-class-name for a {@link Class}
72- * of {@link Application} and the corresponding value is the {@link Class} of {@link Launcher} to launch
73- * said {@link Class}es of {@link Application} with the {@link Platform}.
74- * <p>
75- * For example, the {@code META-INF/build.spawn.local.LocalMachine} properties file for the
76- * {@code LocalMachine} defines the following entries.
77- * <code>
78- * build.spawn.application.Application=build.spawn.local.LocalLauncher
79- * build.spawn.application.java.JavaApplication=build.spawn.java.LocalJavaLauncher
80- * </code>
63+ * Modules that wish to register a {@link Launcher} for a specific platform declare a {@link LauncherRegistration}
64+ * implementation and register it with {@code provides LauncherRegistration with ...} in their {@code module-info}.
8165 *
8266 * @author brian.oliver
8367 * @since Oct-2018
@@ -104,7 +88,7 @@ public abstract class AbstractTemplatedPlatform
10488 * The {@link Launcher}s by {@link Class} of {@link Application} that are supported
10589 * for this {@link Platform}.
10690 */
107- private final LinkedHashMap <Class <? extends Application >, Class <? extends Launcher >> launchers ;
91+ private final LinkedHashMap <Class <? extends Application >, Class <? extends Launcher <?, ?> >> launchers ;
10892
10993 /**
11094 * The {@link Server} that {@link Application}s may use to communicate with this {@link Platform}.
@@ -157,107 +141,14 @@ public AbstractTemplatedPlatform(final String name,
157141 throw new RuntimeException ("Failed to create " + getClass ().getCanonicalName (), e );
158142 }
159143
160- // determine the launchers supported by this platform
144+ // determine the launchers supported by this platform via ServiceLoader discovery
161145 this .launchers = new LinkedHashMap <>();
162146
163- // attempt to load the properties file defining the launchers for this class of platform
164- final String path = "META-INF/" + getClass ().getName ();
165-
166- final String platformClassName = getClass ().getCanonicalName ();
167- LOGGER .debug ("Locating Application Launchers for {} in {} (as System Resources)" , platformClassName , path );
168-
169- try {
170- // determine the properties files defining launchers for the concrete type of platform
171- final List <URL > resources = Collections .list (ClassLoader .getSystemResources (path ));
172-
173- LOGGER .debug ("Located {} System Resource(s) defining Application Launchers" , resources .size ());
174-
175- // attempt to find the Launcher for the class of Application
176- for (final URL resource : resources ) {
177-
178- LOGGER .debug ("Loading Application Launcher(s) from {}" , resource );
179-
180- try (LineNumberReader reader = new LineNumberReader (
181- new BufferedReader (new InputStreamReader (resource .openStream ())))) {
182-
183- String line ;
184- while ((line = reader .readLine ()) != null ) {
185-
186- line = Strings .trim (line );
187-
188- if (!line .startsWith ("#" ) && !Strings .isEmpty (line )) {
189- final String [] parts = line .split ("=" );
190-
191- // the optionally detected name of the class for launching the application
192- Optional <String > launcherClassName = Optional .empty ();
193-
194- // by default we assume the specified launcher is for all classes of Application
195- Class <? extends Application > applicationClass = Application .class ;
196-
197- if (parts .length == 1 ) {
198- // when there's only a single parameter, assume that is the launcher
199- launcherClassName = Optional .of (Strings .trim (parts [0 ]));
200- } else if (parts .length == 2 ) {
201- // when there's a "key=value", the key is the application and the value is the launcher
202- final String specifiedClassName = Strings .trim (parts [0 ]);
203-
204- try {
205- final Class <?> specifiedClass = Class .forName (specifiedClassName );
206-
207- if (Application .class .isAssignableFrom (specifiedClass )) {
208- applicationClass = (Class <? extends Application >) specifiedClass ;
209- launcherClassName = Optional .of (Strings .trim (parts [1 ]));
210- } else {
211- LOGGER .warn ("The specified class [{}] in [{}] at line [{}] is not an {}." ,
212- launcherClassName .get (), resource , reader .getLineNumber (),
213- Application .class .getCanonicalName ());
214- }
215- } catch (final ClassNotFoundException e ) {
216- LOGGER .warn ("The specified class [{}] in [{}] at line [{}] is not found." ,
217- specifiedClassName , resource , reader .getLineNumber ());
218- }
219- } else {
220- // the file isn't formatted correctly!
221- LOGGER .warn (
222- "Invalid property definition detected in [{}] for [{}] at line [{}].\n Line:{}" ,
223- resource , platformClassName , reader .getLineNumber (), line );
224- }
225-
226- if (launcherClassName .isPresent ()) {
227- try {
228- final Class <?> launcherClass = Class .forName (launcherClassName .get ());
229-
230- if (Launcher .class .isAssignableFrom (launcherClass )) {
231-
232- LOGGER .trace ("[{}] classes will use [{}] for launching on [{}]" ,
233- applicationClass , launcherClass , platformClassName );
234-
235- // remember the launcher for this platform
236- this .launchers .put (applicationClass ,
237- (Class <? extends Launcher >) launcherClass );
238- } else {
239- LOGGER .warn ("The specified class [{}] in [{}] at line [{}] is not a {}" ,
240- launcherClassName .get (), resource , reader .getLineNumber (),
241- Launcher .class );
242- }
243- } catch (final InjectionException e ) {
244- LOGGER .warn (
245- "The specified class [{}] in [{}] at line [{}] could not be created. The"
246- + " launcher will not be available" , launcherClassName .get (), resource ,
247- reader .getLineNumber (), e );
248- } catch (final ClassNotFoundException e ) {
249- LOGGER .warn ("The specified class [{}] in [{}] at line [{}] is not found. The "
250- + "launcher will not be available" , launcherClassName .get (), resource ,
251- reader .getLineNumber (), e );
252- }
253- }
254- }
255- }
256- }
257- }
258- } catch (final IOException e ) {
259- LOGGER .warn ("Failed to determine System Resources for [{}]" , path , e );
260- }
147+ ServiceLoader .load (LauncherRegistration .class , getClass ().getClassLoader ())
148+ .stream ()
149+ .map (ServiceLoader .Provider ::get )
150+ .filter (reg -> reg .platformClass ().equals (getClass ()))
151+ .forEach (reg -> this .launchers .put (reg .applicationClass (), reg .launcherClass ()));
261152 }
262153
263154 @ Override
@@ -430,15 +321,15 @@ public <A extends Application> A launch(final Specification<A> specification) {
430321 // determine the class of Launcher for the Application
431322 //
432323 // IMPORTANT: use min() to select the most specific (most derived) registered application class,
433- // NOT findFirst(). The launchers map is a LinkedHashMap populated by iterating META-INF resource
434- // files in classpath order. Using findFirst() meant that whichever jar appeared earlier on the
435- // classpath determined the launcher — e.g. if spawn-local-platform appeared before spawn-local-jdk,
324+ // NOT findFirst(). The launchers map is a LinkedHashMap populated by iterating ServiceLoader
325+ // registrations in discovery order. Using findFirst() meant that whichever module was discovered
326+ // first determined the launcher — e.g. if spawn-local-platform was discovered before spawn-local-jdk,
436327 // then Application→LocalLauncher would win over JDKApplication→LocalJDKLauncher for any
437328 // JDKApplication subclass. This caused silent, order-dependent failures that were extremely hard
438329 // to diagnose (the symptom was "Failed to provide or determine the Executable to launch" or
439330 // the JVM receiving program arguments as JVM flags). Using min() with class hierarchy ordering
440- // ensures the most derived registered type always wins, regardless of classpath order.
441- final Class <? extends Launcher > launcherClass = this .launchers .entrySet ()
331+ // ensures the most derived registered type always wins, regardless of discovery order.
332+ final Class <? extends Launcher <?, ?> > launcherClass = this .launchers .entrySet ()
442333 .stream ()
443334 .filter (e -> e .getKey ().isAssignableFrom (applicationClass ))
444335 .min ((a , b ) -> a .getKey ().isAssignableFrom (b .getKey ()) ? 1 : -1 )
@@ -449,7 +340,8 @@ public <A extends Application> A launch(final Specification<A> specification) {
449340 + applicationClass .getCanonicalName ()));
450341
451342 // establish the Application.Launcher
452- final Launcher <A , Platform > launcher = context .create (launcherClass );
343+ @ SuppressWarnings ("unchecked" )
344+ final Launcher <A , Platform > launcher = (Launcher <A , Platform >) context .create (launcherClass );
453345
454346 // establish the launch Configuration
455347 final var launchConfiguration = launchConfigurationBuilder .build ();
0 commit comments