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