ApplicationLoader.java
package com.cloudforgeci.api.compute;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.annotation.ApplicationPlugin;
import java.util.*;
import java.util.stream.Collectors;
/**
* Auto-discovery utility for ApplicationSpec implementations using Java ServiceLoader.
*
* <p>This class discovers all ApplicationSpec implementations registered via
* META-INF/services/com.cloudforge.core.interfaces.ApplicationSpec and provides
* convenient access methods for deployment tools.</p>
*
* <p>Applications are discovered from:</p>
* <ul>
* <li>Built-in applications in cloudforge-api module</li>
* <li>External plugins providing META-INF/services registration</li>
* </ul>
*
* <h2>Usage Example:</h2>
* <pre>{@code
* // Discover all applications
* Map<String, ApplicationSpec> apps = ApplicationLoader.discover();
*
* // Get applications by category
* List<ApplicationSpec> cicdApps = ApplicationLoader.discoverByCategory("cicd");
*
* // Find specific application
* Optional<ApplicationSpec> jenkins = ApplicationLoader.findById("jenkins");
* }</pre>
*
* @since 3.0.0
*/
public final class ApplicationLoader {
private ApplicationLoader() {
// Utility class
}
/**
* Discover all ApplicationSpec implementations via ServiceLoader.
*
* <p>Returns a map of application ID to ApplicationSpec instance. The application ID
* is obtained from {@link ApplicationSpec#applicationId()}.</p>
*
* <p>Applications without {@link ApplicationPlugin} annotation are still included
* but will have default metadata values.</p>
*
* @return map of application ID to ApplicationSpec (never null, may be empty)
*/
public static Map<String, ApplicationSpec> discover() {
ServiceLoader<ApplicationSpec> loader = ServiceLoader.load(ApplicationSpec.class);
Map<String, ApplicationSpec> applications = new LinkedHashMap<>();
for (ApplicationSpec spec : loader) {
String id = spec.applicationId();
if (applications.containsKey(id)) {
System.err.println("WARNING: Duplicate application ID '" + id + "' - using first registration");
continue;
}
applications.put(id, spec);
}
return applications;
}
/**
* Discover all ApplicationSpec implementations as a list.
*
* <p>Applications are sorted alphabetically by application ID for consistent ordering.</p>
*
* @return list of ApplicationSpec instances (never null, may be empty)
*/
public static List<ApplicationSpec> discoverAsList() {
return discover().values().stream()
.sorted(Comparator.comparing(ApplicationSpec::applicationId))
.collect(Collectors.toList());
}
/**
* Discover ApplicationSpec implementations filtered by category.
*
* <p>Categories are defined in the {@link ApplicationPlugin} annotation:</p>
* <ul>
* <li>cicd - CI/CD platforms</li>
* <li>vcs - Version control systems</li>
* <li>monitoring - Monitoring and observability</li>
* <li>analytics - Business intelligence</li>
* <li>database - Databases and caching</li>
* <li>artifactregistry - Artifact repositories</li>
* <li>secrets - Secrets management</li>
* <li>collaboration - Team collaboration</li>
* </ul>
*
* @param category the category to filter by
* @return list of ApplicationSpec instances in this category
*/
public static List<ApplicationSpec> discoverByCategory(String category) {
return discoverAsList().stream()
.filter(spec -> spec.category().equals(category))
.collect(Collectors.toList());
}
/**
* Discover all available categories.
*
* <p>Returns a sorted list of unique categories from all discovered applications.</p>
*
* @return sorted list of category names
*/
public static List<String> discoverCategories() {
return discoverAsList().stream()
.map(ApplicationSpec::category)
.distinct()
.sorted()
.collect(Collectors.toList());
}
/**
* Find a specific ApplicationSpec by application ID.
*
* @param applicationId the application identifier (e.g., "jenkins", "gitlab")
* @return Optional containing the ApplicationSpec if found
*/
public static Optional<ApplicationSpec> findById(String applicationId) {
return Optional.ofNullable(discover().get(applicationId));
}
/**
* Get applications grouped by category.
*
* <p>Returns a map where keys are category names and values are lists of
* ApplicationSpec instances in that category.</p>
*
* @return map of category to list of applications
*/
public static Map<String, List<ApplicationSpec>> discoverGroupedByCategory() {
return discoverAsList().stream()
.collect(Collectors.groupingBy(
ApplicationSpec::category,
TreeMap::new,
Collectors.toList()
));
}
/**
* Discover applications that support OIDC integration.
*
* @return list of ApplicationSpec instances supporting OIDC
*/
public static List<ApplicationSpec> discoverOidcEnabled() {
return discoverAsList().stream()
.filter(ApplicationSpec::supportsOidcIntegration)
.collect(Collectors.toList());
}
/**
* Discover applications that support Fargate deployment.
*
* @return list of ApplicationSpec instances supporting Fargate
*/
public static List<ApplicationSpec> discoverFargateSupported() {
return discoverAsList().stream()
.filter(ApplicationSpec::supportsFargate)
.collect(Collectors.toList());
}
/**
* Discover applications that support EC2 deployment.
*
* @return list of ApplicationSpec instances supporting EC2
*/
public static List<ApplicationSpec> discoverEc2Supported() {
return discoverAsList().stream()
.filter(ApplicationSpec::supportsEc2)
.collect(Collectors.toList());
}
/**
* Print a formatted catalog of all discovered applications.
*
* <p>Useful for debugging and displaying available applications to users.</p>
*
* @return formatted string containing application catalog
*/
public static String printCatalog() {
StringBuilder sb = new StringBuilder();
sb.append("CloudForge Application Catalog\n");
sb.append("==============================\n\n");
Map<String, List<ApplicationSpec>> grouped = discoverGroupedByCategory();
for (Map.Entry<String, List<ApplicationSpec>> entry : grouped.entrySet()) {
String category = entry.getKey();
List<ApplicationSpec> apps = entry.getValue();
sb.append(category.toUpperCase()).append(":\n");
for (ApplicationSpec app : apps) {
sb.append(" - ").append(app.applicationId())
.append(" (").append(app.displayName()).append(")");
if (!app.description().isEmpty()) {
sb.append("\n ").append(app.description());
}
List<String> features = new ArrayList<>();
if (app.supportsOidcIntegration()) features.add("OIDC");
if (app.supportsFargate()) features.add("Fargate");
if (app.supportsEc2()) features.add("EC2");
if (!features.isEmpty()) {
sb.append("\n Supports: ").append(String.join(", ", features));
}
sb.append("\n");
}
sb.append("\n");
}
return sb.toString();
}
}