From 0c768339973d981b347bc47288c3ecbc21dab962 Mon Sep 17 00:00:00 2001 From: Hannes Greule Date: Sat, 2 Jan 2021 16:44:04 +0100 Subject: [PATCH] Implement CaptionLoader API to be used by third party plugins/addons This allows to load resources from other classloaders than the P2 one. Therefore, we can use this in addons too to manage messages the same way. --- .../com/plotsquared/core/PlotSquared.java | 13 +- .../caption/{ => load}/CaptionLoader.java | 140 +++++++++++------- .../load/ClassLoaderCaptionProvider.java | 70 +++++++++ .../caption/load/DefaultCaptionProvider.java | 71 +++++++++ 4 files changed, 239 insertions(+), 55 deletions(-) rename Core/src/main/java/com/plotsquared/core/configuration/caption/{ => load}/CaptionLoader.java (57%) create mode 100644 Core/src/main/java/com/plotsquared/core/configuration/caption/load/ClassLoaderCaptionProvider.java create mode 100644 Core/src/main/java/com/plotsquared/core/configuration/caption/load/DefaultCaptionProvider.java diff --git a/Core/src/main/java/com/plotsquared/core/PlotSquared.java b/Core/src/main/java/com/plotsquared/core/PlotSquared.java index 5440d1464..0eeb67517 100644 --- a/Core/src/main/java/com/plotsquared/core/PlotSquared.java +++ b/Core/src/main/java/com/plotsquared/core/PlotSquared.java @@ -30,10 +30,11 @@ import com.plotsquared.core.configuration.ConfigurationUtil; import com.plotsquared.core.configuration.MemorySection; import com.plotsquared.core.configuration.Settings; import com.plotsquared.core.configuration.Storage; -import com.plotsquared.core.configuration.caption.CaptionLoader; +import com.plotsquared.core.configuration.caption.load.CaptionLoader; import com.plotsquared.core.configuration.caption.CaptionMap; import com.plotsquared.core.configuration.caption.DummyCaptionMap; import com.plotsquared.core.configuration.caption.TranslatableCaption; +import com.plotsquared.core.configuration.caption.load.DefaultCaptionProvider; import com.plotsquared.core.configuration.file.YamlConfiguration; import com.plotsquared.core.configuration.serialization.ConfigurationSerialization; import com.plotsquared.core.database.DBFunc; @@ -108,6 +109,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -139,6 +141,7 @@ public class PlotSquared { public HashMap> plots_tmp; private YamlConfiguration config; // Localization + private final CaptionLoader captionLoader; private final Map captionMaps = new HashMap<>(); // Platform / Version / Update URL private PlotVersion version; @@ -172,6 +175,10 @@ public class PlotSquared { // ConfigurationSerialization.registerClass(BlockBucket.class, "BlockBucket"); + this.captionLoader = CaptionLoader.of(Locale.ENGLISH, + CaptionLoader.patternExtractor(Pattern.compile("messages_(.*)\\.json")), + DefaultCaptionProvider.forClassLoaderFormatString(this.getClass().getClassLoader(), + "lang/messages_%s.json")); // the path in our jar file // Load caption map try { this.loadCaptionMap(); @@ -232,10 +239,10 @@ public class PlotSquared { // Setup localization CaptionMap captionMap; if (Settings.Enabled_Components.PER_USER_LOCALE) { - captionMap = CaptionLoader.loadAll(new File(this.platform.getDirectory(), "lang").toPath()); + captionMap = this.captionLoader.loadAll(this.platform.getDirectory().toPath().resolve("lang")); } else { String fileName = "messages_" + Settings.Enabled_Components.DEFAULT_LOCALE + ".json"; - captionMap = CaptionLoader.loadSingle(new File(new File(this.platform.getDirectory(), "lang"), fileName).toPath()); + captionMap = this.captionLoader.loadSingle(this.platform.getDirectory().toPath().resolve("lang").resolve(fileName)); } this.captionMaps.put(TranslatableCaption.DEFAULT_NAMESPACE, captionMap); logger.info("Loaded caption map for namespace 'plotsquared': {}", diff --git a/Core/src/main/java/com/plotsquared/core/configuration/caption/CaptionLoader.java b/Core/src/main/java/com/plotsquared/core/configuration/caption/load/CaptionLoader.java similarity index 57% rename from Core/src/main/java/com/plotsquared/core/configuration/caption/CaptionLoader.java rename to Core/src/main/java/com/plotsquared/core/configuration/caption/load/CaptionLoader.java index a0c86e9e2..95cbd75c0 100644 --- a/Core/src/main/java/com/plotsquared/core/configuration/caption/CaptionLoader.java +++ b/Core/src/main/java/com/plotsquared/core/configuration/caption/load/CaptionLoader.java @@ -23,20 +23,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.plotsquared.core.configuration.caption; +package com.plotsquared.core.configuration.caption.load; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.plotsquared.core.configuration.caption.CaptionMap; +import com.plotsquared.core.configuration.caption.LocalizedCaptionMap; +import com.plotsquared.core.configuration.caption.PerUserLocaleCaptionMap; +import com.plotsquared.core.configuration.caption.TranslatableCaption; +import org.checkerframework.checker.nullness.qual.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -48,6 +50,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -59,29 +62,49 @@ import java.util.stream.Stream; public final class CaptionLoader { private static final Logger logger = LoggerFactory.getLogger("P2/" + CaptionLoader.class.getSimpleName()); - private static final Map DEFAULT_MESSAGES; - private static final Locale DEFAULT_LOCALE; private static final Gson GSON; - private static final Pattern FILE_NAME_PATTERN; + + private final Map defaultMessages; + private final Locale defaultLocale; + private final Function localeExtractor; + private final DefaultCaptionProvider captionProvider; static { - FILE_NAME_PATTERN = Pattern.compile("messages_(.*)\\.json"); GSON = new GsonBuilder() .setPrettyPrinting() .disableHtmlEscaping() .create(); - DEFAULT_LOCALE = Locale.ENGLISH; + } + + /** + * Returns a new CaptionLoader instance. That instance will use the internalLocale to extract default values + * from the captionProvider + * + * @param internalLocale the locale used internally to resolve default messages from the caption provider. + * @param localeExtractor a function to extract a locale from a path, e.g. by its name. + * @param captionProvider the provider for default captions. + * @return a CaptionLoader instance that can load and patch message files. + */ + public static CaptionLoader of(final @NonNull Locale internalLocale, + final @NonNull Function<@NonNull Path, @NonNull Locale> localeExtractor, + final @NonNull DefaultCaptionProvider captionProvider) { + return new CaptionLoader(internalLocale, localeExtractor, captionProvider); + } + + private CaptionLoader(final @NonNull Locale internalLocale, + final @NonNull Function<@NonNull Path, @NonNull Locale> localeExtractor, + final @NonNull DefaultCaptionProvider captionProvider) { + this.defaultLocale = internalLocale; + this.localeExtractor = localeExtractor; + this.captionProvider = captionProvider; Map temp; try { - temp = loadResource(DEFAULT_LOCALE); + temp = this.captionProvider.loadDefaults(internalLocale); } catch (Exception e) { logger.error("Failed to load default messages", e); temp = Collections.emptyMap(); } - DEFAULT_MESSAGES = temp; - } - - private CaptionLoader() { + this.defaultMessages = temp; } /** @@ -93,7 +116,7 @@ public final class CaptionLoader { * @see Files#list(Path) * @see #loadSingle(Path) */ - @Nonnull public static CaptionMap loadAll(@Nonnull final Path directory) throws IOException { + public @NonNull CaptionMap loadAll(final @NonNull Path directory) throws IOException { final Map localeMaps = new HashMap<>(); try (final Stream files = Files.list(directory)) { final List captionFiles = files.filter(Files::isRegularFile).collect(Collectors.toList()); @@ -117,18 +140,11 @@ public final class CaptionLoader { * * @param file The file to load * @return A new CaptionMap containing the loaded messages - * @throws IOException if the file couldn't be accessed or read successfully. + * @throws IOException if the file couldn't be accessed or read successfully. * @throws IllegalArgumentException if the file name doesn't match the specified format. */ - @Nonnull public static CaptionMap loadSingle(@Nonnull final Path file) throws IOException { - final String fileName = file.getFileName().toString(); - final Matcher matcher = FILE_NAME_PATTERN.matcher(fileName); - final Locale locale; - if (matcher.matches()) { - locale = Locale.forLanguageTag(matcher.group(1)); - } else { - throw new IllegalArgumentException(fileName + " is an invalid message file (cannot extract locale)"); - } + public @NonNull CaptionMap loadSingle(final @NonNull Path file) throws IOException { + final Locale locale = this.localeExtractor.apply(file); try (final BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { Map map = loadFromReader(reader); if (patch(map, locale)) { @@ -139,29 +155,26 @@ public final class CaptionLoader { } } + /** + * Loads a map of translation keys mapping to their translations from a reader. + * The format is expected to be a json object: + *
{@code
+     * {
+     *     "key1": "value a",
+     *     "key2": "value b",
+     *     ...
+     * }
+     * }
+ * + * @param reader the reader to read the map from. + * @return the translation map. + */ @SuppressWarnings("UnstableApiUsage") - private static Map loadFromReader(final Reader reader) { + static Map loadFromReader(final Reader reader) { final Type type = new TypeToken>() {}.getType(); return new LinkedHashMap<>(GSON.fromJson(reader, type)); } - private static Map loadResource(final Locale locale) { - final String url = String.format("lang/messages_%s.json", locale.toString()); - try { - final InputStream stream = CaptionLoader.class.getClassLoader().getResourceAsStream(url); - if (stream == null) { - logger.warn("No resource for locale '{}' found", locale); - return null; - } - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - return loadFromReader(reader); - } - } catch (final IOException e) { - logger.error("Unable to load language resource", e); - return null; - } - } - private static void save(final Path file, final Map content) { try (final BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { GSON.toJson(content, writer); @@ -173,27 +186,27 @@ public final class CaptionLoader { /** * Add missing entries to the given map. - * Entries are missing if the key exists in {@link #DEFAULT_MESSAGES} but isn't present + * Entries are missing if the key exists in {@link #defaultLocale} but isn't present * in the given map. For a missing key, a value will be loaded either from - * the resource matching the given locale or from {@link #DEFAULT_MESSAGES} if + * the resource matching the given locale or from {@link #defaultLocale} if * no matching resource was found or the key isn't present in the resource. * * @param map the map to patch * @param locale the locale to get the resource from * @return {@code true} if the map was patched. */ - private static boolean patch(final Map map, final Locale locale) { + private boolean patch(final Map map, final Locale locale) { boolean modified = false; Map languageSpecific; - if (locale.equals(DEFAULT_LOCALE)) { - languageSpecific = DEFAULT_MESSAGES; + if (locale.equals(this.defaultLocale)) { + languageSpecific = this.defaultMessages; } else { - languageSpecific = loadResource(locale); - if (languageSpecific == null) { // fallback for languages not provided by PlotSquared - languageSpecific = DEFAULT_MESSAGES; + languageSpecific = this.captionProvider.loadDefaults(locale); + if (languageSpecific == null) { // fallback for languages not provided + languageSpecific = this.defaultMessages; } } - for (Map.Entry entry : DEFAULT_MESSAGES.entrySet()) { + for (Map.Entry entry : this.defaultMessages.entrySet()) { if (!map.containsKey(entry.getKey())) { final String value = languageSpecific.getOrDefault(entry.getKey(), entry.getValue()); map.put(entry.getKey(), value); @@ -202,4 +215,27 @@ public final class CaptionLoader { } return modified; } + + /** + * Returns a function that extracts a locale from a path using the given pattern. + * The pattern is required to have (at least) one capturing group, as this is used to access the locale + * tag.The function will throw an {@link IllegalArgumentException} if the matcher doesn't match the file name + * of the input path. The language tag is loaded using {@link Locale#forLanguageTag(String)}. + * + * @param pattern the pattern to match and extract the language tag with. + * @return a function to extract a locale from a path using a pattern. + * @see Matcher#group(int) + * @see Path#getFileName() + */ + public static @NonNull Function patternExtractor(final @NonNull Pattern pattern) { + return path -> { + final String fileName = path.getFileName().toString(); + final Matcher matcher = pattern.matcher(fileName); + if (matcher.matches()) { + return Locale.forLanguageTag(matcher.group(1)); + } else { + throw new IllegalArgumentException(fileName + " is an invalid message file (cannot extract locale)"); + } + }; + } } diff --git a/Core/src/main/java/com/plotsquared/core/configuration/caption/load/ClassLoaderCaptionProvider.java b/Core/src/main/java/com/plotsquared/core/configuration/caption/load/ClassLoaderCaptionProvider.java new file mode 100644 index 000000000..9189f6a0c --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/configuration/caption/load/ClassLoaderCaptionProvider.java @@ -0,0 +1,70 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2021 IntellectualSites + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.configuration.caption.load; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import static com.plotsquared.core.configuration.caption.load.CaptionLoader.loadFromReader; + +class ClassLoaderCaptionProvider implements DefaultCaptionProvider { + private static final Logger logger = LoggerFactory.getLogger("P2/" + ClassLoaderCaptionProvider.class.getSimpleName()); + private final ClassLoader classLoader; + private final Function urlProvider; + + ClassLoaderCaptionProvider(ClassLoader classLoader, Function urlProvider) { + this.classLoader = classLoader; + this.urlProvider = urlProvider; + } + + @Override + public @Nullable Map loadDefaults(final @NonNull Locale locale) { + final String url = this.urlProvider.apply(locale); + try { + final InputStream stream = this.classLoader.getResourceAsStream(url); + if (stream == null) { + logger.warn("No resource for locale '{}' found", locale); + return null; + } + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + return loadFromReader(reader); + } + } catch (final IOException e) { + logger.error("Unable to load language resource", e); + return null; + } + } +} diff --git a/Core/src/main/java/com/plotsquared/core/configuration/caption/load/DefaultCaptionProvider.java b/Core/src/main/java/com/plotsquared/core/configuration/caption/load/DefaultCaptionProvider.java new file mode 100644 index 000000000..7944f3a12 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/configuration/caption/load/DefaultCaptionProvider.java @@ -0,0 +1,71 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2021 IntellectualSites + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.configuration.caption.load; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +public interface DefaultCaptionProvider { + + /** + * Returns a DefaultCaptionProvider that loads captions from a {@link ClassLoader}'s resources. + * The resource urls are determined by applying the given function to a locale. + * + * @param classLoader the class loader to load caption resources from. + * @param urlProvider the function to get an url from a locale. + * @return a caption provider using a function to determine resource urls. + */ + static DefaultCaptionProvider forClassLoader(ClassLoader classLoader, Function urlProvider) { + return new ClassLoaderCaptionProvider(classLoader, urlProvider); + } + + /** + * Returns a DefaultCaptionProvider that loads captions from a {@link ClassLoader}'s resources. + * The resource urls are determined by replacing the first occurrence of {@code %s} in the string with + * {@link Locale#toString()}. + * + * @param classLoader the class loader to load caption resources from. + * @param toFormat a string that can be formatted to result in a valid resource url when calling + * {@code String.format(toFormat, Locale#toString)} + * @return a caption provider using string formatting to determine resource urls. + */ + static DefaultCaptionProvider forClassLoaderFormatString(ClassLoader classLoader, String toFormat) { + return forClassLoader(classLoader, locale -> String.format(toFormat, locale.toString())); + } + + /** + * Loads default translation values for a specific language and returns it as a map. + * If no default translation exists, {@link null} is returned. A returned map might be empty. + * + * @param locale the locale to load the values for. + * @return a map of default values for the given locale. + */ + @Nullable Map loadDefaults(final @NonNull Locale locale); +}