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.FMLServerStartedEvent;
062import cpw.mods.fml.common.event.FMLServerStartingEvent;
063import cpw.mods.fml.common.event.FMLServerStoppedEvent;
064import cpw.mods.fml.common.event.FMLServerStoppingEvent;
065import cpw.mods.fml.common.event.FMLStateEvent;
066import cpw.mods.fml.common.network.FMLNetworkHandler;
067import cpw.mods.fml.common.versioning.ArtifactVersion;
068import cpw.mods.fml.common.versioning.DefaultArtifactVersion;
069import cpw.mods.fml.common.versioning.VersionParser;
070import cpw.mods.fml.common.versioning.VersionRange;
071
072public 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}