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