com.github.xpenatan.gdx.backends.teavm.config.TeaBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of backend-teavm Show documentation
Show all versions of backend-teavm Show documentation
Tool to generate libgdx to javascript using teaVM
package com.github.xpenatan.gdx.backends.teavm.config;
import com.badlogic.gdx.files.FileHandle;
import com.github.xpenatan.gdx.backends.teavm.TeaClassLoader;
import com.github.xpenatan.gdx.backends.teavm.config.plugins.TeaClassTransformer;
import com.github.xpenatan.gdx.backends.teavm.config.plugins.TeaReflectionSupplier;
import com.github.xpenatan.gdx.backends.teavm.gen.SkipClass;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.teavm.diagnostics.DefaultProblemTextConsumer;
import org.teavm.diagnostics.Problem;
import org.teavm.diagnostics.ProblemProvider;
import org.teavm.model.CallLocation;
import org.teavm.model.MethodReference;
import org.teavm.model.TextLocation;
import org.teavm.tooling.TeaVMTargetType;
import org.teavm.tooling.TeaVMTool;
import org.teavm.vm.TeaVMOptimizationLevel;
import org.teavm.vm.TeaVMPhase;
import org.teavm.vm.TeaVMProgressFeedback;
import org.teavm.vm.TeaVMProgressListener;
/**
* @author xpenatan
*/
public class TeaBuilder {
public static void log(String msg) {
String text = "| " + msg;
logInternalNewLine(text);
}
public static void logInternal(String msg) {
System.err.print(msg);
}
public static void logInternalNewLine(String msg) {
logInternal(msg + "\n");
}
public static void logHeader(String text) {
String msg = "";
msg += "#################################################################\n";
msg += "|\n| " + text + "\n|";
msg += "\n" + "#################################################################";
logInternalNewLine(msg);
}
public static void logEnd() {
String msg = "\n#################################################################";
logInternalNewLine(msg);
}
enum ACCEPT_STATE {
ACCEPT, NOT_ACCEPT, NO_MATCH
}
public interface TeaProgressListener {
void onProgress(float progress);
}
private static final String EXTENSION_FREETYPE = "gdx-freetype-teavm";
private static final String EXTENSION_BOX2D = "gdx-box2d-teavm";
private static final String EXTENSION_BOX2D_GWT = "gdx-box2d-gwt";
public static TeaVMTool config(TeaBuildConfiguration configuration) {
TeaVMTool tool = new TeaVMTool();
return config(tool, configuration, null);
}
public static TeaVMTool config(TeaBuildConfiguration configuration, TeaProgressListener progressListener) {
TeaVMTool tool = new TeaVMTool();
return config(tool, configuration, progressListener);
}
public static TeaVMTool config(TeaVMTool tool, TeaBuildConfiguration configuration) {
return config(tool, configuration);
}
public static TeaVMTool config(TeaVMTool tool, TeaBuildConfiguration configuration, TeaProgressListener progressListener) {
ArrayList acceptedURL = new ArrayList<>();
ArrayList notAcceptedURL = new ArrayList<>();
String webappDirectory = configuration.getWebAppPath();
String webappName = "webapp";
addDefaultReflectionClasses();
automaticReflection(configuration);
configClasspath(configuration, acceptedURL, notAcceptedURL);
TeaBuilder.log("");
TeaBuilder.log("targetDirectory: " + webappDirectory);
TeaBuilder.log("");
URL[] classPaths = acceptedURL.toArray(new URL[acceptedURL.size()]);
Set> typesAnnotatedWith = TeaClassTransformer.reflections.getTypesAnnotatedWith(SkipClass.class);
ArrayList skipClasses = new ArrayList<>();
Iterator> iterator = typesAnnotatedWith.stream().iterator();
while(iterator.hasNext()) {
Class> skipClass = iterator.next();
skipClasses.add(skipClass.getName());
}
skipClasses.addAll(configuration.getSkipClasses());
TeaClassLoader classLoader = new TeaClassLoader(classPaths, TeaBuilder.class.getClassLoader(), skipClasses);
configTool(tool, classLoader, configuration, webappDirectory, webappName, progressListener);
configAssets(classLoader, configuration, webappDirectory, webappName, acceptedURL);
return tool;
}
public static boolean build(TeaVMTool tool) {
return build(tool, false);
}
public static boolean build(TeaVMTool tool, boolean logClassNames) {
boolean isSuccess = false;
try {
long timeStart = new Date().getTime();
tool.generate();
long timeEnd = new Date().getTime();
float seconds = (timeEnd - timeStart) / 1000f;
ProblemProvider problemProvider = tool.getProblemProvider();
Collection classes = tool.getClasses();
List problems = problemProvider.getProblems();
if(problems.size() > 0) {
TeaBuilder.logHeader("Compiler problems");
DefaultProblemTextConsumer p = new DefaultProblemTextConsumer();
for(int i = 0; i < problems.size(); i++) {
Problem problem = problems.get(i);
CallLocation location = problem.getLocation();
MethodReference method = location != null ? location.getMethod() : null;
String classSource = "-";
String methodName = "-";
if(location != null) {
TextLocation sourceLocation = location.getSourceLocation();
if(sourceLocation != null)
classSource = sourceLocation.toString();
if(method != null) {
methodName = method.toString();
}
}
if(i > 0) {
TeaBuilder.log("");
TeaBuilder.log("----");
TeaBuilder.log("");
}
TeaBuilder.log(problem.getSeverity().toString() + "[" + i + "]");
TeaBuilder.log("Class: " + classSource);
TeaBuilder.log("Method: " + methodName);
p.clear();
problem.render(p);
String text = p.getText();
TeaBuilder.log("Text: " + text);
}
TeaBuilder.logEnd();
}
else {
isSuccess = true;
TeaBuilder.logHeader("Build complete in " + seconds + " seconds. Total Classes: " + classes.size());
}
if(logClassNames) {
Stream sorted = classes.stream().sorted();
Iterator iterator = sorted.iterator();
while(iterator.hasNext()) {
String clazz = iterator.next();
TeaBuilder.log(clazz);
}
}
}
catch(Throwable e) {
e.printStackTrace();
return false;
}
return isSuccess;
}
private static void preserveClasses(TeaVMTool tool, TeaBuildConfiguration configuration, TeaClassLoader classLoader) {
//Keep reflection classes
List classesToPreserve = tool.getClassesToPreserve();
ArrayList configClassesToPreserve = configuration.getClassesToPreserve();
List reflectionClasses = TeaReflectionSupplier.getReflectionClasses();
configClassesToPreserve.addAll(reflectionClasses);
// Get classes or packages from reflection. When path is a package, get all classes from it.
ArrayList preserveClasses = classLoader.getAllClasses(configClassesToPreserve);
classesToPreserve.addAll(preserveClasses);
}
private static void sortAcceptedClassPath(ArrayList acceptedURL) {
// The idea here is to replace native java classes with the emulated java class.
// 0 - TeaVM backend - Contains all teavm api stuff to make it work.
// 1 - Extensions - Emulate native extension classes
// TODO make a better sort. Lazy to do it now
// Move extensions to be first so native classes are replaced by the emulated classes
makeClassPathFirst(acceptedURL, EXTENSION_FREETYPE);
makeClassPathFirst(acceptedURL, EXTENSION_BOX2D);
// Move generic backend to be first
makeClassPathFirst(acceptedURL, "backend-teavm");
}
private static void makeClassPathFirst(ArrayList acceptedURL, String module) {
for(int i = 0; i < acceptedURL.size(); i++) {
URL url = acceptedURL.get(i);
String string = url.toString();
if(string.contains(module)) {
acceptedURL.remove(i);
acceptedURL.add(0, url);
break;
}
}
}
private static void automaticReflection(TeaBuildConfiguration configuration) {
for(URL classPath : configuration.getAdditionalClasspath()) {
try {
ZipInputStream zip = new ZipInputStream(classPath.openStream());
TeaBuilder.logHeader("Automatic Reflection Include");
for(ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) {
if(!entry.isDirectory() && entry.getName().endsWith(".class")) {
// This ZipEntry represents a class. Now, what class does it represent?
String className = entry.getName().replace('/', '.'); // including ".class"
String name = className.substring(0, className.length() - ".class".length());
boolean add = false;
for(String toInclude : configuration.getReflectionInclude()) {
if(name.startsWith(toInclude)) add = true;
}
for(String toExclude : configuration.getReflectionExclude()) {
if(name.startsWith(toExclude)) add = false;
}
if(add) {
TeaBuilder.log("Include class: " + name);
TeaReflectionSupplier.addReflectionClass(name);
}
}
}
}
catch(IOException e) {
e.printStackTrace();
}
}
}
private static void addDefaultReflectionClasses() {
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.scenes.scene2d");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.math");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g2d.GlyphLayout");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g2d.TextureRegion");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g2d.Sprite");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g2d.BitmapFont");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g2d.NinePatch");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.materials.MaterialAttribute");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.Color");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.Texture");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.utils.Array");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.utils.Disposable");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.utils.Json");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.utils.ObjectMap");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.utils.OrderedMap");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.utils.Queue");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.VertexAttribute");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.model");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.Net");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.maps.MapObject");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.maps.objects");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.maps.tiled.objects");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.ParticleEffect");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.ParticleController");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.ResourceData");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.ResourceData.SaveData");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.ResourceData.AssetData");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.ParallelArray");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.values");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.emitters");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.influencers");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.graphics.g3d.particles.renderers");
TeaReflectionSupplier.addReflectionClass("com.badlogic.gdx.math.Interpolation");
TeaReflectionSupplier.addReflectionClass("net.mgsx.gltf.data");
}
private static void configClasspath(TeaBuildConfiguration configuration, ArrayList acceptedURL, ArrayList notAcceptedURL) {
String pathSeparator = System.getProperty("path.separator");
String[] classPathEntries = System.getProperty("java.class.path").split(pathSeparator);
for(int i = 0; i < classPathEntries.length; i++) {
String path = classPathEntries[i];
File file = new File(path);
path = path.replace("\\", "/");
URL url = null;
try {
url = file.toURI().toURL();
}
catch(MalformedURLException e) {
e.printStackTrace();
}
ACCEPT_STATE acceptState = acceptPath(path);
boolean accept = acceptState == ACCEPT_STATE.ACCEPT;
if(accept || acceptState == ACCEPT_STATE.NO_MATCH)
accept = configuration.acceptClasspath(url);
if(accept)
acceptedURL.add(url);
else
notAcceptedURL.add(url);
}
acceptedURL.addAll(configuration.getAdditionalClasspath());
sortAcceptedClassPath(acceptedURL);
TeaBuilder.logHeader("ACCEPTED CLASSPATH");
for(int i = 0; i < acceptedURL.size(); i++) {
TeaBuilder.log(i + " true: " + acceptedURL.get(i).getPath());
}
TeaBuilder.logHeader("IGNORED CLASSPATH");
for(int i = 0; i < notAcceptedURL.size(); i++) {
TeaBuilder.log(i + " false: " + notAcceptedURL.get(i).getPath());
}
int size = acceptedURL.size();
if(size <= 0) {
System.err.println("No urls found");
throw new RuntimeException("No Urls Found");
}
}
private static ACCEPT_STATE acceptPath(String path) {
ACCEPT_STATE isValid = ACCEPT_STATE.NO_MATCH;
if(path.contains("junit"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("hamcrest"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("jackson-"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("Java/jdk"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("commons-io"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("org/ow2"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("carrotsearch"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("google/code"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("jcraft"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("joda-time"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("mozilla"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("jutils"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("jinput-"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("lwjgl"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("jlayer-"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("/resources"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("javax"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("jsinterop-annotations"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("gwt-user-"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("sac-"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("gdx-box2d"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("gdx-jnigen"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("gdx-platform"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("generator/core/"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("gdx-bullet-platform"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("org.reflections"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("com.google.guava"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("org.javassist"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("com.google.code.findbugs"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
else if(path.contains("org.slf4j"))
isValid = ACCEPT_STATE.NOT_ACCEPT;
if(path.contains("backend-teavm"))
isValid = ACCEPT_STATE.ACCEPT;
else if(path.contains(EXTENSION_BOX2D_GWT))
isValid = ACCEPT_STATE.ACCEPT;
else if(path.contains(EXTENSION_FREETYPE))
isValid = ACCEPT_STATE.ACCEPT;
else if(path.contains(EXTENSION_BOX2D))
isValid = ACCEPT_STATE.ACCEPT;
else if(path.contains("jParser-loader"))
isValid = ACCEPT_STATE.ACCEPT;
else if(path.contains("jzlib"))
isValid = ACCEPT_STATE.ACCEPT;
return isValid;
}
private static void configTool(TeaVMTool tool, TeaClassLoader classLoader, TeaBuildConfiguration configuration, String webappDirectory, String webappName, TeaProgressListener progressListener) {
boolean setDebugInformationGenerated = false;
boolean setSourceMapsFileGenerated = false;
boolean setSourceFilesCopied = false;
File setTargetDirectory = new File(webappDirectory + File.separator + webappName + File.separator + "teavm");
String setTargetFileName = "app.js";
String tmpdir = System.getProperty("java.io.tmpdir");
File setCacheDirectory = new File(tmpdir + File.separator + "TeaVMCache");
boolean setIncremental = false;
tool.setClassLoader(classLoader);
tool.setDebugInformationGenerated(setDebugInformationGenerated);
tool.setSourceMapsFileGenerated(setSourceMapsFileGenerated);
tool.setSourceFilesCopied(setSourceFilesCopied);
tool.setTargetDirectory(setTargetDirectory);
tool.setTargetFileName(setTargetFileName);
tool.setFastDependencyAnalysis(false);
tool.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE);
String applicationListenerClass = configuration.getApplicationListenerClass();
String mainClass = configuration.getMainClass();
if(applicationListenerClass != null) {
TeaClassTransformer.applicationListener = applicationListenerClass;
TeaClassTransformer.mainClass = mainClass;
}
tool.setMainClass(mainClass);
tool.setIncremental(setIncremental);
tool.setCacheDirectory(setCacheDirectory);
tool.setStrict(false);
tool.setTargetType(TeaVMTargetType.JAVASCRIPT);
tool.setProgressListener(new TeaVMProgressListener() {
TeaVMPhase phase = null;
@Override
public TeaVMProgressFeedback phaseStarted(TeaVMPhase teaVMPhase, int i) {
if(teaVMPhase == TeaVMPhase.DEPENDENCY_ANALYSIS) {
TeaBuilder.logHeader("DEPENDENCY_ANALYSIS");
}
else if(teaVMPhase == TeaVMPhase.COMPILING) {
TeaBuilder.logInternalNewLine("");
TeaBuilder.logHeader("COMPILING");
}
phase = teaVMPhase;
return TeaVMProgressFeedback.CONTINUE;
}
@Override
public TeaVMProgressFeedback progressReached(int i) {
if(phase == TeaVMPhase.DEPENDENCY_ANALYSIS) {
TeaBuilder.logInternal("|");
}
if(phase == TeaVMPhase.COMPILING) {
if(progressListener != null) {
float progress = i / 1000f;
progressListener.onProgress(progress);
}
}
return TeaVMProgressFeedback.CONTINUE;
}
});
preserveClasses(tool, configuration, classLoader);
}
public static void configAssets(TeaClassLoader classLoader, TeaBuildConfiguration configuration, String webappDirectory, String webappName, ArrayList acceptedURL) {
TeaBuilder.logHeader("COPYING ASSETS");
FileHandle webappDistFolder = new FileHandle(webappDirectory);
FileHandle webappFolder = webappDistFolder.child(webappName);
FileHandle assetsFolder = webappFolder.child("assets");
FileHandle scriptsFolder = webappFolder.child("scripts");
FileHandle assetFile = assetsFolder.child("assets.txt");
AssetFilter filter = configuration.assetFilter();
boolean shouldUseDefaultHtmlIndex = configuration.shouldUseDefaultHtmlIndex();
if(shouldUseDefaultHtmlIndex) {
useDefaultHTMLIndexFile(classLoader, configuration, webappDistFolder, webappName, webappFolder);
}
boolean generateAssetPaths = configuration.shouldGenerateAssetFile();
ArrayList alLAssets = new ArrayList<>();
// Copy Assets files
ArrayList assetsPaths = configuration.assetsPath();
for(int i = 0; i < assetsPaths.size(); i++) {
AssetFileHandle assetFileHandle = assetsPaths.get(i);
ArrayList assets = AssetsCopy.copyAssets(assetFileHandle, filter, assetsFolder);
alLAssets.addAll(assets);
}
if(assetFile.exists()) {
// Delete assets.txt before adding the updated list.
assetFile.delete();
}
if(generateAssetPaths) {
AssetsCopy.generateAssetsFile(alLAssets, assetsFolder, assetFile);
}
// Copy assets from resources
List resources = TeaVMResourceProperties.getResources(acceptedURL);
List scripts = new ArrayList<>();
// Filter out javascript
for(int i = 0; i < resources.size(); i++) {
String asset = resources.get(i);
if(asset.endsWith(".js") || asset.endsWith(".wasm")) {
resources.remove(i);
scripts.add(asset);
i--;
}
}
// Copy additional classpath files
ArrayList classPathAssetsFiles = configuration.getAdditionalAssetClasspath();
ArrayList classpathAssets = AssetsCopy.copyResources(classLoader, classPathAssetsFiles, filter, assetsFolder);
// Copy resources
ArrayList resourceAssets = AssetsCopy.copyResources(classLoader, resources, filter, assetsFolder);
// Copy scripts
ArrayList scriptsAssets = AssetsCopy.copyScripts(classLoader, scripts, scriptsFolder);
AssetsCopy.generateAssetsFile(classpathAssets, assetsFolder, assetFile);
AssetsCopy.generateAssetsFile(resourceAssets, assetsFolder, assetFile);
TeaBuilder.log("");
}
private static void useDefaultHTMLIndexFile(TeaClassLoader classLoader, TeaBuildConfiguration configuration, FileHandle webappDistFolder, String webappName, FileHandle webappFolder) {
ArrayList webappAssetsFiles = new ArrayList<>();
webappAssetsFiles.add(webappName);
// Copy webapp folder from resources to destination
AssetsCopy.copyResources(classLoader, webappAssetsFiles, null, webappDistFolder);
TeaBuilder.log("");
FileHandle handler = webappFolder.child("index.html");
String indexHtmlStr = handler.readString();
String logo = configuration.getLogoPath();
String htmlLogo = logo;
boolean showLoadingLogo = configuration.isShowLoadingLogo();
indexHtmlStr = indexHtmlStr.replace("%TITLE%", configuration.getHtmlTitle());
indexHtmlStr = indexHtmlStr.replace("%WIDTH%", configuration.getHtmlWidth());
indexHtmlStr = indexHtmlStr.replace("%HEIGHT%", configuration.getHtmlHeight());
indexHtmlStr = indexHtmlStr.replace("%ARGS%", configuration.getMainClassArgs());
indexHtmlStr = indexHtmlStr.replace(
"%LOGO%", showLoadingLogo ? "" : ""
);
handler.writeString(indexHtmlStr, false);
if(showLoadingLogo) {
ArrayList logoAsset = new ArrayList<>();
logoAsset.add(logo);
AssetsCopy.copyResources(classLoader, logoAsset, null, webappFolder);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy