    001/*
    002 * The FML Forge Mod Loader suite. Copyright (C) 2012 cpw
    003 *
    004 * 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
    005 * Software Foundation; either version 2.1 of the License, or any later version.
    006 *
    007 * 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
    008 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
    009 *
    010 * 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
    011 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
    012 */
    013package cpw.mods.fml.common;
    014
    015import java.io.File;
    016import java.io.FileInputStream;
    017import java.lang.annotation.Annotation;
    018import java.lang.reflect.Field;
    019import java.lang.reflect.Method;
    020import java.lang.reflect.Modifier;
    021import java.security.cert.Certificate;
    022import java.util.Arrays;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Properties;
    026import java.util.Set;
    027import java.util.logging.Level;
    028import java.util.zip.ZipEntry;
    029import java.util.zip.ZipFile;
    030import java.util.zip.ZipInputStream;
    031
    032import com.google.common.base.Function;
    033import com.google.common.base.Predicates;
    034import com.google.common.base.Strings;
    035import com.google.common.base.Throwables;
    036import com.google.common.collect.ArrayListMultimap;
    037import com.google.common.collect.BiMap;
    038import com.google.common.collect.ImmutableBiMap;
    039import com.google.common.collect.ImmutableList;
    040import com.google.common.collect.ImmutableList.Builder;
    041import com.google.common.collect.ImmutableSet;
    042import com.google.common.collect.Iterators;
    043import com.google.common.collect.Lists;
    044import com.google.common.collect.Multimap;
    045import com.google.common.collect.SetMultimap;
    046import com.google.common.collect.Sets;
    047import com.google.common.eventbus.EventBus;
    048import com.google.common.eventbus.Subscribe;
    049
    050import cpw.mods.fml.common.Mod.Instance;
    051import cpw.mods.fml.common.Mod.Metadata;
    052import cpw.mods.fml.common.discovery.ASMDataTable;
    053import cpw.mods.fml.common.discovery.ASMDataTable.ASMData;
    054import cpw.mods.fml.common.event.FMLConstructionEvent;
    055import cpw.mods.fml.common.event.FMLEvent;
    056import cpw.mods.fml.common.event.FMLInitializationEvent;
    057import cpw.mods.fml.common.event.FMLInterModComms.IMCEvent;
    058import cpw.mods.fml.common.event.FMLFingerprintViolationEvent;
    059import cpw.mods.fml.common.event.FMLPostInitializationEvent;
    060import cpw.mods.fml.common.event.FMLPreInitializationEvent;
    061import cpw.mods.fml.common.event.FMLServerAboutToStartEvent;
    062import cpw.mods.fml.common.event.FMLServerStartedEvent;
    063import cpw.mods.fml.common.event.FMLServerStartingEvent;
    064import cpw.mods.fml.common.event.FMLServerStoppedEvent;
    065import cpw.mods.fml.common.event.FMLServerStoppingEvent;
    066import cpw.mods.fml.common.event.FMLStateEvent;
    067import cpw.mods.fml.common.network.FMLNetworkHandler;
    068import cpw.mods.fml.common.versioning.ArtifactVersion;
    069import cpw.mods.fml.common.versioning.DefaultArtifactVersion;
    070import cpw.mods.fml.common.versioning.VersionParser;
    071import cpw.mods.fml.common.versioning.VersionRange;
    072
    073public class FMLModContainer implements ModContainer
    074{
    075    private Mod modDescriptor;
    076    private Object modInstance;
    077    private File source;
    078    private ModMetadata modMetadata;
    079    private String className;
    080    private Map<String, Object> descriptor;
    081    private boolean enabled = true;
    082    private String internalVersion;
    083    private boolean overridesMetadata;
    084    private EventBus eventBus;
    085    private LoadController controller;
    086    private Multimap<Class<? extends Annotation>, Object> annotations;
    087    private DefaultArtifactVersion processedVersion;
    088    private boolean isNetworkMod;
    089
    090    private static final BiMap<Class<? extends FMLEvent>, Class<? extends Annotation>> modAnnotationTypes = ImmutableBiMap.<Class<? extends FMLEvent>, Class<? extends Annotation>>builder()
    091        .put(FMLPreInitializationEvent.class, Mod.PreInit.class)
    092        .put(FMLInitializationEvent.class, Mod.Init.class)
    093        .put(FMLPostInitializationEvent.class, Mod.PostInit.class)
    094        .put(FMLServerAboutToStartEvent.class, Mod.ServerAboutToStart.class)
    095        .put(FMLServerStartingEvent.class, Mod.ServerStarting.class)
    096        .put(FMLServerStartedEvent.class, Mod.ServerStarted.class)
    097        .put(FMLServerStoppingEvent.class, Mod.ServerStopping.class)
    098        .put(FMLServerStoppedEvent.class, Mod.ServerStopped.class)
    099        .put(IMCEvent.class,Mod.IMCCallback.class)
    100        .put(FMLFingerprintViolationEvent.class, Mod.FingerprintWarning.class)
    101        .build();
    102    private static final BiMap<Class<? extends Annotation>, Class<? extends FMLEvent>> modTypeAnnotations = modAnnotationTypes.inverse();
    103    private String annotationDependencies;
    104    private VersionRange minecraftAccepted;
    105    private boolean fingerprintNotPresent;
    106    private Set<String> sourceFingerprints;
    107    private Certificate certificate;
    108
    109
    110    public FMLModContainer(String className, File modSource, Map<String,Object> modDescriptor)
    111    {
    112        this.className = className;
    113        this.source = modSource;
    114        this.descriptor = modDescriptor;
    115    }
    116
    117    @Override
    118    public String getModId()
    119    {
    120        return (String) descriptor.get("modid");
    121    }
    122
    123    @Override
    124    public String getName()
    125    {
    126        return modMetadata.name;
    127    }
    128
    129    @Override
    130    public String getVersion()
    131    {
    132        return internalVersion;
    133    }
    134
    135    @Override
    136    public File getSource()
    137    {
    138        return source;
    139    }
    140
    141    @Override
    142    public ModMetadata getMetadata()
    143    {
    144        return modMetadata;
    145    }
    146
    147    @Override
    148    public void bindMetadata(MetadataCollection mc)
    149    {
    150        modMetadata = mc.getMetadataForId(getModId(), descriptor);
    151
    152        if (descriptor.containsKey("useMetadata"))
    153        {
    154            overridesMetadata = !((Boolean)descriptor.get("useMetadata")).booleanValue();
    155        }
    156
    157        if (overridesMetadata || !modMetadata.useDependencyInformation)
    158        {
    159            Set<ArtifactVersion> requirements = Sets.newHashSet();
    160            List<ArtifactVersion> dependencies = Lists.newArrayList();
    161            List<ArtifactVersion> dependants = Lists.newArrayList();
    162            annotationDependencies = (String) descriptor.get("dependencies");
    163            Loader.instance().computeDependencies(annotationDependencies, requirements, dependencies, dependants);
    164            modMetadata.requiredMods = requirements;
    165            modMetadata.dependencies = dependencies;
    166            modMetadata.dependants = dependants;
    167            FMLLog.log(getModId(), Level.FINEST, "Parsed dependency info : %s %s %s", requirements, dependencies, dependants);
    168        }
    169        else
    170        {
    171            FMLLog.log(getModId(), Level.FINEST, "Using mcmod dependency info : %s %s %s", modMetadata.requiredMods, modMetadata.dependencies, modMetadata.dependants);
    172        }
    173        if (Strings.isNullOrEmpty(modMetadata.name))
    174        {
    175            FMLLog.log(getModId(), Level.INFO,"Mod %s is missing the required element 'name'. Substituting %s", getModId(), getModId());
    176            modMetadata.name = getModId();
    177        }
    178        internalVersion = (String) descriptor.get("version");
    179        if (Strings.isNullOrEmpty(internalVersion))
    180        {
    181            Properties versionProps = searchForVersionProperties();
    182            if (versionProps != null)
    183            {
    184                internalVersion = versionProps.getProperty(getModId()+".version");
    185                FMLLog.log(getModId(), Level.FINE, "Found version %s for mod %s in version.properties, using", internalVersion, getModId());
    186            }
    187
    188        }
    189        if (Strings.isNullOrEmpty(internalVersion) && !Strings.isNullOrEmpty(modMetadata.version))
    190        {
    191            FMLLog.log(getModId(), Level.WARNING, "Mod %s is missing the required element 'version' and a version.properties file could not be found. Falling back to metadata version %s", getModId(), modMetadata.version);
    192            internalVersion = modMetadata.version;
    193        }
    194        if (Strings.isNullOrEmpty(internalVersion))
    195        {
    196            FMLLog.log(getModId(), Level.WARNING, "Mod %s is missing the required element 'version' and no fallback can be found. Substituting '1.0'.", getModId());
    197            modMetadata.version = internalVersion = "1.0";
    198        }
    199
    200        String mcVersionString = (String) descriptor.get("acceptedMinecraftVersions");
    201        if (!Strings.isNullOrEmpty(mcVersionString))
    202        {
    203            minecraftAccepted = VersionParser.parseRange(mcVersionString);
    204        }
    205        else
    206        {
    207            minecraftAccepted = Loader.instance().getMinecraftModContainer().getStaticVersionRange();
    208        }
    209    }
    210
    211    public Properties searchForVersionProperties()
    212    {
    213        try
    214        {
    215            FMLLog.log(getModId(), Level.FINE,"Attempting to load the file version.properties from %s to locate a version number for %s", getSource().getName(), getModId());
    216            Properties version = null;
    217            if (getSource().isFile())
    218            {
    219                ZipFile source = new ZipFile(getSource());
    220                ZipEntry versionFile = source.getEntry("version.properties");
    221                if (versionFile!=null)
    222                {
    223                    version = new Properties();
    224                    version.load(source.getInputStream(versionFile));
    225                }
    226                source.close();
    227            }
    228            else if (getSource().isDirectory())
    229            {
    230                File propsFile = new File(getSource(),"version.properties");
    231                if (propsFile.exists() && propsFile.isFile())
    232                {
    233                    version = new Properties();
    234                    FileInputStream fis = new FileInputStream(propsFile);
    235                    version.load(fis);
    236                    fis.close();
    237                }
    238            }
    239            return version;
    240        }
    241        catch (Exception e)
    242        {
    243            Throwables.propagateIfPossible(e);
    244            FMLLog.log(getModId(), Level.FINEST, "Failed to find a usable version.properties file");
    245            return null;
    246        }
    247    }
    248
    249    @Override
    250    public void setEnabledState(boolean enabled)
    251    {
    252        this.enabled = enabled;
    253    }
    254
    255    @Override
    256    public Set<ArtifactVersion> getRequirements()
    257    {
    258        return modMetadata.requiredMods;
    259    }
    260
    261    @Override
    262    public List<ArtifactVersion> getDependencies()
    263    {
    264        return modMetadata.dependencies;
    265    }
    266
    267    @Override
    268    public List<ArtifactVersion> getDependants()
    269    {
    270        return modMetadata.dependants;
    271    }
    272
    273    @Override
    274    public String getSortingRules()
    275    {
    276        return ((overridesMetadata || !modMetadata.useDependencyInformation) ? Strings.nullToEmpty(annotationDependencies) : modMetadata.printableSortingRules());
    277    }
    278
    279    @Override
    280    public boolean matches(Object mod)
    281    {
    282        return mod == modInstance;
    283    }
    284
    285    @Override
    286    public Object getMod()
    287    {
    288        return modInstance;
    289    }
    290
    291    @Override
    292    public boolean registerBus(EventBus bus, LoadController controller)
    293    {
    294        if (this.enabled)
    295        {
    296            FMLLog.log(getModId(), Level.FINE, "Enabling mod %s", getModId());
    297            this.eventBus = bus;
    298            this.controller = controller;
    299            eventBus.register(this);
    300            return true;
    301        }
    302        else
    303        {
    304            return false;
    305        }
    306    }
    307
    308    private Multimap<Class<? extends Annotation>, Object> gatherAnnotations(Class<?> clazz) throws Exception
    309    {
    310        Multimap<Class<? extends Annotation>,Object> anns = ArrayListMultimap.create();
    311
    312        for (Method m : clazz.getDeclaredMethods())
    313        {
    314            for (Annotation a : m.getAnnotations())
    315            {
    316                if (modTypeAnnotations.containsKey(a.annotationType()))
    317                {
    318                    Class<?>[] paramTypes = new Class[] { modTypeAnnotations.get(a.annotationType()) };
    319
    320                    if (Arrays.equals(m.getParameterTypes(), paramTypes))
    321                    {
    322                        m.setAccessible(true);
    323                        anns.put(a.annotationType(), m);
    324                    }
    325                    else
    326                    {
    327                        FMLLog.log(getModId(), Level.SEVERE,"The mod %s appears to have an invalid method annotation %s. This annotation can only apply to methods with argument types %s -it will not be called", getModId(), a.annotationType().getSimpleName(), Arrays.toString(paramTypes));
    328                    }
    329                }
    330            }
    331        }
    332        return anns;
    333    }
    334
    335    private void processFieldAnnotations(ASMDataTable asmDataTable) throws Exception
    336    {
    337        SetMultimap<String, ASMData> annotations = asmDataTable.getAnnotationsFor(this);
    338
    339        parseSimpleFieldAnnotation(annotations, Instance.class.getName(), new Function<ModContainer, Object>()
    340        {
    341            public Object apply(ModContainer mc)
    342            {
    343                return mc.getMod();
    344            }
    345        });
    346        parseSimpleFieldAnnotation(annotations, Metadata.class.getName(), new Function<ModContainer, Object>()
    347        {
    348            public Object apply(ModContainer mc)
    349            {
    350                return mc.getMetadata();
    351            }
    352        });
    353    }
    354
    355    private void parseSimpleFieldAnnotation(SetMultimap<String, ASMData> annotations, String annotationClassName, Function<ModContainer, Object> retreiver) throws IllegalAccessException
    356    {
    357        String[] annName = annotationClassName.split("\\.");
    358        String annotationName = annName[annName.length - 1];
    359        for (ASMData targets : annotations.get(annotationClassName))
    360        {
    361            String targetMod = (String) targets.getAnnotationInfo().get("value");
    362            Field f = null;
    363            Object injectedMod = null;
    364            ModContainer mc = this;
    365            boolean isStatic = false;
    366            Class<?> clz = modInstance.getClass();
    367            if (!Strings.isNullOrEmpty(targetMod))
    368            {
    369                if (Loader.isModLoaded(targetMod))
    370                {
    371                    mc = Loader.instance().getIndexedModList().get(targetMod);
    372                }
    373                else
    374                {
    375                    mc = null;
    376                }
    377            }
    378            if (mc != null)
    379            {
    380                try
    381                {
    382                    clz = Class.forName(targets.getClassName(), true, Loader.instance().getModClassLoader());
    383                    f = clz.getDeclaredField(targets.getObjectName());
    384                    f.setAccessible(true);
    385                    isStatic = Modifier.isStatic(f.getModifiers());
    386                    injectedMod = retreiver.apply(mc);
    387                }
    388                catch (Exception e)
    389                {
    390                    Throwables.propagateIfPossible(e);
    391                    FMLLog.log(getModId(), Level.WARNING, e, "Attempting to load @%s in class %s for %s and failing", annotationName, targets.getClassName(), mc.getModId());
    392                }
    393            }
    394            if (f != null)
    395            {
    396                Object target = null;
    397                if (!isStatic)
    398                {
    399                    target = modInstance;
    400                    if (!modInstance.getClass().equals(clz))
    401                    {
    402                        FMLLog.log(getModId(), Level.WARNING, "Unable to inject @%s in non-static field %s.%s for %s as it is NOT the primary mod instance", annotationName, targets.getClassName(), targets.getObjectName(), mc.getModId());
    403                        continue;
    404                    }
    405                }
    406                f.set(target, injectedMod);
    407            }
    408        }
    409    }
    410
    411    @Subscribe
    412    public void constructMod(FMLConstructionEvent event)
    413    {
    414        try
    415        {
    416            ModClassLoader modClassLoader = event.getModClassLoader();
    417            modClassLoader.addFile(source);
    418            Class<?> clazz = Class.forName(className, true, modClassLoader);
    419
    420            Certificate[] certificates = clazz.getProtectionDomain().getCodeSource().getCertificates();
    421            int len = 0;
    422            if (certificates != null)
    423            {
    424                len = certificates.length;
    425            }
    426            Builder<String> certBuilder = ImmutableList.<String>builder();
    427            for (int i = 0; i < len; i++)
    428            {
    429                certBuilder.add(CertificateHelper.getFingerprint(certificates[i]));
    430            }
    431
    432            ImmutableList<String> certList = certBuilder.build();
    433            sourceFingerprints = ImmutableSet.copyOf(certList);
    434
    435            String expectedFingerprint = (String) descriptor.get("certificateFingerprint");
    436
    437            fingerprintNotPresent = true;
    438
    439            if (expectedFingerprint != null && !expectedFingerprint.isEmpty())
    440            {
    441                if (!sourceFingerprints.contains(expectedFingerprint))
    442                {
    443                    Level warnLevel = Level.SEVERE;
    444                    if (source.isDirectory())
    445                    {
    446                        warnLevel = Level.FINER;
    447                    }
    448                    FMLLog.log(getModId(), warnLevel, "The mod %s is expecting signature %s for source %s, however there is no signature matching that description", getModId(), expectedFingerprint, source.getName());
    449                }
    450                else
    451                {
    452                    certificate = certificates[certList.indexOf(expectedFingerprint)];
    453                    fingerprintNotPresent = false;
    454                }
    455            }
    456
    457            annotations = gatherAnnotations(clazz);
    458            isNetworkMod = FMLNetworkHandler.instance().registerNetworkMod(this, clazz, event.getASMHarvestedData());
    459            modInstance = clazz.newInstance();
    460            if (fingerprintNotPresent)
    461            {
    462                eventBus.post(new FMLFingerprintViolationEvent(source.isDirectory(), source, ImmutableSet.copyOf(this.sourceFingerprints), expectedFingerprint));
    463            }
    464            ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide());
    465            processFieldAnnotations(event.getASMHarvestedData());
    466        }
    467        catch (Throwable e)
    468        {
    469            controller.errorOccurred(this, e);
    470            Throwables.propagateIfPossible(e);
    471        }
    472    }
    473
    474    @Subscribe
    475    public void handleModStateEvent(FMLEvent event)
    476    {
    477        Class<? extends Annotation> annotation = modAnnotationTypes.get(event.getClass());
    478        if (annotation == null)
    479        {
    480            return;
    481        }
    482        try
    483        {
    484            for (Object o : annotations.get(annotation))
    485            {
    486                Method m = (Method) o;
    487                m.invoke(modInstance, event);
    488            }
    489        }
    490        catch (Throwable t)
    491        {
    492            controller.errorOccurred(this, t);
    493            Throwables.propagateIfPossible(t);
    494        }
    495    }
    496
    497    @Override
    498    public ArtifactVersion getProcessedVersion()
    499    {
    500        if (processedVersion == null)
    501        {
    502            processedVersion = new DefaultArtifactVersion(getModId(), getVersion());
    503        }
    504        return processedVersion;
    505    }
    506    @Override
    507    public boolean isImmutable()
    508    {
    509        return false;
    510    }
    511
    512    @Override
    513    public boolean isNetworkMod()
    514    {
    515        return isNetworkMod;
    516    }
    517
    518    @Override
    519    public String getDisplayVersion()
    520    {
    521        return modMetadata.version;
    522    }
    523
    524    @Override
    525    public VersionRange acceptableMinecraftVersionRange()
    526    {
    527        return minecraftAccepted;
    528    }
    529
    530    @Override
    531    public Certificate getSigningCertificate()
    532    {
    533        return certificate;
    534    }
    535}