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