    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    private String modLanguage;
    109    private ILanguageAdapter languageAdapter;
    110
    111    public static interface ILanguageAdapter {
    112        public Object getNewInstance(FMLModContainer container, Class<?> objectClass, ClassLoader classLoader) throws Exception;
    113    }
    114
    115    public static class ScalaAdapter implements ILanguageAdapter {
    116        @Override
    117        public Object getNewInstance(FMLModContainer container, Class<?> scalaObjectClass, ClassLoader classLoader) throws Exception
    118        {
    119            System.out.println("Scala class : "+ scalaObjectClass);
    120            Class<?> sObjectClass = Class.forName(scalaObjectClass.getName()+"$",true,classLoader);
    121            return sObjectClass.getField("MODULE$").get(null);
    122        }
    123    }
    124
    125    public static class JavaAdapter implements ILanguageAdapter {
    126        @Override
    127        public Object getNewInstance(FMLModContainer container, Class<?> objectClass, ClassLoader classLoader) throws Exception
    128        {
    129            return objectClass.newInstance();
    130        }
    131    }
    132    public FMLModContainer(String className, File modSource, Map<String,Object> modDescriptor)
    133    {
    134        this.className = className;
    135        this.source = modSource;
    136        this.descriptor = modDescriptor;
    137        this.modLanguage = (String) modDescriptor.get("modLanguage");
    138        this.languageAdapter = "scala".equals(modLanguage) ? new ScalaAdapter() : new JavaAdapter();
    139    }
    140
    141    private ILanguageAdapter getLanguageAdapter()
    142    {
    143        return languageAdapter;
    144    }
    145    @Override
    146    public String getModId()
    147    {
    148        return (String) descriptor.get("modid");
    149    }
    150
    151    @Override
    152    public String getName()
    153    {
    154        return modMetadata.name;
    155    }
    156
    157    @Override
    158    public String getVersion()
    159    {
    160        return internalVersion;
    161    }
    162
    163    @Override
    164    public File getSource()
    165    {
    166        return source;
    167    }
    168
    169    @Override
    170    public ModMetadata getMetadata()
    171    {
    172        return modMetadata;
    173    }
    174
    175    @Override
    176    public void bindMetadata(MetadataCollection mc)
    177    {
    178        modMetadata = mc.getMetadataForId(getModId(), descriptor);
    179
    180        if (descriptor.containsKey("useMetadata"))
    181        {
    182            overridesMetadata = !((Boolean)descriptor.get("useMetadata")).booleanValue();
    183        }
    184
    185        if (overridesMetadata || !modMetadata.useDependencyInformation)
    186        {
    187            Set<ArtifactVersion> requirements = Sets.newHashSet();
    188            List<ArtifactVersion> dependencies = Lists.newArrayList();
    189            List<ArtifactVersion> dependants = Lists.newArrayList();
    190            annotationDependencies = (String) descriptor.get("dependencies");
    191            Loader.instance().computeDependencies(annotationDependencies, requirements, dependencies, dependants);
    192            modMetadata.requiredMods = requirements;
    193            modMetadata.dependencies = dependencies;
    194            modMetadata.dependants = dependants;
    195            FMLLog.log(getModId(), Level.FINEST, "Parsed dependency info : %s %s %s", requirements, dependencies, dependants);
    196        }
    197        else
    198        {
    199            FMLLog.log(getModId(), Level.FINEST, "Using mcmod dependency info : %s %s %s", modMetadata.requiredMods, modMetadata.dependencies, modMetadata.dependants);
    200        }
    201        if (Strings.isNullOrEmpty(modMetadata.name))
    202        {
    203            FMLLog.log(getModId(), Level.INFO,"Mod %s is missing the required element 'name'. Substituting %s", getModId(), getModId());
    204            modMetadata.name = getModId();
    205        }
    206        internalVersion = (String) descriptor.get("version");
    207        if (Strings.isNullOrEmpty(internalVersion))
    208        {
    209            Properties versionProps = searchForVersionProperties();
    210            if (versionProps != null)
    211            {
    212                internalVersion = versionProps.getProperty(getModId()+".version");
    213                FMLLog.log(getModId(), Level.FINE, "Found version %s for mod %s in version.properties, using", internalVersion, getModId());
    214            }
    215
    216        }
    217        if (Strings.isNullOrEmpty(internalVersion) && !Strings.isNullOrEmpty(modMetadata.version))
    218        {
    219            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);
    220            internalVersion = modMetadata.version;
    221        }
    222        if (Strings.isNullOrEmpty(internalVersion))
    223        {
    224            FMLLog.log(getModId(), Level.WARNING, "Mod %s is missing the required element 'version' and no fallback can be found. Substituting '1.0'.", getModId());
    225            modMetadata.version = internalVersion = "1.0";
    226        }
    227
    228        String mcVersionString = (String) descriptor.get("acceptedMinecraftVersions");
    229        if (!Strings.isNullOrEmpty(mcVersionString))
    230        {
    231            minecraftAccepted = VersionParser.parseRange(mcVersionString);
    232        }
    233        else
    234        {
    235            minecraftAccepted = Loader.instance().getMinecraftModContainer().getStaticVersionRange();
    236        }
    237    }
    238
    239    public Properties searchForVersionProperties()
    240    {
    241        try
    242        {
    243            FMLLog.log(getModId(), Level.FINE,"Attempting to load the file version.properties from %s to locate a version number for %s", getSource().getName(), getModId());
    244            Properties version = null;
    245            if (getSource().isFile())
    246            {
    247                ZipFile source = new ZipFile(getSource());
    248                ZipEntry versionFile = source.getEntry("version.properties");
    249                if (versionFile!=null)
    250                {
    251                    version = new Properties();
    252                    version.load(source.getInputStream(versionFile));
    253                }
    254                source.close();
    255            }
    256            else if (getSource().isDirectory())
    257            {
    258                File propsFile = new File(getSource(),"version.properties");
    259                if (propsFile.exists() && propsFile.isFile())
    260                {
    261                    version = new Properties();
    262                    FileInputStream fis = new FileInputStream(propsFile);
    263                    version.load(fis);
    264                    fis.close();
    265                }
    266            }
    267            return version;
    268        }
    269        catch (Exception e)
    270        {
    271            Throwables.propagateIfPossible(e);
    272            FMLLog.log(getModId(), Level.FINEST, "Failed to find a usable version.properties file");
    273            return null;
    274        }
    275    }
    276
    277    @Override
    278    public void setEnabledState(boolean enabled)
    279    {
    280        this.enabled = enabled;
    281    }
    282
    283    @Override
    284    public Set<ArtifactVersion> getRequirements()
    285    {
    286        return modMetadata.requiredMods;
    287    }
    288
    289    @Override
    290    public List<ArtifactVersion> getDependencies()
    291    {
    292        return modMetadata.dependencies;
    293    }
    294
    295    @Override
    296    public List<ArtifactVersion> getDependants()
    297    {
    298        return modMetadata.dependants;
    299    }
    300
    301    @Override
    302    public String getSortingRules()
    303    {
    304        return ((overridesMetadata || !modMetadata.useDependencyInformation) ? Strings.nullToEmpty(annotationDependencies) : modMetadata.printableSortingRules());
    305    }
    306
    307    @Override
    308    public boolean matches(Object mod)
    309    {
    310        return mod == modInstance;
    311    }
    312
    313    @Override
    314    public Object getMod()
    315    {
    316        return modInstance;
    317    }
    318
    319    @Override
    320    public boolean registerBus(EventBus bus, LoadController controller)
    321    {
    322        if (this.enabled)
    323        {
    324            FMLLog.log(getModId(), Level.FINE, "Enabling mod %s", getModId());
    325            this.eventBus = bus;
    326            this.controller = controller;
    327            eventBus.register(this);
    328            return true;
    329        }
    330        else
    331        {
    332            return false;
    333        }
    334    }
    335
    336    private Multimap<Class<? extends Annotation>, Object> gatherAnnotations(Class<?> clazz) throws Exception
    337    {
    338        Multimap<Class<? extends Annotation>,Object> anns = ArrayListMultimap.create();
    339
    340        for (Method m : clazz.getDeclaredMethods())
    341        {
    342            for (Annotation a : m.getAnnotations())
    343            {
    344                if (modTypeAnnotations.containsKey(a.annotationType()))
    345                {
    346                    Class<?>[] paramTypes = new Class[] { modTypeAnnotations.get(a.annotationType()) };
    347
    348                    if (Arrays.equals(m.getParameterTypes(), paramTypes))
    349                    {
    350                        m.setAccessible(true);
    351                        anns.put(a.annotationType(), m);
    352                    }
    353                    else
    354                    {
    355                        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));
    356                    }
    357                }
    358            }
    359        }
    360        return anns;
    361    }
    362
    363    private void processFieldAnnotations(ASMDataTable asmDataTable) throws Exception
    364    {
    365        SetMultimap<String, ASMData> annotations = asmDataTable.getAnnotationsFor(this);
    366
    367        parseSimpleFieldAnnotation(annotations, Instance.class.getName(), new Function<ModContainer, Object>()
    368        {
    369            public Object apply(ModContainer mc)
    370            {
    371                return mc.getMod();
    372            }
    373        });
    374        parseSimpleFieldAnnotation(annotations, Metadata.class.getName(), new Function<ModContainer, Object>()
    375        {
    376            public Object apply(ModContainer mc)
    377            {
    378                return mc.getMetadata();
    379            }
    380        });
    381    }
    382
    383    private void parseSimpleFieldAnnotation(SetMultimap<String, ASMData> annotations, String annotationClassName, Function<ModContainer, Object> retreiver) throws IllegalAccessException
    384    {
    385        String[] annName = annotationClassName.split("\\.");
    386        String annotationName = annName[annName.length - 1];
    387        for (ASMData targets : annotations.get(annotationClassName))
    388        {
    389            String targetMod = (String) targets.getAnnotationInfo().get("value");
    390            Field f = null;
    391            Object injectedMod = null;
    392            ModContainer mc = this;
    393            boolean isStatic = false;
    394            Class<?> clz = modInstance.getClass();
    395            if (!Strings.isNullOrEmpty(targetMod))
    396            {
    397                if (Loader.isModLoaded(targetMod))
    398                {
    399                    mc = Loader.instance().getIndexedModList().get(targetMod);
    400                }
    401                else
    402                {
    403                    mc = null;
    404                }
    405            }
    406            if (mc != null)
    407            {
    408                try
    409                {
    410                    clz = Class.forName(targets.getClassName(), true, Loader.instance().getModClassLoader());
    411                    f = clz.getDeclaredField(targets.getObjectName());
    412                    f.setAccessible(true);
    413                    isStatic = Modifier.isStatic(f.getModifiers());
    414                    injectedMod = retreiver.apply(mc);
    415                }
    416                catch (Exception e)
    417                {
    418                    Throwables.propagateIfPossible(e);
    419                    FMLLog.log(getModId(), Level.WARNING, e, "Attempting to load @%s in class %s for %s and failing", annotationName, targets.getClassName(), mc.getModId());
    420                }
    421            }
    422            if (f != null)
    423            {
    424                Object target = null;
    425                if (!isStatic)
    426                {
    427                    target = modInstance;
    428                    if (!modInstance.getClass().equals(clz))
    429                    {
    430                        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());
    431                        continue;
    432                    }
    433                }
    434                f.set(target, injectedMod);
    435            }
    436        }
    437    }
    438
    439    @Subscribe
    440    public void constructMod(FMLConstructionEvent event)
    441    {
    442        try
    443        {
    444            ModClassLoader modClassLoader = event.getModClassLoader();
    445            modClassLoader.addFile(source);
    446            Class<?> clazz = Class.forName(className, true, modClassLoader);
    447
    448            Certificate[] certificates = clazz.getProtectionDomain().getCodeSource().getCertificates();
    449            int len = 0;
    450            if (certificates != null)
    451            {
    452                len = certificates.length;
    453            }
    454            Builder<String> certBuilder = ImmutableList.<String>builder();
    455            for (int i = 0; i < len; i++)
    456            {
    457                certBuilder.add(CertificateHelper.getFingerprint(certificates[i]));
    458            }
    459
    460            ImmutableList<String> certList = certBuilder.build();
    461            sourceFingerprints = ImmutableSet.copyOf(certList);
    462
    463            String expectedFingerprint = (String) descriptor.get("certificateFingerprint");
    464
    465            fingerprintNotPresent = true;
    466
    467            if (expectedFingerprint != null && !expectedFingerprint.isEmpty())
    468            {
    469                if (!sourceFingerprints.contains(expectedFingerprint))
    470                {
    471                    Level warnLevel = Level.SEVERE;
    472                    if (source.isDirectory())
    473                    {
    474                        warnLevel = Level.FINER;
    475                    }
    476                    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());
    477                }
    478                else
    479                {
    480                    certificate = certificates[certList.indexOf(expectedFingerprint)];
    481                    fingerprintNotPresent = false;
    482                }
    483            }
    484
    485            annotations = gatherAnnotations(clazz);
    486            isNetworkMod = FMLNetworkHandler.instance().registerNetworkMod(this, clazz, event.getASMHarvestedData());
    487            modInstance = getLanguageAdapter().getNewInstance(this,clazz, modClassLoader);
    488            if (fingerprintNotPresent)
    489            {
    490                eventBus.post(new FMLFingerprintViolationEvent(source.isDirectory(), source, ImmutableSet.copyOf(this.sourceFingerprints), expectedFingerprint));
    491            }
    492            ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide());
    493            processFieldAnnotations(event.getASMHarvestedData());
    494        }
    495        catch (Throwable e)
    496        {
    497            controller.errorOccurred(this, e);
    498            Throwables.propagateIfPossible(e);
    499        }
    500    }
    501
    502    @Subscribe
    503    public void handleModStateEvent(FMLEvent event)
    504    {
    505        Class<? extends Annotation> annotation = modAnnotationTypes.get(event.getClass());
    506        if (annotation == null)
    507        {
    508            return;
    509        }
    510        try
    511        {
    512            for (Object o : annotations.get(annotation))
    513            {
    514                Method m = (Method) o;
    515                m.invoke(modInstance, event);
    516            }
    517        }
    518        catch (Throwable t)
    519        {
    520            controller.errorOccurred(this, t);
    521            Throwables.propagateIfPossible(t);
    522        }
    523    }
    524
    525    @Override
    526    public ArtifactVersion getProcessedVersion()
    527    {
    528        if (processedVersion == null)
    529        {
    530            processedVersion = new DefaultArtifactVersion(getModId(), getVersion());
    531        }
    532        return processedVersion;
    533    }
    534    @Override
    535    public boolean isImmutable()
    536    {
    537        return false;
    538    }
    539
    540    @Override
    541    public boolean isNetworkMod()
    542    {
    543        return isNetworkMod;
    544    }
    545
    546    @Override
    547    public String getDisplayVersion()
    548    {
    549        return modMetadata.version;
    550    }
    551
    552    @Override
    553    public VersionRange acceptableMinecraftVersionRange()
    554    {
    555        return minecraftAccepted;
    556    }
    557
    558    @Override
    559    public Certificate getSigningCertificate()
    560    {
    561        return certificate;
    562    }
    563}