/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.script; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import groovy.lang.GroovyClassLoader; import org.codehaus.groovy.control.CompilationFailedException; import org.lanternpowered.api.script.Script; import org.lanternpowered.api.script.ScriptGameRegistry; import org.lanternpowered.api.script.ScriptObjectTypes; import org.lanternpowered.server.asset.AssetRepository; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.script.json.JsonSerializers; import org.lanternpowered.server.script.transformer.AdditionalImportsScriptTransformer; import org.lanternpowered.server.script.transformer.FirstSuccessTransformer; import org.lanternpowered.server.script.transformer.ImportsCollectorTransformer; import org.lanternpowered.server.script.transformer.ReferencedScriptTransformer; import org.lanternpowered.server.script.transformer.RelocatedMethodScriptTransformer; import org.lanternpowered.server.script.transformer.RelocatedScriptTransformer; import org.lanternpowered.server.script.transformer.ScriptTransformerContext; import org.lanternpowered.server.script.transformer.SequentialTransformer; import org.lanternpowered.server.script.transformer.SimpleScriptTransformer; import org.lanternpowered.server.script.transformer.StripPackageNameTransformer; import org.lanternpowered.server.script.transformer.Transformer; import org.lanternpowered.server.script.transformer.TransformerException; import org.lanternpowered.server.script.transformer.TransformerUtil; import org.lanternpowered.server.world.weather.WeatherBuilder; import org.lanternpowered.server.world.weather.WeatherBuilderJsonSerializer; import org.spongepowered.api.CatalogType; import org.spongepowered.api.asset.Asset; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; public class LanternScriptGameRegistry implements ScriptGameRegistry { private static final LanternScriptGameRegistry instance = new LanternScriptGameRegistry(); public static LanternScriptGameRegistry get() { return instance; } private final ScriptFunctionGenerator scriptFunctionGenerator = new ScriptFunctionGenerator(); private final Transformer transformerPipeline = SequentialTransformer.of( FirstSuccessTransformer.of( new RelocatedScriptTransformer(), new RelocatedMethodScriptTransformer(), new SimpleScriptTransformer() ), new StripPackageNameTransformer(), new ImportsCollectorTransformer(), new ReferencedScriptTransformer(), new AdditionalImportsScriptTransformer() ); private final Map<String, LanternScript<Object>> assetScripts = new ConcurrentHashMap<>(); private final Map<String, LanternScript<Object>> functionAssetScripts = new ConcurrentHashMap<>(); private final GroovyClassLoader classLoader; private final Map<Class<?>, Class<?>> constructorClasses = ImmutableMap.<Class<?>, Class<?>>builder() .put(ScriptObjectTypes.WEATHER, WeatherBuilder.class) .build(); private final Gson gson = JsonSerializers.register(new GsonBuilder()) .registerTypeAdapter(WeatherBuilder.class, new WeatherBuilderJsonSerializer()) .setPrettyPrinting() .create(); private LanternScriptGameRegistry() { this.classLoader = new GroovyClassLoader(this.getClass().getClassLoader()); } /** * Gets the {@link LanternScript} instance for the * specified {@link Asset} id. * * @param id The asset id * @return The script */ public Optional<LanternScript<Object>> getScript(String id) { return Optional.ofNullable(this.assetScripts.get(id.toLowerCase(Locale.ENGLISH))); } @Override public <T extends CatalogType> T register(Asset asset, Class<T> objectType) { return null; } @Override public <T extends CatalogType> T register(Asset asset, String id, Class<T> objectType) { return null; } @Override public <T extends CatalogType> T register(Object plugin, String asset, Class<T> objectType) { return null; } @Override public <T extends CatalogType> T register(Object plugin, String asset, String id, Class<T> objectType) { return null; } public <T extends CatalogType> T construct(Object plugin, String asset, String id, Class<T> objectType) { final AssetRepository assetRepository = Lantern.getAssetRepository(); final Asset theAsset = assetRepository.get(plugin, asset).orElseThrow( () -> new IllegalArgumentException("There is no asset with the specified id: " + asset)); return this.construct(theAsset, id, objectType); } public <T extends CatalogType> T construct(Asset asset, String id, Class<T> objectType) { checkNotNull(asset, "asset"); checkNotNull(objectType, "objectType"); checkArgument(this.constructorClasses.containsKey(objectType), "The object type %s isn't supported!", objectType.getName()); final Class<?> constructorClass = this.constructorClasses.get(objectType); final CatalogTypeConstructor<T> constructor; try (final InputStream is = asset.getUrl().openStream()) { final InputStreamReader reader = new InputStreamReader(is); constructor = (CatalogTypeConstructor<T>) this.gson.fromJson(reader, constructorClass); } catch (IOException e) { throw new IllegalArgumentException(e); } final String pluginId = asset.getOwner().getId(); return constructor.create(pluginId, id); } @SuppressWarnings("unchecked") @Override public <T> Script<T> compile(Asset asset, Class<T> function) { final String id = ((org.lanternpowered.api.asset.Asset) asset).getId(); return (Script<T>) this.functionAssetScripts.computeIfAbsent(id, id0 -> { try { return this.compileScript(Joiner.on('\n').join(asset.readLines()), (ScriptFunctionMethod) ScriptFunctionMethod.of(function), asset, null); } catch (IOException e) { throw new IllegalArgumentException("Failed to read the asset data: " + id0); } }); } @Override public <T> Script<T> compile(String scriptSource, Class<T> function) { checkNotNull(scriptSource, "scriptSource"); return this.compileScript(scriptSource, ScriptFunctionMethod.of(function), null, null); } @Override public <T> Script<T> compile(Object plugin, String asset, Class<T> function) { final AssetRepository assetRepository = Lantern.getAssetRepository(); final Asset theAsset = assetRepository.get(plugin, asset).orElseThrow( () -> new IllegalArgumentException("There is no asset with the specified id: " + asset)); return this.compile(theAsset, function); } @Override public Script<Object> compile(Asset asset) { final String id = ((org.lanternpowered.api.asset.Asset) asset).getId(); return this.assetScripts.computeIfAbsent(id, id0 -> { try { return this.compileScript(Joiner.on('\n').join(asset.readLines()), null, asset, null); } catch (IOException e) { throw new IllegalArgumentException("Failed to read the asset data: " + id0); } }); } @Override public Script<Object> compile(String scriptSource) { checkNotNull(scriptSource, "scriptSource"); return this.compileScript(scriptSource, null, null, null); } @Override public Script<Object> compile(Object plugin, String asset) { final AssetRepository assetRepository = Lantern.getAssetRepository(); final Asset theAsset = assetRepository.get(plugin, asset).orElseThrow( () -> new IllegalArgumentException("There is no asset with the specified id: " + asset + " for the specified plugin:" + plugin)); return this.compile(theAsset); } private Script<Object> compile0(String assetId) { final AssetRepository assetRepository = Lantern.getAssetRepository(); final Asset theAsset = assetRepository.get(assetId).orElseThrow( () -> new IllegalArgumentException("There is no asset with the specified id: " + assetId)); return this.compile(theAsset); } private <F> LanternScript<F> compileScript(String code, @Nullable ScriptFunctionMethod<F> functionMethod, @Nullable Asset asset, @Nullable Script<F> script) { final TransformedScript transformedScript = this.transformScript(code, functionMethod, asset == null ? null : ((org.lanternpowered.api.asset.Asset) asset).getId()); transformedScript.getDependencies().forEach(this::compile0); final Class<?> theClass; try { theClass = this.classLoader.parseClass(transformedScript.getCode(), transformedScript.getClassName()); } catch (CompilationFailedException e) { throw new IllegalArgumentException("Failed to compile the script source.\nOriginal code:\n``\n" + code + "\n``\nTransformed code:\n``\n" + transformedScript.getCode() + "\n``", e); } LanternScript<F> script1 = (LanternScript<F>) script; if (script1 == null) { script1 = new LanternScript<>(code); script1.setAsset((org.lanternpowered.api.asset.Asset) asset); if (functionMethod != null) { script1.setProxyFunction((ScriptFunction) this.scriptFunctionGenerator.get(functionMethod).get(script1)); } script1.setFunctionMethod(functionMethod); } try { script1.setFunction(theClass.newInstance()); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalArgumentException("Failed to instantiate the script object", e); } return script1; } /** * Transforms the source into something compilable. * * @param code The code * @return The transformed script */ private TransformedScript transformScript(String code, @Nullable ScriptFunctionMethod functionMethod, @Nullable String asset) { final String className; if (asset != null) { className = TransformerUtil.generateClassNameFromAssetPath(asset); } else { final String name = LanternScript.class.getName(); className = name.substring(0, name.lastIndexOf('.')) + ".gen.UnknownScript" + UUID.randomUUID().toString().replace("-", ""); } final ScriptTransformerContext context = new ScriptTransformerContext(className, code, functionMethod, asset); try { this.transformerPipeline.transform(context); } catch (TransformerException e) { throw new IllegalArgumentException("Failed to transform the script source.\nCode:\n``" + code + "``", e); } // TODO: Move to pipeline if (functionMethod != null) { if (functionMethod.getFunctionClass().isInterface()) { context.addInterface(functionMethod.getFunctionClass()); } } return new TransformedScript(context.compile(), className, ImmutableSet.copyOf(context.getDependencies())); } private final class TransformedScript { private final String code; private final String className; private final Set<String> dependencies; private TransformedScript(String code, String className, Set<String> dependencies) { this.className = className; this.dependencies = dependencies; this.code = code; } public String getCode() { return this.code; } public Set<String> getDependencies() { return this.dependencies; } public String getClassName() { return this.className; } } }