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