diff --git a/build.gradle b/build.gradle index b01ff76..f406e39 100644 --- a/build.gradle +++ b/build.gradle @@ -22,14 +22,14 @@ repositories { dependencies { implementation 'com.google.code.gson:gson:2.10.1' - implementation 'io.javalin:javalin:5.6.3' + implementation 'io.javalin:javalin:6.0.0-beta.3' implementation 'org.slf4j:slf4j-simple:2.0.9' implementation 'commons-io:commons-io:2.15.1' implementation 'org.jetbrains:annotations:24.1.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' testImplementation "org.junit.jupiter:junit-jupiter-params:5.9.2" - testImplementation 'io.javalin:javalin-testtools:5.6.3' + testImplementation 'io.javalin:javalin-testtools:6.0.0-beta.3' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/checkstyle.xml b/checkstyle.xml index 2690e6f..015bf1b 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -156,7 +156,7 @@ - + diff --git a/src/main/java/net/fabricmc/meta/FabricMeta.java b/src/main/java/net/fabricmc/meta/FabricMeta.java index e80714f..8fb8aba 100644 --- a/src/main/java/net/fabricmc/meta/FabricMeta.java +++ b/src/main/java/net/fabricmc/meta/FabricMeta.java @@ -35,15 +35,19 @@ import net.fabricmc.meta.data.VersionDatabase; import net.fabricmc.meta.utils.Reference; +import net.fabricmc.meta.web.CacheHandler; import net.fabricmc.meta.web.WebServer; public class FabricMeta { + // TODO remove all this static access + @Deprecated() public static volatile VersionDatabase database; private static final Logger LOGGER = LoggerFactory.getLogger(VersionDatabase.class); private static final Map config = new HashMap<>(); private static boolean configInitialized; private static URL heartbeatUrl; // URL pinged with every successful update() + private static CacheHandler cacheHandler = new CacheHandler(); public static void main(String[] args) { Path configFile = Paths.get("config.json"); @@ -77,12 +81,14 @@ public static void main(String[] args) { ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); executorService.scheduleAtFixedRate(FabricMeta::update, 1, 1, TimeUnit.MINUTES); - WebServer.start(); + WebServer webServer = new WebServer(() -> database, cacheHandler); + webServer.createServer().start(5555); } private static void update() { try { database = VersionDatabase.generate(); + cacheHandler.invalidateCache(); updateHeartbeat(); } catch (Throwable t) { if (database == null) { diff --git a/src/main/java/net/fabricmc/meta/data/DataProvider.java b/src/main/java/net/fabricmc/meta/data/DataProvider.java new file mode 100644 index 0000000..ee73424 --- /dev/null +++ b/src/main/java/net/fabricmc/meta/data/DataProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.data; + +import java.util.List; + +import com.google.gson.JsonObject; + +import net.fabricmc.meta.models.BaseVersion; +import net.fabricmc.meta.models.MavenBuildGameVersion; +import net.fabricmc.meta.models.MavenBuildVersion; +import net.fabricmc.meta.models.MavenVersion; +import net.fabricmc.meta.utils.LoaderMeta; + +public interface DataProvider { + @Deprecated // TODO work to remove + VersionDatabase getVersionDatabase(); + + default List getGameVersions() { + return getVersionDatabase().game; + } + + default List getMappingVersions() { + return getVersionDatabase().mappings; + } + + default List getIntermediaryVersions() { + return getVersionDatabase().intermediary; + } + + default List getLoaderVersions() { + return getVersionDatabase().getLoader(); + } + + default JsonObject getLoaderInstallerJson(String mavenNotation) { + return LoaderMeta.getMeta(mavenNotation); + } +} diff --git a/src/main/java/net/fabricmc/meta/data/VersionDatabase.java b/src/main/java/net/fabricmc/meta/data/VersionDatabase.java index 8982b3f..54bd90e 100644 --- a/src/main/java/net/fabricmc/meta/data/VersionDatabase.java +++ b/src/main/java/net/fabricmc/meta/data/VersionDatabase.java @@ -30,13 +30,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.fabricmc.meta.models.BaseVersion; +import net.fabricmc.meta.models.MavenBuildGameVersion; +import net.fabricmc.meta.models.MavenBuildVersion; +import net.fabricmc.meta.models.MavenUrlVersion; +import net.fabricmc.meta.models.MavenVersion; import net.fabricmc.meta.utils.MinecraftLauncherMeta; import net.fabricmc.meta.utils.PomParser; -import net.fabricmc.meta.web.models.BaseVersion; -import net.fabricmc.meta.web.models.MavenBuildGameVersion; -import net.fabricmc.meta.web.models.MavenBuildVersion; -import net.fabricmc.meta.web.models.MavenUrlVersion; -import net.fabricmc.meta.web.models.MavenVersion; public class VersionDatabase { public static final PomParser MAPPINGS_PARSER = new PomParser(LOCAL_FABRIC_MAVEN_URL + "net/fabricmc/yarn/maven-metadata.xml"); @@ -63,10 +63,8 @@ public static VersionDatabase generate() throws IOException, XMLStreamException database.intermediary = INTERMEDIARY_PARSER.getMeta(MavenVersion::new, "net.fabricmc:intermediary:"); database.loader = LOADER_PARSER.getMeta(MavenBuildVersion::new, "net.fabricmc:fabric-loader:", list -> { for (BaseVersion version : list) { - if (isPublicLoaderVersion(version)) { - version.setStable(true); - break; - } + version.setStable(true); + break; } }); database.installer = INSTALLER_PARSER.getMeta(MavenUrlVersion::new, "net.fabricmc:fabric-installer:"); @@ -114,14 +112,6 @@ private void loadMcData() throws IOException { } public List getLoader() { - return loader.stream().filter(VersionDatabase::isPublicLoaderVersion).collect(Collectors.toList()); - } - - private static boolean isPublicLoaderVersion(BaseVersion version) { - return true; - } - - public List getAllLoader() { return Collections.unmodifiableList(loader); } } diff --git a/src/main/java/net/fabricmc/meta/web/models/BaseVersion.java b/src/main/java/net/fabricmc/meta/models/BaseVersion.java similarity index 96% rename from src/main/java/net/fabricmc/meta/web/models/BaseVersion.java rename to src/main/java/net/fabricmc/meta/models/BaseVersion.java index fd2dd39..e6f7a6b 100644 --- a/src/main/java/net/fabricmc/meta/web/models/BaseVersion.java +++ b/src/main/java/net/fabricmc/meta/models/BaseVersion.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; import java.util.function.Predicate; diff --git a/src/main/java/net/fabricmc/meta/web/models/LoaderInfoBase.java b/src/main/java/net/fabricmc/meta/models/LoaderInfoBase.java similarity index 94% rename from src/main/java/net/fabricmc/meta/web/models/LoaderInfoBase.java rename to src/main/java/net/fabricmc/meta/models/LoaderInfoBase.java index 41f6a23..6323028 100644 --- a/src/main/java/net/fabricmc/meta/web/models/LoaderInfoBase.java +++ b/src/main/java/net/fabricmc/meta/models/LoaderInfoBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; public interface LoaderInfoBase { MavenBuildVersion getLoader(); diff --git a/src/main/java/net/fabricmc/meta/web/models/LoaderInfoV1.java b/src/main/java/net/fabricmc/meta/models/LoaderInfoV1.java similarity index 92% rename from src/main/java/net/fabricmc/meta/web/models/LoaderInfoV1.java rename to src/main/java/net/fabricmc/meta/models/LoaderInfoV1.java index 5523c7a..90a077e 100644 --- a/src/main/java/net/fabricmc/meta/web/models/LoaderInfoV1.java +++ b/src/main/java/net/fabricmc/meta/models/LoaderInfoV1.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; import com.google.gson.JsonObject; import org.jetbrains.annotations.Nullable; @@ -34,7 +34,7 @@ public LoaderInfoV1(MavenBuildVersion loader, MavenBuildGameVersion mappings) { } public LoaderInfoV1 populateMeta() { - launcherMeta = LoaderMeta.getMeta(this); + launcherMeta = LoaderMeta.getMeta(getLoader().getMaven()); return this; } diff --git a/src/main/java/net/fabricmc/meta/web/models/LoaderInfoV2.java b/src/main/java/net/fabricmc/meta/models/LoaderInfoV2.java similarity index 93% rename from src/main/java/net/fabricmc/meta/web/models/LoaderInfoV2.java rename to src/main/java/net/fabricmc/meta/models/LoaderInfoV2.java index 0547f43..d8d569a 100644 --- a/src/main/java/net/fabricmc/meta/web/models/LoaderInfoV2.java +++ b/src/main/java/net/fabricmc/meta/models/LoaderInfoV2.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; import com.google.gson.JsonObject; import org.jetbrains.annotations.Nullable; @@ -34,7 +34,7 @@ public LoaderInfoV2(MavenBuildVersion loader, MavenVersion intermediary) { } public LoaderInfoV2 populateMeta() { - launcherMeta = LoaderMeta.getMeta(this); + launcherMeta = LoaderMeta.getMeta(getLoader().getMaven()); return this; } diff --git a/src/main/java/net/fabricmc/meta/web/models/MavenBuildGameVersion.java b/src/main/java/net/fabricmc/meta/models/MavenBuildGameVersion.java similarity index 96% rename from src/main/java/net/fabricmc/meta/web/models/MavenBuildGameVersion.java rename to src/main/java/net/fabricmc/meta/models/MavenBuildGameVersion.java index dd13358..14d8f0d 100644 --- a/src/main/java/net/fabricmc/meta/web/models/MavenBuildGameVersion.java +++ b/src/main/java/net/fabricmc/meta/models/MavenBuildGameVersion.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; import net.fabricmc.meta.utils.YarnVersionParser; diff --git a/src/main/java/net/fabricmc/meta/web/models/MavenBuildVersion.java b/src/main/java/net/fabricmc/meta/models/MavenBuildVersion.java similarity index 96% rename from src/main/java/net/fabricmc/meta/web/models/MavenBuildVersion.java rename to src/main/java/net/fabricmc/meta/models/MavenBuildVersion.java index 104a299..bc282b8 100644 --- a/src/main/java/net/fabricmc/meta/web/models/MavenBuildVersion.java +++ b/src/main/java/net/fabricmc/meta/models/MavenBuildVersion.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; public class MavenBuildVersion extends MavenVersion { String separator; diff --git a/src/main/java/net/fabricmc/meta/web/models/MavenUrlVersion.java b/src/main/java/net/fabricmc/meta/models/MavenUrlVersion.java similarity index 96% rename from src/main/java/net/fabricmc/meta/web/models/MavenUrlVersion.java rename to src/main/java/net/fabricmc/meta/models/MavenUrlVersion.java index 010f322..69b0654 100644 --- a/src/main/java/net/fabricmc/meta/web/models/MavenUrlVersion.java +++ b/src/main/java/net/fabricmc/meta/models/MavenUrlVersion.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; import net.fabricmc.meta.utils.Reference; diff --git a/src/main/java/net/fabricmc/meta/web/models/MavenVersion.java b/src/main/java/net/fabricmc/meta/models/MavenVersion.java similarity index 95% rename from src/main/java/net/fabricmc/meta/web/models/MavenVersion.java rename to src/main/java/net/fabricmc/meta/models/MavenVersion.java index 162ea30..4cb099e 100644 --- a/src/main/java/net/fabricmc/meta/web/models/MavenVersion.java +++ b/src/main/java/net/fabricmc/meta/models/MavenVersion.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web.models; +package net.fabricmc.meta.models; public class MavenVersion extends BaseVersion { String maven; diff --git a/src/main/java/net/fabricmc/meta/utils/LoaderMeta.java b/src/main/java/net/fabricmc/meta/utils/LoaderMeta.java index 2bb910d..e62a943 100644 --- a/src/main/java/net/fabricmc/meta/utils/LoaderMeta.java +++ b/src/main/java/net/fabricmc/meta/utils/LoaderMeta.java @@ -22,17 +22,18 @@ import java.io.IOException; import java.net.URL; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import org.apache.commons.io.FileUtils; - -import net.fabricmc.meta.web.WebServer; -import net.fabricmc.meta.web.models.LoaderInfoBase; +import org.jetbrains.annotations.Nullable; public class LoaderMeta { - public static final File BASE_DIR = new File("metadata"); + private static final File BASE_DIR = new File("metadata"); + private static final Gson GSON = new GsonBuilder().create(); - public static JsonObject getMeta(LoaderInfoBase loaderInfo) { - String loaderMaven = loaderInfo.getLoader().getMaven(); + @Nullable + public static JsonObject getMeta(String loaderMaven) { String[] split = loaderMaven.split(":"); String path = String.format("%s/%s/%s", split[0].replaceAll("\\.", "/"), split[1], split[2]); String filename = String.format("%s-%s.json", split[1], split[2]); @@ -51,7 +52,7 @@ public static JsonObject getMeta(LoaderInfoBase loaderInfo) { } try { - JsonObject jsonObject = WebServer.GSON.fromJson(new FileReader(launcherMetaFile), JsonObject.class); + JsonObject jsonObject = GSON.fromJson(new FileReader(launcherMetaFile), JsonObject.class); return jsonObject; } catch (FileNotFoundException e) { e.printStackTrace(); diff --git a/src/main/java/net/fabricmc/meta/utils/PomParser.java b/src/main/java/net/fabricmc/meta/utils/PomParser.java index 862ff27..ae7dc59 100644 --- a/src/main/java/net/fabricmc/meta/utils/PomParser.java +++ b/src/main/java/net/fabricmc/meta/utils/PomParser.java @@ -33,7 +33,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; -import net.fabricmc.meta.web.models.BaseVersion; +import net.fabricmc.meta.models.BaseVersion; public class PomParser { public String path; diff --git a/src/main/java/net/fabricmc/meta/web/CacheHandler.java b/src/main/java/net/fabricmc/meta/web/CacheHandler.java new file mode 100644 index 0000000..29510cb --- /dev/null +++ b/src/main/java/net/fabricmc/meta/web/CacheHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.web; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.javalin.http.Handler; + +public class CacheHandler { + private final Map cache = new ConcurrentHashMap<>(); + + public CacheHandler() { + } + + public Handler before() { + return ctx -> { + Response response = cache.get(ctx.path()); + + if (response == null) { + return; + } + + // Replay the response + ctx.status(response.status()); + response.headers().forEach(ctx::header); + ctx.contentType(response.contentType()); + ctx.result(response.body()); + + ctx.skipRemainingHandlers(); + }; + } + + public Handler after() { + return ctx -> { + if (ctx.statusCode() != 200) { + return; + } + + if (!ctx.queryParamMap().isEmpty()) { + // Don't cache any requests with query params to prevent the cache from growing too big. + // Maybe look into something better here + return; + } + + cache.put(ctx.path(), new Response( + ctx.statusCode(), + readAllBytes(ctx.resultInputStream()), + ctx.headerMap(), + ctx.res().getContentType())); + }; + } + + private static byte[] readAllBytes(InputStream is) throws IOException { + is.reset(); + byte[] bytes = is.readAllBytes(); + is.reset(); + return bytes; + } + + public void invalidateCache() { + cache.clear(); + } + + private record Response( + int status, + byte[] body, + Map headers, + String contentType) { + } +} diff --git a/src/main/java/net/fabricmc/meta/web/Endpoint.java b/src/main/java/net/fabricmc/meta/web/Endpoint.java new file mode 100644 index 0000000..5f7951b --- /dev/null +++ b/src/main/java/net/fabricmc/meta/web/Endpoint.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.web; + +import java.time.Duration; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.javalin.apibuilder.EndpointGroup; +import io.javalin.http.ContentType; +import io.javalin.http.Context; +import io.javalin.http.Handler; + +import net.fabricmc.meta.data.DataProvider; + +public abstract class Endpoint { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + protected final DataProvider dataProvider; + + protected Endpoint(DataProvider dataProvider) { + this.dataProvider = dataProvider; + } + + public abstract EndpointGroup routes(); + + protected Handler cache(Duration duration) { + return ctx -> { + ctx.header("Cache-Control", "public, max-age=" + duration.getSeconds()); + }; + } + + // Return a json list with no params + protected Handler result(JsonListHandler handler) { + return ctx -> { + List result = handler.apply(dataProvider); + jsonResult(ctx, result); + }; + } + + // Return a json list with one string param + protected Handler result(String key, JsonListHandler1 handler) { + return ctx -> { + final String value = ctx.pathParamAsClass(key, String.class).get(); + List result = handler.apply(dataProvider, value); + jsonResult(ctx, result); + }; + } + + // Return a json list with two string params + protected Handler result(String key1, String key2, JsonListHandler2 handler) { + return ctx -> { + final String value1 = ctx.pathParamAsClass(key1, String.class).get(); + final String value2 = ctx.pathParamAsClass(key2, String.class).get(); + List result = handler.apply(dataProvider, value1, value2); + jsonResult(ctx, result); + }; + } + + // Return a json list with two string params + protected Handler result(String key1, String key2, JsonHandler2 handler) { + return ctx -> { + final String value1 = ctx.pathParamAsClass(key1, String.class).get(); + final String value2 = ctx.pathParamAsClass(key2, String.class).get(); + JsonModel result = handler.apply(dataProvider, value1, value2); + jsonResult(ctx, result); + }; + } + + protected void jsonResult(Context ctx, Object result) { + ctx.contentType(ContentType.APPLICATION_JSON); + ctx.result(GSON.toJson(result)); + } + + protected interface JsonListHandler { + List apply(DataProvider dataProvider); + } + + protected interface JsonListHandler1 { + List apply(DataProvider dataProvider, String key1); + } + + protected interface JsonListHandler2 { + List apply(DataProvider dataProvider, String key1, String key2); + } + + protected interface JsonHandler2 { + JsonModel apply(DataProvider dataProvider, String key1, String key2); + } +} diff --git a/src/main/java/net/fabricmc/meta/web/EndpointsV1.java b/src/main/java/net/fabricmc/meta/web/EndpointsV1.java deleted file mode 100644 index 8bb14e3..0000000 --- a/src/main/java/net/fabricmc/meta/web/EndpointsV1.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2019 FabricMC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.fabricmc.meta.web; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import io.javalin.http.Context; - -import net.fabricmc.meta.FabricMeta; -import net.fabricmc.meta.web.models.LoaderInfoV1; -import net.fabricmc.meta.web.models.MavenBuildGameVersion; -import net.fabricmc.meta.web.models.MavenBuildVersion; - -@SuppressWarnings("Duplicates") -public class EndpointsV1 { - public static void setup() { - WebServer.jsonGet("/v1/versions", () -> FabricMeta.database); - - WebServer.jsonGet("/v1/versions/game", () -> FabricMeta.database.game); - WebServer.jsonGet("/v1/versions/game/{game_version}", context -> filter(context, FabricMeta.database.game)); - - WebServer.jsonGet("/v1/versions/mappings", () -> FabricMeta.database.mappings); - WebServer.jsonGet("/v1/versions/mappings/{game_version}", context -> filter(context, FabricMeta.database.mappings)); - - WebServer.jsonGet("/v1/versions/loader", () -> FabricMeta.database.getLoader()); - WebServer.jsonGet("/v1/versions/loader/{game_version}", EndpointsV1::getLoaderInfoAll); - WebServer.jsonGet("/v1/versions/loader/{game_version}/{loader_version}", EndpointsV1::getLoaderInfo); - } - - private static > List filter(Context context, List versionList) { - if (!context.pathParamMap().containsKey("game_version")) { - return Collections.emptyList(); - } - - return versionList.stream().filter(t -> t.test(context.pathParam("game_version"))).collect(Collectors.toList()); - } - - private static Object getLoaderInfo(Context context) { - if (!context.pathParamMap().containsKey("game_version")) { - return null; - } - - if (!context.pathParamMap().containsKey("loader_version")) { - return null; - } - - String gameVersion = context.pathParam("game_version"); - String loaderVersion = context.pathParam("loader_version"); - - MavenBuildVersion loader = FabricMeta.database.getAllLoader().stream() - .filter(mavenBuildVersion -> loaderVersion.equals(mavenBuildVersion.getVersion())) - .findFirst().orElse(null); - - MavenBuildGameVersion mappings = FabricMeta.database.mappings.stream() - .filter(t -> t.test(gameVersion)) - .findFirst().orElse(null); - - if (loader == null) { - context.status(400); - return "no loader version found for " + gameVersion; - } - - if (mappings == null) { - context.status(400); - return "no mappings version found for " + gameVersion; - } - - return new LoaderInfoV1(loader, mappings).populateMeta(); - } - - private static Object getLoaderInfoAll(Context context) { - if (!context.pathParamMap().containsKey("game_version")) { - return null; - } - - String gameVersion = context.pathParam("game_version"); - - MavenBuildGameVersion mappings = FabricMeta.database.mappings.stream() - .filter(t -> t.test(gameVersion)) - .findFirst().orElse(null); - - if (mappings == null) { - return Collections.emptyList(); - } - - List infoList = new ArrayList<>(); - - for (MavenBuildVersion loader : FabricMeta.database.getLoader()) { - infoList.add(new LoaderInfoV1(loader, mappings)); - } - - return infoList; - } -} diff --git a/src/main/java/net/fabricmc/meta/web/JsonModel.java b/src/main/java/net/fabricmc/meta/web/JsonModel.java new file mode 100644 index 0000000..2bdcf0d --- /dev/null +++ b/src/main/java/net/fabricmc/meta/web/JsonModel.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.web; + +// A marker interface for all records that are serialized to JSON as part of the public api. +public interface JsonModel { +} diff --git a/src/main/java/net/fabricmc/meta/web/ProfileHandler.java b/src/main/java/net/fabricmc/meta/web/ProfileHandler.java index 8cc3ced..0935fec 100644 --- a/src/main/java/net/fabricmc/meta/web/ProfileHandler.java +++ b/src/main/java/net/fabricmc/meta/web/ProfileHandler.java @@ -35,8 +35,9 @@ import com.google.gson.JsonObject; import org.apache.commons.io.IOUtils; +import net.fabricmc.meta.models.LoaderInfoV2; import net.fabricmc.meta.utils.Reference; -import net.fabricmc.meta.web.models.LoaderInfoV2; +import net.fabricmc.meta.web.v2.EndpointsV2; public class ProfileHandler { private static final Executor EXECUTOR = Executors.newFixedThreadPool(2); diff --git a/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java b/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java index 2f58149..1292095 100644 --- a/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java +++ b/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java @@ -41,8 +41,8 @@ import org.apache.commons.io.FileUtils; import net.fabricmc.meta.FabricMeta; +import net.fabricmc.meta.models.BaseVersion; import net.fabricmc.meta.utils.Reference; -import net.fabricmc.meta.web.models.BaseVersion; public class ServerBootstrap { private static final Path CACHE_DIR = Paths.get("metadata", "installer"); @@ -62,7 +62,7 @@ private static Handler boostrapHandler() { final String installerVersion = getAndValidateVersion(ctx, FabricMeta.database.installer, "installer_version"); final String gameVersion = getAndValidateVersion(ctx, FabricMeta.database.game, "game_version"); - final String loaderVersion = getAndValidateVersion(ctx, FabricMeta.database.getAllLoader(), "loader_version"); + final String loaderVersion = getAndValidateVersion(ctx, FabricMeta.database.getLoader(), "loader_version"); validateLoaderVersion(loaderVersion); validateInstallerVersion(installerVersion); diff --git a/src/main/java/net/fabricmc/meta/web/WebServer.java b/src/main/java/net/fabricmc/meta/web/WebServer.java index aa514b5..d2a535d 100644 --- a/src/main/java/net/fabricmc/meta/web/WebServer.java +++ b/src/main/java/net/fabricmc/meta/web/WebServer.java @@ -16,6 +16,10 @@ package net.fabricmc.meta.web; +import static io.javalin.apibuilder.ApiBuilder.after; +import static io.javalin.apibuilder.ApiBuilder.before; +import static io.javalin.apibuilder.ApiBuilder.path; + import java.util.function.Function; import java.util.function.Supplier; @@ -24,34 +28,53 @@ import io.javalin.Javalin; import io.javalin.http.Context; import io.javalin.http.Header; +import io.javalin.plugin.bundled.CorsPlugin; import io.javalin.plugin.bundled.CorsPluginConfig; +import io.javalin.plugin.bundled.RouteOverviewPlugin; + +import net.fabricmc.meta.data.DataProvider; +import net.fabricmc.meta.web.v1.EndpointsV1; +import net.fabricmc.meta.web.v2.EndpointsV2; public class WebServer { + @Deprecated(forRemoval = true) public static Javalin javalin; - public static Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - public static Javalin create() { - if (javalin != null) { - javalin.stop(); - } + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private final DataProvider dataProvider; + private final CacheHandler cacheHandler; + + private final EndpointsV1 endpointsV1; + + public WebServer(DataProvider dataProvider, CacheHandler cacheHandler) { + this.dataProvider = dataProvider; + this.cacheHandler = cacheHandler; - javalin = Javalin.create(config -> { - config.plugins.enableRouteOverview("/"); + endpointsV1 = new EndpointsV1(dataProvider); + } + + public Javalin createServer() { + Javalin javalin = Javalin.create(config -> { + config.useVirtualThreads = true; config.showJavalinBanner = false; - config.plugins.enableCors(cors -> cors.add(CorsPluginConfig::anyHost)); + config.registerPlugin(new RouteOverviewPlugin(routeOverview -> routeOverview.path = "/")); + config.registerPlugin(new CorsPlugin(cors -> cors.addRule(CorsPluginConfig.CorsRule::anyHost))); + + config.router.apiBuilder(() -> { + before(cacheHandler.before()); + after(cacheHandler.after()); + path("v1", endpointsV1.routes()); + }); }); - EndpointsV1.setup(); + // TODO remove this + WebServer.javalin = javalin; EndpointsV2.setup(); return javalin; } - public static void start() { - assert javalin == null; - create().start(5555); - } - public static void jsonGet(String route, Supplier supplier) { javalin.get(route, ctx -> { T object = supplier.get(); diff --git a/src/main/java/net/fabricmc/meta/web/v1/EndpointsV1.java b/src/main/java/net/fabricmc/meta/web/v1/EndpointsV1.java new file mode 100644 index 0000000..83bbe45 --- /dev/null +++ b/src/main/java/net/fabricmc/meta/web/v1/EndpointsV1.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.web.v1; + +import static io.javalin.apibuilder.ApiBuilder.before; +import static io.javalin.apibuilder.ApiBuilder.get; + +import java.time.Duration; + +import io.javalin.apibuilder.EndpointGroup; + +import net.fabricmc.meta.data.DataProvider; +import net.fabricmc.meta.web.Endpoint; + +public class EndpointsV1 extends Endpoint { + public EndpointsV1(DataProvider dataProvider) { + super(dataProvider); + } + + @Override + public EndpointGroup routes() { + return () -> { + before(cache(Duration.ofMinutes(1))); + + get("versions", ctx -> jsonResult(ctx, dataProvider.getVersionDatabase())); + get("versions/game", result(ModelsV1::gameVersions)); + get("versions/game/{game_version}", result("game_version", ModelsV1::gameVersions)); + get("versions/mappings", result(ModelsV1::mappingVersions)); + get("versions/mappings/{game_version}", result("game_version", ModelsV1::mappingVersions)); + get("versions/loader", result(ModelsV1::loaderVersions)); + get("versions/loader/{game_version}", result("game_version", ModelsV1::loaderInfo)); + get("versions/loader/{game_version}/{loader_version}", result("game_version", "loader_version", ModelsV1::loaderLauncherInfo)); + }; + } +} diff --git a/src/main/java/net/fabricmc/meta/web/v1/ModelsV1.java b/src/main/java/net/fabricmc/meta/web/v1/ModelsV1.java new file mode 100644 index 0000000..046a236 --- /dev/null +++ b/src/main/java/net/fabricmc/meta/web/v1/ModelsV1.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.web.v1; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import com.google.gson.JsonObject; +import io.javalin.http.BadRequestResponse; +import io.javalin.http.InternalServerErrorResponse; + +import net.fabricmc.meta.data.DataProvider; +import net.fabricmc.meta.models.BaseVersion; +import net.fabricmc.meta.models.MavenBuildGameVersion; +import net.fabricmc.meta.models.MavenBuildVersion; +import net.fabricmc.meta.web.JsonModel; + +/** + * Strongly defined records of the public API. Take extra care when changing method return types and record components. + */ +public sealed interface ModelsV1 extends JsonModel permits ModelsV1.GameVersion, ModelsV1.MappingVersion, ModelsV1.LoaderVersion, ModelsV1.LoaderInfo, ModelsV1.LoaderLauncherInfo { + /** + * /v1/versions/game + */ + static List gameVersions(DataProvider dataProvider) { + LinkedList versions = new LinkedList<>(); + + for (BaseVersion version : dataProvider.getGameVersions()) { + versions.add(GameVersion.from(version)); + } + + return versions; + } + + /** + * /v1/game/{game_version} + */ + static List gameVersions(DataProvider dataProvider, String gameVersion) { + for (BaseVersion version : dataProvider.getGameVersions()) { + if (version.getVersion().equals(gameVersion)) { + return List.of(GameVersion.from(version)); + } + } + + return Collections.emptyList(); + } + + /** + * /v1/versions/mappings + */ + static List mappingVersions(DataProvider dataProvider) { + LinkedList versions = new LinkedList<>(); + + for (MavenBuildGameVersion version : dataProvider.getMappingVersions()) { + versions.add(MappingVersion.from(version)); + } + + return versions; + } + + /** + * /v1/mappings/{game_version} + */ + static List mappingVersions(DataProvider dataProvider, String gameVersion) { + LinkedList versions = new LinkedList<>(); + + for (MavenBuildGameVersion version : dataProvider.getMappingVersions()) { + if (version.getGameVersion().equals(gameVersion)) { + versions.add(MappingVersion.from(version)); + } + } + + return versions; + } + + /** + * /v1/versions/loader + */ + static List loaderVersions(DataProvider dataProvider) { + LinkedList versions = new LinkedList<>(); + + for (MavenBuildVersion version : dataProvider.getLoaderVersions()) { + versions.add(LoaderVersion.from(version)); + } + + return versions; + } + + /** + * /v1/versions/loader/{game_version}/{loader_version} + */ + static List loaderInfo(DataProvider dataProvider, String gameVersion) { + MavenBuildGameVersion mappings = null; + + for (MavenBuildGameVersion version : dataProvider.getMappingVersions()) { + if (version.test(gameVersion)) { + mappings = version; + break; + } + } + + if (mappings == null) { + return Collections.emptyList(); + } + + List infoList = new LinkedList<>(); + + for (MavenBuildVersion loader : dataProvider.getLoaderVersions()) { + infoList.add(new LoaderInfo(LoaderVersion.from(loader), MappingVersion.from(mappings))); + } + + return infoList; + } + + /** + * /v1/versions/loader/{game_version}/{loader_version} + */ + static LoaderLauncherInfo loaderLauncherInfo(DataProvider dataProvider, String gameVersion, String loaderVersion) { + MavenBuildVersion loader = null; + MavenBuildGameVersion mappings = null; + + for (MavenBuildVersion version : dataProvider.getLoaderVersions()) { + if (loaderVersion.equals(version.getVersion())) { + loader = version; + break; + } + } + + for (MavenBuildGameVersion version : dataProvider.getMappingVersions()) { + if (version.test(gameVersion)) { + mappings = version; + break; + } + } + + if (loader == null) { + throw new BadRequestResponse("no loader version found for " + gameVersion); + } + + if (mappings == null) { + throw new BadRequestResponse("no mappings version found for " + gameVersion); + } + + final JsonObject installerJson = dataProvider.getLoaderInstallerJson(loader.getMaven()); + + if (installerJson == null) { + throw new InternalServerErrorResponse("Failed to load installer json, report to Fabric"); + } + + return new LoaderLauncherInfo( + LoaderVersion.from(loader), + MappingVersion.from(mappings), + installerJson + ); + } + + record GameVersion(String version, boolean stable) implements ModelsV1 { + private static GameVersion from(BaseVersion version) { + return new GameVersion(version.getVersion(), version.isStable()); + } + } + + record MappingVersion(String gameVersion, String separator, int build, String maven, String version, boolean stable) implements ModelsV1 { + private static MappingVersion from(MavenBuildGameVersion version) { + return new MappingVersion(version.getGameVersion(), version.getSeparator(), version.getBuild(), version.getMaven(), version.getVersion(), version.isStable()); + } + } + + record LoaderVersion(String separator, int build, String maven, String version, boolean stable) implements ModelsV1 { + private static LoaderVersion from(MavenBuildVersion version) { + return new LoaderVersion(version.getSeparator(), version.getBuild(), version.getMaven(), version.getVersion(), version.isStable()); + } + } + + record LoaderInfo(LoaderVersion loader, MappingVersion mappings) implements ModelsV1 { } + + record LoaderLauncherInfo(LoaderVersion loader, MappingVersion mappings, Object launcherMeta) implements ModelsV1 { } +} diff --git a/src/main/java/net/fabricmc/meta/web/EndpointsV2.java b/src/main/java/net/fabricmc/meta/web/v2/EndpointsV2.java similarity index 93% rename from src/main/java/net/fabricmc/meta/web/EndpointsV2.java rename to src/main/java/net/fabricmc/meta/web/v2/EndpointsV2.java index cb488c3..5a55311 100644 --- a/src/main/java/net/fabricmc/meta/web/EndpointsV2.java +++ b/src/main/java/net/fabricmc/meta/web/v2/EndpointsV2.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.meta.web; +package net.fabricmc.meta.web.v2; import java.io.InputStream; import java.util.ArrayList; @@ -30,11 +30,14 @@ import io.javalin.http.Header; import net.fabricmc.meta.FabricMeta; -import net.fabricmc.meta.web.models.BaseVersion; -import net.fabricmc.meta.web.models.LoaderInfoV2; -import net.fabricmc.meta.web.models.MavenBuildGameVersion; -import net.fabricmc.meta.web.models.MavenBuildVersion; -import net.fabricmc.meta.web.models.MavenVersion; +import net.fabricmc.meta.models.BaseVersion; +import net.fabricmc.meta.models.LoaderInfoV2; +import net.fabricmc.meta.models.MavenBuildGameVersion; +import net.fabricmc.meta.models.MavenBuildVersion; +import net.fabricmc.meta.models.MavenVersion; +import net.fabricmc.meta.web.ProfileHandler; +import net.fabricmc.meta.web.ServerBootstrap; +import net.fabricmc.meta.web.WebServer; @SuppressWarnings("Duplicates") public class EndpointsV2 { @@ -98,7 +101,7 @@ private static Object getLoaderInfo(Context context) { String gameVersion = context.pathParam("game_version"); String loaderVersion = context.pathParam("loader_version"); - MavenBuildVersion loader = FabricMeta.database.getAllLoader().stream() + MavenBuildVersion loader = FabricMeta.database.getLoader().stream() .filter(mavenBuildVersion -> loaderVersion.equals(mavenBuildVersion.getVersion())) .findFirst().orElse(null); diff --git a/src/test/java/net/fabricmc/meta/test/TestUtils.java b/src/test/java/net/fabricmc/meta/test/TestUtils.java new file mode 100644 index 0000000..573a113 --- /dev/null +++ b/src/test/java/net/fabricmc/meta/test/TestUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.test; + +import io.javalin.Javalin; + +import net.fabricmc.meta.FabricMeta; +import net.fabricmc.meta.web.CacheHandler; +import net.fabricmc.meta.web.WebServer; + +public class TestUtils { + public static Javalin createServer() { + CacheHandler cacheHandler = new CacheHandler(); + return new WebServer(() -> FabricMeta.database, cacheHandler).createServer(); + } +} diff --git a/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java b/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java index f8b21d3..a35986f 100644 --- a/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java +++ b/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java @@ -32,7 +32,7 @@ import org.junit.jupiter.params.provider.MethodSource; import net.fabricmc.meta.FabricMeta; -import net.fabricmc.meta.web.WebServer; +import net.fabricmc.meta.test.TestUtils; // Tests that the local response matches the remote response of the version in prod public class ComparisonTests { @@ -79,7 +79,7 @@ public static Stream provideEndpoints() { @ParameterizedTest @MethodSource("provideEndpoints") void compareEndpoint(String endpoint) { - JavalinTest.test(WebServer.create(), (server, client) -> { + JavalinTest.test(TestUtils.createServer(), (server, client) -> { compareEndpoint(endpoint, client); }); } diff --git a/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java b/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java index 915000d..d3d3a78 100644 --- a/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java +++ b/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import net.fabricmc.meta.FabricMeta; -import net.fabricmc.meta.web.WebServer; +import net.fabricmc.meta.test.TestUtils; public class EndpointsV2Tests { @BeforeAll @@ -35,7 +35,7 @@ static void beforeAll() { @Test void versions() { - JavalinTest.test(WebServer.create(), (server, client) -> { + JavalinTest.test(TestUtils.createServer(), (server, client) -> { Response response = client.get("/v2/versions"); assertEquals(200, response.code()); String body = response.body().string(); diff --git a/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java b/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java index 91bb015..261f45a 100644 --- a/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java +++ b/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java @@ -29,7 +29,7 @@ import org.junit.jupiter.api.io.TempDir; import net.fabricmc.meta.FabricMeta; -import net.fabricmc.meta.web.WebServer; +import net.fabricmc.meta.test.TestUtils; public class ServerBootstrapTests { @TempDir @@ -43,7 +43,7 @@ static void beforeAll() { @Test void serverJar() { - JavalinTest.test(WebServer.create(), (server, client) -> { + JavalinTest.test(TestUtils.createServer(), (server, client) -> { Response response = client.get("/v2/versions/loader/stable/stable/stable/server/jar"); assertEquals(200, response.code()); Path jarFile = tempDir.resolve("server.jar");