/*
* This file is part of Mixin, licensed under the MIT License (MIT).
*
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.spongepowered.tools.obfuscation;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.util.ITokenProvider;
import org.spongepowered.tools.obfuscation.interfaces.IJavadocProvider;
import org.spongepowered.tools.obfuscation.interfaces.IMixinAnnotationProcessor;
import org.spongepowered.tools.obfuscation.interfaces.IMixinValidator;
import org.spongepowered.tools.obfuscation.interfaces.IMixinValidator.ValidationPass;
import org.spongepowered.tools.obfuscation.interfaces.IObfuscationManager;
import org.spongepowered.tools.obfuscation.interfaces.ITypeHandleProvider;
import org.spongepowered.tools.obfuscation.mirror.AnnotationHandle;
import org.spongepowered.tools.obfuscation.mirror.TypeHandle;
import org.spongepowered.tools.obfuscation.mirror.TypeHandleSimulated;
import org.spongepowered.tools.obfuscation.mirror.TypeReference;
import org.spongepowered.tools.obfuscation.struct.InjectorRemap;
import org.spongepowered.tools.obfuscation.validation.ParentValidator;
import org.spongepowered.tools.obfuscation.validation.TargetValidator;
import com.google.common.collect.ImmutableList;
/**
* Mixin info manager, stores all of the mixin info during processing and also
* manages access to the mappings
*/
final class AnnotatedMixins implements IMixinAnnotationProcessor, ITokenProvider, ITypeHandleProvider, IJavadocProvider {
private static final String MAPID_SYSTEM_PROPERTY = "mixin.target.mapid";
/**
* Singleton instances for each ProcessingEnvironment
*/
private static Map<ProcessingEnvironment, AnnotatedMixins> instances = new HashMap<ProcessingEnvironment, AnnotatedMixins>();
/**
* Detected compiler environment
*/
private final CompilerEnvironment env;
/**
* Local processing environment
*/
private final ProcessingEnvironment processingEnv;
/**
* Mixins during processing phase
*/
private final Map<String, AnnotatedMixin> mixins = new HashMap<String, AnnotatedMixin>();
/**
* Mixins created during this AP pass
*/
private final List<AnnotatedMixin> mixinsForPass = new ArrayList<AnnotatedMixin>();
/**
* Obfuscation manager
*/
private final IObfuscationManager obf;
/**
* Rule validators
*/
private final List<IMixinValidator> validators;
/**
* Resolved tokens for constraint validation
*/
private final Map<String, Integer> tokenCache = new HashMap<String, Integer>();
/**
* Serialisable mixin target map
*/
private final TargetMap targets;
/**
* Properties file used to specify options when AP options cannot be
* configured via the build script (eg. when using AP with MCP)
*/
private Properties properties;
/**
* Private constructor, get instances using {@link #getMixinsForEnvironment}
*/
private AnnotatedMixins(ProcessingEnvironment processingEnv) {
this.env = this.detectEnvironment(processingEnv);
this.processingEnv = processingEnv;
this.printMessage(Kind.NOTE, "SpongePowered MIXIN Annotation Processor Version=" + MixinBootstrap.VERSION);
this.targets = this.initTargetMap();
this.obf = new ObfuscationManager(this);
this.obf.init();
this.validators = ImmutableList.<IMixinValidator>of(
new ParentValidator(this),
new TargetValidator(this)
);
this.initTokenCache(this.getOption(SupportedOptions.TOKENS));
}
protected TargetMap initTargetMap() {
TargetMap targets = TargetMap.create(System.getProperty(AnnotatedMixins.MAPID_SYSTEM_PROPERTY));
System.setProperty(AnnotatedMixins.MAPID_SYSTEM_PROPERTY, targets.getSessionId());
String targetsFileName = this.getOption(SupportedOptions.DEPENDENCY_TARGETS_FILE);
if (targetsFileName != null) {
try {
targets.readImports(new File(targetsFileName));
} catch (IOException ex) {
this.printMessage(Kind.WARNING, "Could not read from specified imports file: " + targetsFileName);
}
}
return targets;
}
private void initTokenCache(String tokens) {
if (tokens != null) {
Pattern tokenPattern = Pattern.compile("^([A-Z0-9\\-_\\.]+)=([0-9]+)$");
String[] tokenValues = tokens.replaceAll("\\s", "").toUpperCase().split("[;,]");
for (String tokenValue : tokenValues) {
Matcher tokenMatcher = tokenPattern.matcher(tokenValue);
if (tokenMatcher.matches()) {
this.tokenCache.put(tokenMatcher.group(1), Integer.parseInt(tokenMatcher.group(2)));
}
}
}
}
@Override
public ITypeHandleProvider getTypeProvider() {
return this;
}
@Override
public ITokenProvider getTokenProvider() {
return this;
}
@Override
public IObfuscationManager getObfuscationManager() {
return this.obf;
}
@Override
public IJavadocProvider getJavadocProvider() {
return this;
}
@Override
public ProcessingEnvironment getProcessingEnvironment() {
return this.processingEnv;
}
@Override
public CompilerEnvironment getCompilerEnvironment() {
return this.env;
}
@Override
public Integer getToken(String token) {
if (this.tokenCache.containsKey(token)) {
return this.tokenCache.get(token);
}
String option = this.getOption(token);
Integer value = null;
try {
value = Integer.valueOf(Integer.parseInt(option));
} catch (Exception ex) {
// npe or number format exception
}
this.tokenCache.put(token, value);
return value;
}
@Override
public String getOption(String option) {
if (option == null) {
return null;
}
String value = this.processingEnv.getOptions().get(option);
if (value != null) {
return value;
}
return this.getProperties().getProperty(option);
}
public Properties getProperties() {
if (this.properties == null) {
this.properties = new Properties();
try {
Filer filer = this.processingEnv.getFiler();
FileObject propertyFile = filer.getResource(StandardLocation.SOURCE_PATH, "", "mixin.properties");
if (propertyFile != null) {
InputStream inputStream = propertyFile.openInputStream();
this.properties.load(inputStream);
inputStream.close();
}
} catch (Exception ex) {
// ignore
}
}
return this.properties;
}
private CompilerEnvironment detectEnvironment(ProcessingEnvironment processingEnv) {
if (processingEnv.getClass().getName().contains("jdt")) {
return CompilerEnvironment.JDT;
}
return CompilerEnvironment.JAVAC;
}
/**
* Write out generated mappings
*/
public void writeMappings() {
this.obf.writeMappings();
}
/**
* Write out stored references
*/
public void writeReferences() {
this.obf.writeReferences();
}
/**
* Clear all registered mixins
*/
public void clear() {
this.mixins.clear();
}
/**
* Register a new mixin class
*/
public void registerMixin(TypeElement mixinType) {
String name = mixinType.getQualifiedName().toString();
if (!this.mixins.containsKey(name)) {
AnnotatedMixin mixin = new AnnotatedMixin(this, mixinType);
this.targets.registerTargets(mixin);
mixin.runValidators(ValidationPass.EARLY, this.validators);
this.mixins.put(name, mixin);
this.mixinsForPass.add(mixin);
}
}
/**
* Get a registered mixin
*/
public AnnotatedMixin getMixin(TypeElement mixinType) {
return this.getMixin(mixinType.getQualifiedName().toString());
}
/**
* Get a registered mixin
*/
public AnnotatedMixin getMixin(String mixinType) {
return this.mixins.get(mixinType);
}
public Collection<TypeMirror> getMixinsTargeting(TypeMirror targetType) {
return this.getMixinsTargeting((TypeElement)((DeclaredType)targetType).asElement());
}
public Collection<TypeMirror> getMixinsTargeting(TypeElement targetType) {
List<TypeMirror> minions = new ArrayList<TypeMirror>();
for (TypeReference mixin : this.targets.getMixinsTargeting(targetType)) {
TypeHandle handle = mixin.getHandle(this.processingEnv);
if (handle != null) {
minions.add(handle.getType());
}
}
return minions;
}
/**
* Register an {@link org.spongepowered.asm.mixin.gen.Accessor} method
*
* @param mixinType Mixin class
* @param method Accessor method
*/
public void registerAccessor(TypeElement mixinType, ExecutableElement method) {
AnnotatedMixin mixinClass = this.getMixin(mixinType);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found @Accessor annotation on a non-mixin method", method);
return;
}
AnnotationHandle accessor = AnnotationHandle.of(method, Accessor.class);
mixinClass.registerAccessor(method, accessor, this.shouldRemap(mixinClass, accessor));
}
/**
* Register an {@link org.spongepowered.asm.mixin.gen.Accessor} method
*
* @param mixinType Mixin class
* @param method Accessor method
*/
public void registerInvoker(TypeElement mixinType, ExecutableElement method) {
AnnotatedMixin mixinClass = this.getMixin(mixinType);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found @Accessor annotation on a non-mixin method", method);
return;
}
AnnotationHandle invoker = AnnotationHandle.of(method, Invoker.class);
mixinClass.registerInvoker(method, invoker, this.shouldRemap(mixinClass, invoker));
}
/**
* Register an {@link org.spongepowered.asm.mixin.Overwrite} method
*
* @param mixinType Mixin class
* @param method Overwrite method
*/
public void registerOverwrite(TypeElement mixinType, ExecutableElement method) {
AnnotatedMixin mixinClass = this.getMixin(mixinType);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found @Overwrite annotation on a non-mixin method", method);
return;
}
mixinClass.registerOverwrite(method, AnnotationHandle.of(method, Overwrite.class));
}
/**
* Register a {@link org.spongepowered.asm.mixin.Shadow} field
*
* @param mixinType Mixin class
* @param field Shadow field
* @param shadow {@link org.spongepowered.asm.mixin.Shadow} annotation
*/
public void registerShadow(TypeElement mixinType, VariableElement field, AnnotationHandle shadow) {
AnnotatedMixin mixinClass = this.getMixin(mixinType);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found @Shadow annotation on a non-mixin field", field);
return;
}
mixinClass.registerShadow(field, shadow, this.shouldRemap(mixinClass, shadow));
}
/**
* Register a {@link org.spongepowered.asm.mixin.Shadow} method
*
* @param mixinType Mixin class
* @param method Shadow method
* @param shadow {@link org.spongepowered.asm.mixin.Shadow} annotation
*/
public void registerShadow(TypeElement mixinType, ExecutableElement method, AnnotationHandle shadow) {
AnnotatedMixin mixinClass = this.getMixin(mixinType);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found @Shadow annotation on a non-mixin method", method);
return;
}
mixinClass.registerShadow(method, shadow, this.shouldRemap(mixinClass, shadow));
}
/**
* Register a {@link org.spongepowered.asm.mixin.injection.Inject} method
*
* @param mixinType Mixin class
* @param method Injector method
* @param inject {@link org.spongepowered.asm.mixin.injection.Inject}
* annotation
*/
public void registerInjector(TypeElement mixinType, ExecutableElement method, AnnotationHandle inject) {
AnnotatedMixin mixinClass = this.getMixin(mixinType);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found " + inject + " annotation on a non-mixin method", method);
return;
}
InjectorRemap remap = new InjectorRemap(this.shouldRemap(mixinClass, inject));
mixinClass.registerInjector(method, inject, remap);
remap.dispatchPendingMessages(this);
}
/**
* Register an {@link org.spongepowered.asm.mixin.Implements} declaration on
* a mixin class
*
* @param mixin Annotated mixin
* @param implementsAnnotation
* {@link org.spongepowered.asm.mixin.Implements} annotation
*/
public void registerSoftImplements(TypeElement mixin, AnnotationHandle implementsAnnotation) {
AnnotatedMixin mixinClass = this.getMixin(mixin);
if (mixinClass == null) {
this.printMessage(Kind.ERROR, "Found @Implements annotation on a non-mixin class");
return;
}
mixinClass.registerSoftImplements(implementsAnnotation);
}
/**
* Called from each AP class to notify the environment that a new pass is
* starting
*/
public void onPassStarted() {
this.mixinsForPass.clear();
}
/**
* Called from each AP when a pass is completed
*/
public void onPassCompleted() {
if (!"true".equalsIgnoreCase(this.getOption(SupportedOptions.DISABLE_TARGET_EXPORT))) {
this.targets.write(true);
}
for (AnnotatedMixin mixin : this.mixinsForPass) {
mixin.runValidators(ValidationPass.LATE, this.validators);
}
}
private boolean shouldRemap(AnnotatedMixin mixinClass, AnnotationHandle annotation) {
return annotation.getBoolean("remap", mixinClass.remap());
}
/**
* Print a message to the AP messager
*/
@Override
public void printMessage(Diagnostic.Kind kind, CharSequence msg) {
if (this.env == CompilerEnvironment.JAVAC || kind != Kind.NOTE) {
this.processingEnv.getMessager().printMessage(kind, msg);
}
}
/**
* Print a message to the AP messager
*/
@Override
public void printMessage(Diagnostic.Kind kind, CharSequence msg, Element element) {
this.processingEnv.getMessager().printMessage(kind, msg, element);
}
/**
* Print a message to the AP messager
*/
@Override
public void printMessage(Kind kind, CharSequence msg, Element element, AnnotationMirror annotation) {
this.processingEnv.getMessager().printMessage(kind, msg, element, annotation);
}
/**
* Print a message to the AP messager
*/
@Override
public void printMessage(Kind kind, CharSequence msg, Element element, AnnotationMirror annotation, AnnotationValue value) {
this.processingEnv.getMessager().printMessage(kind, msg, element, annotation, value);
}
/**
* Get a TypeHandle representing another type in the current processing
* environment
*/
@Override
public TypeHandle getTypeHandle(String name) {
name = name.replace('/', '.');
Elements elements = this.processingEnv.getElementUtils();
TypeElement element = elements.getTypeElement(name);
if (element != null) {
try {
return new TypeHandle(element);
} catch (NullPointerException ex) {
// probably bad package
}
}
int lastDotPos = name.lastIndexOf('.');
if (lastDotPos > -1) {
String pkg = name.substring(0, lastDotPos);
PackageElement packageElement = elements.getPackageElement(pkg);
if (packageElement != null) {
return new TypeHandle(packageElement, name);
}
}
return null;
}
/**
* Returns a simulated TypeHandle for a non-existing type
*/
@Override
public TypeHandle getSimulatedHandle(String name, TypeMirror simulatedTarget) {
name = name.replace('/', '.');
int lastDotPos = name.lastIndexOf('.');
if (lastDotPos > -1) {
String pkg = name.substring(0, lastDotPos);
PackageElement packageElement = this.processingEnv.getElementUtils().getPackageElement(pkg);
if (packageElement != null) {
return new TypeHandleSimulated(packageElement, name, simulatedTarget);
}
}
return new TypeHandleSimulated(name, simulatedTarget);
}
/* (non-Javadoc)
* @see org.spongepowered.tools.obfuscation.IJavadocProvider
* #getJavadoc(javax.lang.model.element.Element)
*/
@Override
public String getJavadoc(Element element) {
Elements elements = this.processingEnv.getElementUtils();
return elements.getDocComment(element);
}
/**
* Get the mixin manager instance for this environment
*/
public static AnnotatedMixins getMixinsForEnvironment(ProcessingEnvironment processingEnv) {
AnnotatedMixins mixins = AnnotatedMixins.instances.get(processingEnv);
if (mixins == null) {
mixins = new AnnotatedMixins(processingEnv);
AnnotatedMixins.instances.put(processingEnv, mixins);
}
return mixins;
}
}