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