diff --git a/api/src/main/java/app/simplecloud/api/CloudApi.java b/api/src/main/java/app/simplecloud/api/CloudApi.java index ff811a9..886ef35 100644 --- a/api/src/main/java/app/simplecloud/api/CloudApi.java +++ b/api/src/main/java/app/simplecloud/api/CloudApi.java @@ -1,5 +1,6 @@ package app.simplecloud.api; +import app.simplecloud.api.blueprint.BlueprintApi; import app.simplecloud.api.cache.QueryCache; import app.simplecloud.api.cache.QueryKey; import app.simplecloud.api.event.EventApi; @@ -96,6 +97,16 @@ static CloudApi create(CloudApiOptions options) { */ PersistentServerApi persistentServer(); + /** + * Returns the blueprint management API. + * + *

Use this to create, read, update, and delete blueprints that groups and servers can use + * as reusable source templates. + * + * @return the blueprint API + */ + BlueprintApi blueprint(); + /** * Returns the event subscription API. * diff --git a/api/src/main/java/app/simplecloud/api/blueprint/BlueprintApi.java b/api/src/main/java/app/simplecloud/api/blueprint/BlueprintApi.java new file mode 100644 index 0000000..8855623 --- /dev/null +++ b/api/src/main/java/app/simplecloud/api/blueprint/BlueprintApi.java @@ -0,0 +1,61 @@ +package app.simplecloud.api.blueprint; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * API for managing blueprints. + * + *

Blueprints define reusable runtime and software templates that groups and servers can use + * as their base source configuration. + */ +public interface BlueprintApi { + + /** + * Retrieves a blueprint by its unique identifier. + * + * @param id the blueprint ID + * @return a CompletableFuture that completes with the blueprint, or fails if not found + */ + CompletableFuture getBlueprintById(String id); + + /** + * Retrieves a blueprint by its name. + * + * @param name the blueprint name + * @return a CompletableFuture that completes with the blueprint, or fails if not found + */ + CompletableFuture getBlueprintByName(String name); + + /** + * Retrieves all blueprints. + * + * @return a CompletableFuture that completes with all available blueprints + */ + CompletableFuture> getAllBlueprints(); + + /** + * Creates a new blueprint. + * + * @param request the blueprint configuration to create + * @return a CompletableFuture that completes with the created blueprint + */ + CompletableFuture createBlueprint(CreateBlueprintRequest request); + + /** + * Updates an existing blueprint. + * + * @param id the blueprint ID to update + * @param request the fields to update + * @return a CompletableFuture that completes with the updated blueprint + */ + CompletableFuture updateBlueprint(String id, UpdateBlueprintRequest request); + + /** + * Deletes a blueprint. + * + * @param id the blueprint ID to delete + * @return a CompletableFuture that completes when the blueprint has been deleted + */ + CompletableFuture deleteBlueprint(String id); +} diff --git a/api/src/main/java/app/simplecloud/api/blueprint/CreateBlueprintRequest.java b/api/src/main/java/app/simplecloud/api/blueprint/CreateBlueprintRequest.java new file mode 100644 index 0000000..78703f9 --- /dev/null +++ b/api/src/main/java/app/simplecloud/api/blueprint/CreateBlueprintRequest.java @@ -0,0 +1,131 @@ +package app.simplecloud.api.blueprint; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Request to create a new blueprint. + */ +public class CreateBlueprintRequest { + private final String name; + private final String configurator; + private final String minecraftVersion; + private final String serverSoftware; + private final String serverUrl; + private final String softwareVersion; + private final RuntimeConfig runtimeConfig; + private final List workflowSteps; + + private CreateBlueprintRequest(Builder builder) { + this.name = builder.name; + this.configurator = builder.configurator; + this.minecraftVersion = builder.minecraftVersion; + this.serverSoftware = builder.serverSoftware; + this.serverUrl = builder.serverUrl; + this.softwareVersion = builder.softwareVersion; + this.runtimeConfig = builder.runtimeConfig; + this.workflowSteps = builder.workflowSteps; + } + + public String getName() { + return name; + } + + @Nullable + public String getConfigurator() { + return configurator; + } + + @Nullable + public String getMinecraftVersion() { + return minecraftVersion; + } + + @Nullable + public String getServerSoftware() { + return serverSoftware; + } + + @Nullable + public String getServerUrl() { + return serverUrl; + } + + @Nullable + public String getSoftwareVersion() { + return softwareVersion; + } + + @Nullable + public RuntimeConfig getRuntimeConfig() { + return runtimeConfig; + } + + @Nullable + public List getWorkflowSteps() { + return workflowSteps; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String configurator; + private String minecraftVersion; + private String serverSoftware; + private String serverUrl; + private String softwareVersion; + private RuntimeConfig runtimeConfig; + private List workflowSteps; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder configurator(String configurator) { + this.configurator = configurator; + return this; + } + + public Builder minecraftVersion(String minecraftVersion) { + this.minecraftVersion = minecraftVersion; + return this; + } + + public Builder serverSoftware(String serverSoftware) { + this.serverSoftware = serverSoftware; + return this; + } + + public Builder serverUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + + public Builder softwareVersion(String softwareVersion) { + this.softwareVersion = softwareVersion; + return this; + } + + public Builder runtimeConfig(RuntimeConfig runtimeConfig) { + this.runtimeConfig = runtimeConfig; + return this; + } + + public Builder workflowSteps(List workflowSteps) { + this.workflowSteps = workflowSteps; + return this; + } + + public CreateBlueprintRequest build() { + if (name == null) { + throw new IllegalStateException("name is required"); + } + return new CreateBlueprintRequest(this); + } + } +} diff --git a/api/src/main/java/app/simplecloud/api/blueprint/UpdateBlueprintRequest.java b/api/src/main/java/app/simplecloud/api/blueprint/UpdateBlueprintRequest.java new file mode 100644 index 0000000..bce676e --- /dev/null +++ b/api/src/main/java/app/simplecloud/api/blueprint/UpdateBlueprintRequest.java @@ -0,0 +1,130 @@ +package app.simplecloud.api.blueprint; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Request to update an existing blueprint. + * All fields are optional. + */ +public class UpdateBlueprintRequest { + private final String name; + private final String configurator; + private final String minecraftVersion; + private final String serverSoftware; + private final String serverUrl; + private final String softwareVersion; + private final RuntimeConfig runtimeConfig; + private final List workflowSteps; + + private UpdateBlueprintRequest(Builder builder) { + this.name = builder.name; + this.configurator = builder.configurator; + this.minecraftVersion = builder.minecraftVersion; + this.serverSoftware = builder.serverSoftware; + this.serverUrl = builder.serverUrl; + this.softwareVersion = builder.softwareVersion; + this.runtimeConfig = builder.runtimeConfig; + this.workflowSteps = builder.workflowSteps; + } + + @Nullable + public String getName() { + return name; + } + + @Nullable + public String getConfigurator() { + return configurator; + } + + @Nullable + public String getMinecraftVersion() { + return minecraftVersion; + } + + @Nullable + public String getServerSoftware() { + return serverSoftware; + } + + @Nullable + public String getServerUrl() { + return serverUrl; + } + + @Nullable + public String getSoftwareVersion() { + return softwareVersion; + } + + @Nullable + public RuntimeConfig getRuntimeConfig() { + return runtimeConfig; + } + + @Nullable + public List getWorkflowSteps() { + return workflowSteps; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String configurator; + private String minecraftVersion; + private String serverSoftware; + private String serverUrl; + private String softwareVersion; + private RuntimeConfig runtimeConfig; + private List workflowSteps; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder configurator(String configurator) { + this.configurator = configurator; + return this; + } + + public Builder minecraftVersion(String minecraftVersion) { + this.minecraftVersion = minecraftVersion; + return this; + } + + public Builder serverSoftware(String serverSoftware) { + this.serverSoftware = serverSoftware; + return this; + } + + public Builder serverUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + + public Builder softwareVersion(String softwareVersion) { + this.softwareVersion = softwareVersion; + return this; + } + + public Builder runtimeConfig(RuntimeConfig runtimeConfig) { + this.runtimeConfig = runtimeConfig; + return this; + } + + public Builder workflowSteps(List workflowSteps) { + this.workflowSteps = workflowSteps; + return this; + } + + public UpdateBlueprintRequest build() { + return new UpdateBlueprintRequest(this); + } + } +} diff --git a/api/src/main/java/app/simplecloud/api/cache/EntityType.java b/api/src/main/java/app/simplecloud/api/cache/EntityType.java index 8d24484..e4d72d0 100644 --- a/api/src/main/java/app/simplecloud/api/cache/EntityType.java +++ b/api/src/main/java/app/simplecloud/api/cache/EntityType.java @@ -7,6 +7,7 @@ public enum EntityType { SERVER, GROUP, + BLUEPRINT, PERSISTENT_SERVER, PLAYER } diff --git a/api/src/main/java/app/simplecloud/api/cache/QueryCache.java b/api/src/main/java/app/simplecloud/api/cache/QueryCache.java index 9f58657..b6c00ea 100644 --- a/api/src/main/java/app/simplecloud/api/cache/QueryCache.java +++ b/api/src/main/java/app/simplecloud/api/cache/QueryCache.java @@ -84,7 +84,7 @@ public interface QueryCache { * For example, {@code invalidateAll(QueryKey.of("servers"))} invalidates: *

* diff --git a/api/src/main/java/app/simplecloud/api/cache/QueryKey.java b/api/src/main/java/app/simplecloud/api/cache/QueryKey.java index f2c81df..efcec6b 100644 --- a/api/src/main/java/app/simplecloud/api/cache/QueryKey.java +++ b/api/src/main/java/app/simplecloud/api/cache/QueryKey.java @@ -12,7 +12,7 @@ *

Example keys: *

{@code
  * QueryKey.of("server", serverId)              // Single server by ID
- * QueryKey.of("servers", "group", groupName)   // Servers filtered by group
+ * QueryKey.of("servers", "serverBaseName", serverBaseName) // Servers filtered by group or persistent server name
  * QueryKey.of("servers")                       // All servers
  * QueryKey.of("groups")                        // All groups
  * QueryKey.of("group", groupId)                // Single group by ID
@@ -21,7 +21,7 @@
  * 

Hierarchical matching for invalidation: *

{@code
  * // Invalidating QueryKey.of("servers") will also invalidate:
- * // - QueryKey.of("servers", "group", "Lobby")
+ * // - QueryKey.of("servers", "serverBaseName", "Lobby")
  * // - QueryKey.of("servers", "query", ...)
  * cache.invalidateAll(QueryKey.of("servers"));
  * }
@@ -55,6 +55,7 @@ public EntityType getEntityType() { return switch (first) { case "server", "servers" -> EntityType.SERVER; case "group", "groups" -> EntityType.GROUP; + case "blueprint", "blueprints" -> EntityType.BLUEPRINT; case "persistentServer", "persistentServers" -> EntityType.PERSISTENT_SERVER; case "player", "players" -> EntityType.PLAYER; default -> null; @@ -72,7 +73,7 @@ public Object[] getParts() { * Checks if this key matches or is a parent of the given key. * Used for invalidation patterns. * - *

Example: {@code QueryKey.of("servers")} matches {@code QueryKey.of("servers", "group", "Lobby")} + *

Example: {@code QueryKey.of("servers")} matches {@code QueryKey.of("servers", "serverBaseName", "Lobby")} * * @param other the key to check against * @return true if this key is a prefix of the other key diff --git a/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java b/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java index 5e467b8..f358ed7 100644 --- a/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java +++ b/api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java @@ -2,10 +2,12 @@ import app.simplecloud.api.CloudApi; import app.simplecloud.api.CloudApiOptions; +import app.simplecloud.api.blueprint.BlueprintApi; import app.simplecloud.api.cache.CacheConfig; import app.simplecloud.api.cache.QueryCache; import app.simplecloud.api.event.EventApi; import app.simplecloud.api.group.GroupApi; +import app.simplecloud.api.internal.blueprint.BlueprintApiImpl; import app.simplecloud.api.internal.cache.CacheEventListener; import app.simplecloud.api.internal.cache.NoOpQueryCache; import app.simplecloud.api.internal.cache.QueryCacheImpl; @@ -33,6 +35,7 @@ public class CloudApiImpl implements CloudApi { private final CacheEventListener cacheEventListener; private final ServerApi serverApi; private final GroupApi groupApi; + private final BlueprintApi blueprintApi; private final PersistentServerApi persistentServerApi; private final EventApi eventApi; private final PlayerApi playerApi; @@ -65,6 +68,7 @@ public CloudApiImpl(CloudApiOptions options) { this.eventApi = new EventApiImpl(natsClient, options.getNetworkId()); this.serverApi = new ServerApiImpl(options, queryCache); this.groupApi = new GroupApiImpl(options, queryCache); + this.blueprintApi = new BlueprintApiImpl(options, queryCache); this.persistentServerApi = new PersistentServerApiImpl(options, queryCache); this.playerApi = new PlayerApiImpl(options, natsClient); @@ -88,6 +92,11 @@ public ServerApi server() { return serverApi; } + @Override + public BlueprintApi blueprint() { + return blueprintApi; + } + @Override public PersistentServerApi persistentServer() { return persistentServerApi; diff --git a/api/src/main/java/app/simplecloud/api/internal/blueprint/BlueprintApiImpl.java b/api/src/main/java/app/simplecloud/api/internal/blueprint/BlueprintApiImpl.java new file mode 100644 index 0000000..3fbac65 --- /dev/null +++ b/api/src/main/java/app/simplecloud/api/internal/blueprint/BlueprintApiImpl.java @@ -0,0 +1,204 @@ +package app.simplecloud.api.internal.blueprint; + +import app.simplecloud.api.CloudApiOptions; +import app.simplecloud.api.blueprint.Blueprint; +import app.simplecloud.api.blueprint.BlueprintApi; +import app.simplecloud.api.blueprint.CreateBlueprintRequest; +import app.simplecloud.api.blueprint.RuntimeConfig; +import app.simplecloud.api.blueprint.UpdateBlueprintRequest; +import app.simplecloud.api.cache.QueryCache; +import app.simplecloud.api.cache.QueryKey; +import app.simplecloud.api.web.ApiException; +import app.simplecloud.api.web.apis.BlueprintsApi; +import app.simplecloud.api.web.models.ModelsBlueprintSummary; +import app.simplecloud.api.web.models.ModelsCreateBlueprintRequest; +import app.simplecloud.api.web.models.ModelsCreateBlueprintResponse; +import app.simplecloud.api.web.models.ModelsListBlueprintsResponse; +import app.simplecloud.api.web.models.ModelsRuntimeConfig; +import app.simplecloud.api.web.models.ModelsUpdateBlueprintRequest; +import app.simplecloud.api.web.models.V0BlueprintsPostRequest; +import app.simplecloud.api.web.models.V0BlueprintsPutRequest; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class BlueprintApiImpl implements BlueprintApi { + + private final CloudApiOptions options; + private final BlueprintsApi blueprintsApi; + private final QueryCache cache; + + public BlueprintApiImpl(CloudApiOptions options, QueryCache cache) { + this.options = options; + this.cache = cache; + this.blueprintsApi = new BlueprintsApi(); + this.blueprintsApi.setCustomBaseUrl(options.getControllerUrl()); + if (options.getComponent() != null && !options.getComponent().isBlank()) { + this.blueprintsApi.getApiClient().addDefaultHeader("X-SC-Component", options.getComponent()); + } + } + + @Override + public CompletableFuture getBlueprintById(String id) { + QueryKey key = QueryKey.of("blueprint", id); + + return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> { + try { + ModelsListBlueprintsResponse response = listBlueprints(); + List blueprints = response.getBlueprints(); + if (blueprints != null) { + for (ModelsBlueprintSummary summary : blueprints) { + if (id.equals(summary.getBlueprintId())) { + return new BlueprintImpl(summary); + } + } + } + throw new RuntimeException("Blueprint not found: " + id); + } catch (ApiException e) { + throw new RuntimeException(e); + } + })); + } + + @Override + public CompletableFuture getBlueprintByName(String name) { + QueryKey key = QueryKey.of("blueprint", "name", name); + + return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> { + try { + ModelsListBlueprintsResponse response = listBlueprints(); + List blueprints = response.getBlueprints(); + if (blueprints != null) { + for (ModelsBlueprintSummary summary : blueprints) { + if (name.equals(summary.getName())) { + return new BlueprintImpl(summary); + } + } + } + throw new RuntimeException("Blueprint not found: " + name); + } catch (ApiException e) { + throw new RuntimeException(e); + } + })); + } + + @Override + public CompletableFuture> getAllBlueprints() { + QueryKey key = QueryKey.of("blueprints"); + + return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> { + try { + ModelsListBlueprintsResponse response = listBlueprints(); + List blueprints = response.getBlueprints(); + if (blueprints == null) { + return List.of(); + } + + return blueprints.stream() + .map(BlueprintImpl::new) + .toList(); + } catch (ApiException e) { + throw new RuntimeException(e); + } + })); + } + + @Override + public CompletableFuture createBlueprint(CreateBlueprintRequest request) { + return CompletableFuture.supplyAsync(() -> { + try { + ModelsCreateBlueprintRequest apiRequest = new ModelsCreateBlueprintRequest(); + apiRequest.setName(request.getName()); + apiRequest.setConfigurator(request.getConfigurator()); + apiRequest.setMinecraftVersion(request.getMinecraftVersion()); + apiRequest.setServerSoftware(request.getServerSoftware()); + apiRequest.setServerUrl(request.getServerUrl()); + apiRequest.setSoftwareVersion(request.getSoftwareVersion()); + apiRequest.setWorkflowSteps(request.getWorkflowSteps()); + apiRequest.setRuntimeConfig(convertRuntimeConfig(request.getRuntimeConfig())); + + ModelsCreateBlueprintResponse response = blueprintsApi.v0BlueprintsPost( + options.getNetworkId(), + options.getNetworkSecret(), + new V0BlueprintsPostRequest(apiRequest) + ); + + cache.invalidateAll(QueryKey.of("blueprint")); + cache.invalidateAll(QueryKey.of("blueprints")); + + return getBlueprintById(response.getBlueprintId()).join(); + } catch (ApiException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public CompletableFuture updateBlueprint(String id, UpdateBlueprintRequest request) { + return CompletableFuture.supplyAsync(() -> { + try { + ModelsUpdateBlueprintRequest apiRequest = new ModelsUpdateBlueprintRequest(); + apiRequest.setName(request.getName()); + apiRequest.setConfigurator(request.getConfigurator()); + apiRequest.setMinecraftVersion(request.getMinecraftVersion()); + apiRequest.setServerSoftware(request.getServerSoftware()); + apiRequest.setServerUrl(request.getServerUrl()); + apiRequest.setSoftwareVersion(request.getSoftwareVersion()); + apiRequest.setWorkflowSteps(request.getWorkflowSteps()); + apiRequest.setRuntimeConfig(convertRuntimeConfig(request.getRuntimeConfig())); + + blueprintsApi.v0BlueprintsPut( + options.getNetworkId(), + options.getNetworkSecret(), + id, + new V0BlueprintsPutRequest(apiRequest) + ); + + cache.invalidateAll(QueryKey.of("blueprint")); + cache.invalidateAll(QueryKey.of("blueprints")); + + return getBlueprintById(id).join(); + } catch (ApiException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public CompletableFuture deleteBlueprint(String id) { + return CompletableFuture.runAsync(() -> { + try { + blueprintsApi.v0BlueprintsDelete( + options.getNetworkId(), + options.getNetworkSecret(), + id + ); + + cache.invalidateAll(QueryKey.of("blueprint")); + cache.invalidateAll(QueryKey.of("blueprints")); + } catch (ApiException e) { + throw new RuntimeException(e); + } + }); + } + + private ModelsListBlueprintsResponse listBlueprints() throws ApiException { + return blueprintsApi.v0BlueprintsGet( + options.getNetworkId(), + options.getNetworkSecret() + ); + } + + private ModelsRuntimeConfig convertRuntimeConfig(RuntimeConfig runtimeConfig) { + if (runtimeConfig == null) { + return null; + } + + ModelsRuntimeConfig result = new ModelsRuntimeConfig(); + if (runtimeConfig.getType() != null) { + result.setType(runtimeConfig.getType().name()); + } + result.setWith(runtimeConfig.getWith()); + return result; + } +} diff --git a/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java b/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java index 1505c9f..030e702 100644 --- a/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java +++ b/api/src/main/java/app/simplecloud/api/internal/cache/CacheEventListener.java @@ -133,6 +133,22 @@ private void setupListeners(EventApi eventApi) { schedulePatternInvalidation(QueryKey.of("groups")); })); + // Blueprint events + subscriptions.add(eventApi.blueprint().onCreated(event -> { + schedulePatternInvalidation(QueryKey.of("blueprint")); + schedulePatternInvalidation(QueryKey.of("blueprints")); + })); + + subscriptions.add(eventApi.blueprint().onUpdated(event -> { + schedulePatternInvalidation(QueryKey.of("blueprint")); + schedulePatternInvalidation(QueryKey.of("blueprints")); + })); + + subscriptions.add(eventApi.blueprint().onDeleted(event -> { + schedulePatternInvalidation(QueryKey.of("blueprint")); + schedulePatternInvalidation(QueryKey.of("blueprints")); + })); + // Persistent server events subscriptions.add(eventApi.persistentServer().onCreated(event -> { schedulePatternInvalidation(QueryKey.of("persistentServers")); diff --git a/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java b/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java index e346a0d..428e3c8 100644 --- a/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java +++ b/api/src/main/java/app/simplecloud/api/internal/server/ServerApiImpl.java @@ -3,6 +3,8 @@ import app.simplecloud.api.CloudApiOptions; import app.simplecloud.api.cache.QueryCache; import app.simplecloud.api.cache.QueryKey; +import app.simplecloud.api.group.Group; +import app.simplecloud.api.persistentserver.PersistentServer; import app.simplecloud.api.server.Server; import app.simplecloud.api.server.ServerApi; import app.simplecloud.api.server.ServerQuery; @@ -69,13 +71,46 @@ public CompletableFuture getServerById(String id) { } @Override - public CompletableFuture getServerByNumericalId(String groupName, int numericalId) { - QueryKey key = QueryKey.of("server", "numerical", groupName, numericalId); + public CompletableFuture getServerByName(String serverName, char splitChar) { + ParsedServerName parsedServerName = parseServerName(serverName, splitChar); + if (parsedServerName == null) { + return CompletableFuture.completedFuture(null); + } + + QueryKey key = QueryKey.of("server", "serverName", serverName, splitChar); return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> { try { ServerQuery query = ServerQuery.create() - .filterByServerGroupName(groupName) + .filterByServerBaseName(parsedServerName.fullName(), parsedServerName.serverBaseName()) + .filterByNumericalId(-1, parsedServerName.numericalId()); + ModelsListServersResponse serversResponse = executeQuery(query); + + List servers = serversResponse.getServers(); + if (servers == null || servers.isEmpty()) { + throw new RuntimeException("Server not found: " + serverName); + } + + Server server = findServerByName(servers, parsedServerName); + if (server != null) { + return server; + } + + throw new RuntimeException("Server not found: " + serverName); + } catch (ApiException e) { + throw new RuntimeException(e); + } + })); + } + + @Override + public CompletableFuture getServerByNumericalId(String serverBaseName, int numericalId) { + QueryKey key = QueryKey.of("server", "numerical", "serverBaseName", serverBaseName, numericalId); + + return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> { + try { + ServerQuery query = ServerQuery.create() + .filterByServerBaseName(serverBaseName) .filterByNumericalId(numericalId); ModelsListServersResponse serversResponse = executeQuery(query); @@ -89,7 +124,7 @@ public CompletableFuture getServerByNumericalId(String groupName, int nu return new ServerImpl(summary); } } - throw new RuntimeException("Server not found in group " + groupName + " with numerical ID " + numericalId); + throw new RuntimeException("Server not found for server base name " + serverBaseName + " with numerical ID " + numericalId); } catch (ApiException e) { throw new RuntimeException(e); @@ -98,13 +133,13 @@ public CompletableFuture getServerByNumericalId(String groupName, int nu } @Override - public CompletableFuture> getServersByGroup(String groupName) { - QueryKey key = QueryKey.of("servers", "group", groupName); + public CompletableFuture> getServersByServerBaseName(String serverBaseName) { + QueryKey key = QueryKey.of("servers", "serverBaseName", serverBaseName); return cache.fetch(key, () -> CompletableFuture.supplyAsync(() -> { try { ServerQuery query = ServerQuery.create() - .filterByServerGroupName(groupName); + .filterByServerBaseName(serverBaseName); ModelsListServersResponse serversResponse = executeQuery(query); List servers = serversResponse.getServers(); @@ -154,7 +189,7 @@ private QueryKey buildQueryKey(@Nullable ServerQuery query) { query.getServerhostId(), query.getPersistentServerId(), query.getServerGroupTypes(), - query.getServerGroupNames(), + query.getServerBaseNames(), query.getServerGroupTags(), query.getNumericalIds(), query.getSortBy(), @@ -185,9 +220,9 @@ private ModelsListServersResponse executeQuery(@Nullable ServerQuery query) thro .collect(java.util.stream.Collectors.joining(",")); } - String serverGroupName = null; - if (query != null && query.getServerGroupNames() != null && !query.getServerGroupNames().isEmpty()) { - serverGroupName = String.join(",", query.getServerGroupNames()); + String serverBaseName = null; + if (query != null && query.getServerBaseNames() != null && !query.getServerBaseNames().isEmpty()) { + serverBaseName = String.join(",", query.getServerBaseNames()); } String serverGroupTags = null; @@ -211,7 +246,7 @@ private ModelsListServersResponse executeQuery(@Nullable ServerQuery query) thro serverhostId, persistentServerId, serverGroupType, - serverGroupName, + serverBaseName, serverGroupTags, numericalIds, sortBy, @@ -344,4 +379,73 @@ public CompletableFuture> deleteServerProperties(String id, } }); } + + private Server findServerByName(List servers, ParsedServerName parsedServerName) { + Server fallback = null; + + for (ModelsServerSummary summary : servers) { + Server server = new ServerImpl(summary); + if (isExactPersistentServerMatch(server, parsedServerName) + || isExactGroupServerMatch(server, parsedServerName)) { + return server; + } + + if (fallback == null && summary.getNumericalId() != null + && (summary.getNumericalId() == -1 || summary.getNumericalId() == parsedServerName.numericalId())) { + fallback = server; + } + } + + return fallback; + } + + private boolean isExactPersistentServerMatch(Server server, ParsedServerName parsedServerName) { + if (server.getNumericalId() != -1) { + return false; + } + + PersistentServer persistentServer = server.getPersistentServer(); + return persistentServer != null && parsedServerName.fullName().equals(persistentServer.getName()); + } + + private boolean isExactGroupServerMatch(Server server, ParsedServerName parsedServerName) { + if (server.getNumericalId() != parsedServerName.numericalId()) { + return false; + } + + Group group = server.getGroup(); + return group != null && parsedServerName.serverBaseName().equals(group.getName()); + } + + @Nullable + private ParsedServerName parseServerName(String serverName, char splitChar) { + if (serverName == null) { + return null; + } + + String trimmedServerName = serverName.trim(); + if (trimmedServerName.isEmpty()) { + return null; + } + + int separatorIndex = trimmedServerName.lastIndexOf(splitChar); + if (separatorIndex <= 0 || separatorIndex == trimmedServerName.length() - 1) { + return new ParsedServerName(trimmedServerName, trimmedServerName, -1); + } + + String serverBaseName = trimmedServerName.substring(0, separatorIndex).trim(); + String numericalIdPart = trimmedServerName.substring(separatorIndex + 1).trim(); + if (serverBaseName.isEmpty() || numericalIdPart.isEmpty()) { + return new ParsedServerName(trimmedServerName, trimmedServerName, -1); + } + + try { + return new ParsedServerName(trimmedServerName, serverBaseName, Integer.parseInt(numericalIdPart)); + } catch (NumberFormatException ignored) { + return new ParsedServerName(trimmedServerName, trimmedServerName, -1); + } + } + + private record ParsedServerName(String fullName, String serverBaseName, int numericalId) { + } } diff --git a/api/src/main/java/app/simplecloud/api/server/ServerApi.java b/api/src/main/java/app/simplecloud/api/server/ServerApi.java index 6cec26a..91bf2fa 100644 --- a/api/src/main/java/app/simplecloud/api/server/ServerApi.java +++ b/api/src/main/java/app/simplecloud/api/server/ServerApi.java @@ -21,22 +21,49 @@ public interface ServerApi { */ CompletableFuture getServerById(String id); + /** + * Retrieves a server by its runtime name. + * + *

The provided name may refer either to a persistent server or to a group-backed server + * instance. The lookup uses the provided split character to separate the base name from the + * trailing numerical suffix, then resolves either the persistent server name with numerical ID + * {@code -1}, or the base name with the parsed numerical ID. + * + *

If the provided name does not contain the split character, or the trailing segment is not + * a valid numerical suffix, the whole input is treated as a persistent server base name. + * + * @param serverName the runtime server name + * @param splitChar the character used to split the base name from the numerical suffix + * @return a CompletableFuture that completes with the server, or null if the provided name is blank + */ + CompletableFuture getServerByName(String serverName, char splitChar); + /** * Retrieves a server by its numerical ID. * - * @param groupName the name of the server group + * @param serverBaseName the persistent server name or group name used by the server query filter * @param numericalId the numerical server ID * @return a CompletableFuture that completes with the server, or fails if not found */ - CompletableFuture getServerByNumericalId(String groupName, int numericalId); + CompletableFuture getServerByNumericalId(String serverBaseName, int numericalId); /** - * Retrieves all servers belonging to a specific group. + * Retrieves all servers whose base configuration name matches the provided server base name. * - * @param groupName the name of the server group - * @return a CompletableFuture that completes with a list of servers in the specified group + *

This matches either a persistent server name or a group name. + * + * @param serverBaseName the server base name to filter by + * @return a CompletableFuture that completes with a list of matching servers + */ + CompletableFuture> getServersByServerBaseName(String serverBaseName); + + /** + * @deprecated Use {@link #getServersByServerBaseName(String)} instead. */ - CompletableFuture> getServersByGroup(String groupName); + @Deprecated + default CompletableFuture> getServersByGroup(String groupName) { + return getServersByServerBaseName(groupName); + } /** * Retrieves all servers, optionally filtered by a query. diff --git a/api/src/main/java/app/simplecloud/api/server/ServerQuery.java b/api/src/main/java/app/simplecloud/api/server/ServerQuery.java index 95101c2..6e18412 100644 --- a/api/src/main/java/app/simplecloud/api/server/ServerQuery.java +++ b/api/src/main/java/app/simplecloud/api/server/ServerQuery.java @@ -26,7 +26,7 @@ public class ServerQuery { private String serverhostId; private String persistentServerId; private List serverGroupTypes; - private List serverGroupNames; + private List serverBaseNames; private List serverGroupTags; private List numericalIds; private String sortBy; @@ -125,24 +125,43 @@ public ServerQuery filterByServerGroupType(GroupServerType... types) { } @Nullable - public List getServerGroupNames() { - return serverGroupNames; + public List getServerBaseNames() { + return serverBaseNames; } /** - * Filter by one or more server group names. + * Filter by one or more server base names. + * + *

This matches either a persistent server name or a group name. * - * @param names the server group names to filter by + * @param serverBaseNames the server base names to filter by * @return this query for chaining */ - public ServerQuery filterByServerGroupName(String... names) { - if (this.serverGroupNames == null) { - this.serverGroupNames = new ArrayList<>(); + public ServerQuery filterByServerBaseName(String... serverBaseNames) { + if (this.serverBaseNames == null) { + this.serverBaseNames = new ArrayList<>(); } - this.serverGroupNames.addAll(Arrays.asList(names)); + this.serverBaseNames.addAll(Arrays.asList(serverBaseNames)); return this; } + /** + * @deprecated Use {@link #getServerBaseNames()} instead. + */ + @Deprecated + @Nullable + public List getServerGroupNames() { + return getServerBaseNames(); + } + + /** + * @deprecated Use {@link #filterByServerBaseName(String...)} instead. + */ + @Deprecated + public ServerQuery filterByServerGroupName(String... names) { + return filterByServerBaseName(names); + } + @Nullable public List getServerGroupTags() { return serverGroupTags;