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                if (expectedFingerprint!=null && expectedFingerprint != "" &&  !sourceFingerprints.contains(expectedFingerprint))
435                {
436                    Level warnLevel = Level.SEVERE;
437                    if (source.isDirectory())
438                    {
439                        warnLevel = Level.FINER;
440                    }
441                    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());
442                }
443                else
444                {
445                    certificate = certificates[certList.indexOf(expectedFingerprint)];
446                    fingerprintNotPresent = false;
447                }
448                annotations = gatherAnnotations(clazz);
449                isNetworkMod = FMLNetworkHandler.instance().registerNetworkMod(this, clazz, event.getASMHarvestedData());
450                modInstance = clazz.newInstance();
451                if (fingerprintNotPresent)
452                {
453                    eventBus.post(new FMLFingerprintViolationEvent(source.isDirectory(), source, ImmutableSet.copyOf(this.sourceFingerprints)));
454                }
455                ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide());
456                processFieldAnnotations(event.getASMHarvestedData());
457            }
458            catch (Throwable e)
459            {
460                controller.errorOccurred(this, e);
461                Throwables.propagateIfPossible(e);
462            }
463        }
464    
465        @Subscribe
466        public void handleModStateEvent(FMLEvent event)
467        {
468            Class<? extends Annotation> annotation = modAnnotationTypes.get(event.getClass());
469            if (annotation == null)
470            {
471                return;
472            }
473            try
474            {
475                for (Object o : annotations.get(annotation))
476                {
477                    Method m = (Method) o;
478                    m.invoke(modInstance, event);
479                }
480            }
481            catch (Throwable t)
482            {
483                controller.errorOccurred(this, t);
484                Throwables.propagateIfPossible(t);
485            }
486        }
487    
488        @Override
489        public ArtifactVersion getProcessedVersion()
490        {
491            if (processedVersion == null)
492            {
493                processedVersion = new DefaultArtifactVersion(getModId(), getVersion());
494            }
495            return processedVersion;
496        }
497        @Override
498        public boolean isImmutable()
499        {
500            return false;
501        }
502    
503        @Override
504        public boolean isNetworkMod()
505        {
506            return isNetworkMod;
507        }
508    
509        @Override
510        public String getDisplayVersion()
511        {
512            return modMetadata.version;
513        }
514    
515        @Override
516        public VersionRange acceptableMinecraftVersionRange()
517        {
518            return minecraftAccepted;
519        }
520    
521        @Override
522        public Certificate getSigningCertificate()
523        {
524            return certificate;
525        }
526    }