Initial implementation of JSON based annotation scanning.

Disabled by default for now, until proven to be fully function.
Enable using -Dfml.enableJsonAnnotations=true
This commit is contained in:
LexManos 2018-02-07 00:43:32 -08:00
parent 274f4cd440
commit 816d33de28
6 changed files with 505 additions and 40 deletions

View File

@ -19,9 +19,12 @@
package net.minecraftforge.fml.common.discovery;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.jar.JarFile;
import net.minecraftforge.fml.common.FMLLog;
@ -29,18 +32,22 @@ import net.minecraftforge.fml.common.LoaderException;
import net.minecraftforge.fml.common.MetadataCollection;
import net.minecraftforge.fml.common.ModContainer;
import net.minecraftforge.fml.common.ModContainerFactory;
import net.minecraftforge.fml.common.discovery.ASMDataTable.ASMData;
import net.minecraftforge.fml.common.discovery.asm.ASMModParser;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.Level;
import net.minecraftforge.fml.common.discovery.json.JsonAnnotationLoader;
import java.util.regex.Matcher;
import java.util.zip.ZipEntry;
import org.objectweb.asm.Type;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
public class JarDiscoverer implements ITypeDiscoverer
{
private static final boolean ENABLE_JSON_TEST = "true".equals(System.getProperty("fml.enableJsonAnnotations", "false"));
@Override
public List<ModContainer> discover(ModCandidate candidate, ASMDataTable table)
{
@ -63,42 +70,11 @@ public class JarDiscoverer implements ITypeDiscoverer
FMLLog.log.debug("The mod container {} appears to be missing an mcmod.info file", candidate.getModContainer().getName());
mc = MetadataCollection.from(null, "");
}
for (ZipEntry ze : Collections.list(jar.entries()))
{
if (ze.getName()!=null && ze.getName().startsWith("__MACOSX"))
{
continue;
}
Matcher match = classFile.matcher(ze.getName());
if (match.matches())
{
ASMModParser modParser;
try
{
try (InputStream inputStream = jar.getInputStream(ze))
{
modParser = new ASMModParser(inputStream);
}
candidate.addClassEntry(ze.getName());
}
catch (LoaderException e)
{
FMLLog.log.error("There was a problem reading the entry {} in the jar {} - probably a corrupt zip", candidate.getModContainer().getPath(), e);
jar.close();
throw e;
}
modParser.validate();
modParser.sendToTable(table, candidate);
ModContainer container = ModContainerFactory.instance().build(modParser, candidate.getModContainer(), candidate);
if (container!=null)
{
table.addContainer(container);
foundMods.add(container);
container.bindMetadata(mc);
container.setClassVersion(modParser.getClassVersion());
}
}
}
if (ENABLE_JSON_TEST && jar.getEntry(JsonAnnotationLoader.ANNOTATION_JSON) != null)
findClassesJSON(candidate, table, jar, foundMods, mc);
else
findClassesASM(candidate, table, jar, foundMods, mc);
}
catch (Exception e)
{
@ -107,4 +83,78 @@ public class JarDiscoverer implements ITypeDiscoverer
return foundMods;
}
private void findClassesASM(ModCandidate candidate, ASMDataTable table, JarFile jar, List<ModContainer> foundMods, MetadataCollection mc) throws IOException
{
for (ZipEntry ze : Collections.list(jar.entries()))
{
if (ze.getName()!=null && ze.getName().startsWith("__MACOSX"))
{
continue;
}
Matcher match = classFile.matcher(ze.getName());
if (match.matches())
{
ASMModParser modParser;
try
{
try (InputStream inputStream = jar.getInputStream(ze))
{
modParser = new ASMModParser(inputStream);
}
candidate.addClassEntry(ze.getName());
}
catch (LoaderException e)
{
FMLLog.log.error("There was a problem reading the entry {} in the jar {} - probably a corrupt zip", candidate.getModContainer().getPath(), e);
jar.close();
throw e;
}
modParser.validate();
modParser.sendToTable(table, candidate);
ModContainer container = ModContainerFactory.instance().build(modParser, candidate.getModContainer(), candidate);
if (container!=null)
{
table.addContainer(container);
foundMods.add(container);
container.bindMetadata(mc);
container.setClassVersion(modParser.getClassVersion());
}
}
}
}
private void findClassesJSON(ModCandidate candidate, ASMDataTable table, JarFile jar, List<ModContainer> foundMods, MetadataCollection mc) throws IOException
{
FMLLog.log.info("Loading jar {} annotation data from json", candidate.getModContainer().getPath());
ZipEntry json = jar.getEntry(JsonAnnotationLoader.ANNOTATION_JSON);
Multimap<String, ASMData> annos = JsonAnnotationLoader.loadJson(jar.getInputStream(json), candidate, table);
for (Entry<Type, Constructor<? extends ModContainer>> entry : ModContainerFactory.modTypes.entrySet())
{
Type type = entry.getKey();
Constructor<? extends ModContainer> ctr = entry.getValue();
for (ASMData data : annos.get(type.getClassName()))
{
FMLLog.log.debug("Identified a mod of type {} ({}) - loading", type.getClassName(), data.getClassName());
try
{
ModContainer ret = ctr.newInstance(data.getClassName(), candidate, data.getAnnotationInfo());
if (!ret.shouldLoadInEnvironment())
FMLLog.log.debug("Skipping mod {}, container opted to not load.", data.getClassName());
else
{
table.addContainer(ret);
foundMods.add(ret);
ret.bindMetadata(mc);
//ret.setClassVersion(classVersion); // Not really needed anymore as we're forcing J8. Maybe think of reinstating for J9 support? After LaunchWraper re-do.
}
}
catch (Exception e)
{
FMLLog.log.error("Unable to construct {} container", data.getClassName(), e);
}
}
}
}
}

View File

@ -107,6 +107,7 @@ public class ModDiscoverer
}
else if (modFile.isDirectory())
{
//TODO Remove in 1.13+ Mods should never be directory based anymore.
FMLLog.log.debug("Found a candidate mod directory {}", modFile.getName());
addCandidate(new ModCandidate(modFile, modFile, ContainerType.DIR));
}

View File

@ -32,7 +32,7 @@ import com.google.common.collect.Maps;
public class ModAnnotation
{
public class EnumHolder
public static class EnumHolder
{
private final String desc;
private final String value;

View File

@ -0,0 +1,165 @@
/*
* Minecraft Forge
* Copyright (c) 2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.common.discovery.json;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.apache.commons.lang3.Validate;
import org.objectweb.asm.Type;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.discovery.asm.ModAnnotation.EnumHolder;
//Package private, modders shouldn't access this. Do it through ASMDataTable.
class ASMInfo
{
String name;
String[] interfaces;
List<Annotation> annotations;
private Map<Integer, Annotation> byID;
public Annotation getSubAnnotation(int id)
{
if (byID == null)
{
byID = Maps.newHashMap();
annotations.forEach(a -> { if (a.id != null) byID.put(a.id, a); });
}
return byID.get(id);
}
public enum TargetType { CLASS, FIELD, METHOD, SUBTYPE };
public enum ValueType
{
BOOL(Boolean::valueOf, v -> {boolean[] ret = new boolean[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Boolean.parseBoolean(v[x]); return ret; }),
BYTE(Byte::valueOf, v -> {byte[] ret = new byte[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Byte.parseByte(v[x]); return ret; }),
CHAR(x -> x.charAt(0), v -> {char[] ret = new char[v.length]; for (int x = 0; x < v.length; x++) ret[x] = v[x].charAt(0); return ret; }),
SHORT(Short::valueOf, v -> {short[] ret = new short[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Short.parseShort(v[x]); return ret; }),
INT(Integer::valueOf, v -> {int[] ret = new int[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Integer.parseInt(v[x]); return ret; }),
LONG(Long::valueOf, v -> {long[] ret = new long[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Long.parseLong(v[x]); return ret; }),
FLOAT(Float::valueOf, v -> {float[] ret = new float[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Float.parseFloat(v[x]); return ret; }),
DOUBLE(Double::valueOf, v -> {double[] ret = new double[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Double.parseDouble(v[x]); return ret; }),
STRING(x -> x, x -> x),
CLASS(Type::getType, v -> {Type[] ret = new Type[v.length]; for (int x = 0; x < v.length; x++) ret[x] = Type.getType(v[x]); return ret; }),
ENUM(ValueType::getEnumHolder, v -> {EnumHolder[] ret = new EnumHolder[v.length]; for (int x = 0; x < v.length; x++) ret[x] = ValueType.getEnumHolder(v[x]); return ret; }),
ANNOTATION(null, null),
NULL(x -> null, x -> null);
public final Function<String, Object> single;
public final Function<String[], Object> array;
private ValueType(Function<String, Object> single, Function<String[], Object> array)
{
this.single = single;
this.array = array;
}
private static EnumHolder getEnumHolder(String value)
{
int idx = value.lastIndexOf('/');
if (idx <= 1)
throw new IllegalArgumentException("Can not create a EnumHolder for value: " + value);
return new EnumHolder(value.substring(0, idx - 1), value.substring(idx));
}
};
static class Annotation
{
TargetType type;
String name;
String target;
Integer id;
ValueHolder value;
Map<String, ValueHolder> values;
private Map<String, Object> _values;
public Map<String, Object> getValues(ASMInfo pool)
{
if (_values == null)
{
_values = Maps.newHashMap();
if (values != null)
values.forEach((k, v) -> _values.put(k, v.get(pool)));
else
_values.put("value", value);
}
return _values;
}
}
static class ValueHolder
{
ValueType type;
String value;
String[] values;
private Object _value;
private ValueType getType()
{
return type == null ? ValueType.STRING : type;
}
public Object get(ASMInfo pool)
{
if (_value == null)
{
if (values != null)
{
if (type == ValueType.ANNOTATION)
{
List<Map<String, Object>> list = Lists.newArrayList();
_value = list;
for (String s : values)
{
Annotation sub = pool.getSubAnnotation(Integer.parseInt(s));
if (sub == null)
FMLLog.log.error("Invalid Sub-Annotation in Annotation JSON: " + s);
else
list.add(sub.getValues(pool));
}
}
else
_value = getType().array.apply(values);
}
else
{
if (type == ValueType.ANNOTATION)
{
Annotation sub = pool.getSubAnnotation(Integer.parseInt(value));
if (sub == null)
FMLLog.log.error("Invalid Sub-Annotation in Annotation JSON: " + value);
else
_value = sub.getValues(pool);
}
else
_value = getType().single.apply(value);
}
}
return _value;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Minecraft Forge
* Copyright (c) 2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.common.discovery.json;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.minecraftforge.fml.common.discovery.ASMDataTable;
import net.minecraftforge.fml.common.discovery.ASMDataTable.ASMData;
import net.minecraftforge.fml.common.discovery.ModCandidate;
import net.minecraftforge.fml.common.discovery.json.ASMInfo.Annotation;
public class JsonAnnotationLoader
{
public static final String ANNOTATION_JSON = "META-INF/fml_cache_annotation.json";
private static final Gson GSON = new GsonBuilder().create();
private static final Type INFO_TABLE = new TypeToken<Map<String, ASMInfo>>(){}.getType();
public static Multimap<String, ASMData> loadJson(InputStream data, ModCandidate candidate, ASMDataTable table)
{
Map<String, ASMInfo> map = GSON.fromJson(new InputStreamReader(data), INFO_TABLE);
Multimap<String, ASMData> ret = HashMultimap.create();
for (Entry<String, ASMInfo> entry : map.entrySet())
{
//TODO: Java9 Multi-Release Jars, picking the correct class for the current platform. For now we just ignore them.
if (entry.getKey().startsWith("META-INF/"))
continue;
ASMInfo asm_info = entry.getValue();
if (asm_info.interfaces != null)
{
for (String type : asm_info.interfaces)
{
table.addASMData(candidate, type, asm_info.name, null, null);
ret.put(type, new ASMData(candidate, type, asm_info.name, null, null));
}
}
if (asm_info.annotations != null)
{
for (Annotation anno : asm_info.annotations)
{
table.addASMData(candidate, anno.name, asm_info.name, anno.target, anno.getValues(asm_info));
ret.put(anno.name, new ASMData(candidate, anno.name, asm_info.name, anno.target, anno.getValues(asm_info)));
}
}
}
return ret;
}
}

View File

@ -0,0 +1,172 @@
/*
* Minecraft Forge
* Copyright (c) 2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.test;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.junit.Test;
import net.minecraftforge.fml.common.discovery.ASMDataTable;
import net.minecraftforge.fml.common.discovery.ContainerType;
import net.minecraftforge.fml.common.discovery.ModCandidate;
import net.minecraftforge.fml.common.discovery.asm.ASMModParser;
import net.minecraftforge.fml.common.discovery.json.JsonAnnotationLoader;
public class TestAnnotationParser
{
private static String TEST_JAR = "forestry_1.12.2-5.7.0.0.jar";
public static Pattern classFile = Pattern.compile("[^\\s\\$]+(\\$[^\\s]+)?\\.class$");
private static final int RUN_COUNT = 100;
private File getFile()
{
ClassLoader cl = getClass().getClassLoader();
URL url = cl.getResource(TEST_JAR);
return url == null ? null : new File(url.getFile());
}
@Test
public void testAnnotationLoaderASM() throws IOException
{
File jar = getFile();
if (jar == null)
return; //Skip this test if the test jar doesn't exist.
Timer timer = new Timer();
for( int x = 0; x < RUN_COUNT; x++)
{
timer.start();
loadAnnotationsASM(jar);
timer.end(null);
}
System.out.println("LoaderASM: " + timer.finish());
}
@Test
public void testAnnotationLoaderJSON() throws IOException
{
File jar = getFile();
if (jar == null)
return; //Skip this test if the test jar doesn't exist.
Timer timer = new Timer();
for( int x = 0; x < RUN_COUNT; x++)
{
timer.start();
loadAnnotationsJSON(jar);
timer.end(null);
}
System.out.println("LoaderJSON: " + timer.finish());
}
private void loadAnnotationsASM(File jar) throws IOException
{
ASMDataTable dataTable = new ASMDataTable();
ModCandidate candidate = new ModCandidate(jar, jar, ContainerType.JAR);
try (ZipFile in = new ZipFile(jar))
{
for (ZipEntry e : Collections.list(in.entries()))
{
if (e.getName() != null && e.getName().startsWith("__MACOSX"))
continue;
Matcher match = classFile.matcher(e.getName());
if (match.matches())
{
ASMModParser modParser;
try (InputStream inputStream = in.getInputStream(e))
{
modParser = new ASMModParser(inputStream);
}
//candidate.addClassEntry(e.getName());
if (modParser != null)
modParser.sendToTable(dataTable, candidate);
}
}
}
}
private void loadAnnotationsJSON(File jar) throws IOException
{
ASMDataTable dataTable = new ASMDataTable();
ModCandidate candidate = new ModCandidate(jar, jar, ContainerType.JAR);
try (ZipFile in = new ZipFile(jar))
{
// We need to loop everything to gather a list of class files, but as we're not reading every entry we should be faster?
for (ZipEntry e : Collections.list(in.entries()))
{
if (e.getName() != null && e.getName().startsWith("__MACOSX"))
continue;
Matcher match = classFile.matcher(e.getName());
if (match.matches())
{
//We check for classes, make this fancier and support multi-release jars?
//candidate.addClassEntry(e.getName());
}
}
InputStream json_input = in.getInputStream(in.getEntry(JsonAnnotationLoader.ANNOTATION_JSON));
JsonAnnotationLoader.loadJson(json_input, candidate, dataTable);
}
}
private static class Timer
{
private long start;
private long min = Long.MAX_VALUE;
private long max = Long.MIN_VALUE;
private long total = 0;
private int count = 0;
public void start()
{
this.start = System.currentTimeMillis();
}
public void end(String message)
{
long now = System.currentTimeMillis();
long time = now - start;
if (message != null)
System.out.println(String.format(message, time));
min = Long.min(min, time);
max = Long.max(max, time);
total += time;
count++;
}
public String finish()
{
return "Runs: " + count +
" Min: " + min +
" Max: " + max +
" Total: " + total +
" Average: " + (total/count);
}
}
}