/* *******************************************************************
* Copyright (c) 2010 Contributors
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License v1.0
* which accompanies this distribution and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Andy Clement - SpringSource
* ******************************************************************/
package org.aspectj.ajdt.internal.compiler.lookup;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.aspectj.ajdt.internal.compiler.IOutputClassFileNameProvider;
import org.aspectj.asm.internal.CharOperation;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.World;
import org.aspectj.weaver.patterns.ExactTypePattern;
import org.aspectj.weaver.patterns.TypePattern;
/**
* Collects up information about the application of ITDs and relevant declares - it can then output source code as if those ITDs had
* been pushed in. Supports the simulated push-in of:
* <ul>
* <li>declare at_type
* <li>itd method
* <li>itd field
* <li>itd ctor
* <li>declare parents
* </ul>
*
* @author Andy Clement
* @since 1.6.9
*/
public class PushinCollector {
private final static String OPTION_SUFFIX = "suffix";
private final static String OPTION_DIR = "dir";
private final static String OPTION_PKGDIRS = "packageDirs";
private final static String OPTION_DEBUG = "debug";
private final static String OPTION_LINENUMS = "lineNums";
private final static String OPTION_DUMPUNCHANGED = "dumpUnchanged";
private World world;
private boolean debug = false;
private boolean dumpUnchanged = false;
private IOutputClassFileNameProvider outputFileNameProvider;
private String specifiedOutputDirectory;
private boolean includePackageDirs;
private boolean includeLineNumberComments;
private String suffix;
// This first collection stores the 'text' for the declarations.
private Map<AbstractMethodDeclaration, RepresentationAndLocation> codeRepresentation = new HashMap<AbstractMethodDeclaration, RepresentationAndLocation>();
// This stores the new annotations
private Map<SourceTypeBinding, List<String>> additionalAnnotations = new HashMap<SourceTypeBinding, List<String>>();
// This stores the new parents
private Map<SourceTypeBinding, List<ExactTypePattern>> additionalParents = new HashMap<SourceTypeBinding, List<ExactTypePattern>>();
// This indicates which types are affected by which intertype declarations
private Map<SourceTypeBinding, List<AbstractMethodDeclaration>> newDeclarations = new HashMap<SourceTypeBinding, List<AbstractMethodDeclaration>>();
private PushinCollector(World world, Properties configuration) {
this.world = world;
// Configure the instance based on the input properties
specifiedOutputDirectory = configuration.getProperty(OPTION_DIR);
includePackageDirs = configuration.getProperty(OPTION_PKGDIRS, "true").equalsIgnoreCase("true");
includeLineNumberComments = configuration.getProperty(OPTION_LINENUMS, "false").equalsIgnoreCase("true");
debug = configuration.getProperty(OPTION_DEBUG, "false").equalsIgnoreCase("true");
dumpUnchanged = configuration.getProperty(OPTION_DUMPUNCHANGED, "false").equalsIgnoreCase("true");
String specifiedSuffix = configuration.getProperty(OPTION_SUFFIX, "pushedin");
if (specifiedSuffix.length() > 0) {
StringBuilder sb = new StringBuilder();
sb.append(".").append(specifiedSuffix);
suffix = sb.toString();
} else {
suffix = "";
}
if (debug) {
System.out.println("Configured to create pushin side files:" + configuration);
System.out.println("dumpUnchanged=" + dumpUnchanged + "\nincludePackageDirs=" + includePackageDirs);
}
}
private String getName(CompilationUnitDeclaration cud) {
if (cud == null) {
return "UNKNOWN";
}
if (cud.scope == null) {
return "UNKNOWN";
}
if (cud.scope.referenceContext == null) {
return "UNKNOWN";
}
return new String(cud.scope.referenceContext.getFileName());
}
/**
* @return true if the type is affected by something (itd/declare anno/declare parent)
*/
private boolean hasChanged(SourceTypeBinding stb) {
return newDeclarations.get(stb) != null || additionalParents.get(stb) != null || additionalAnnotations.get(stb) != null;
}
/**
* Produce the modified source that looks like the itds and declares have been applied.
*/
public void dump(CompilationUnitDeclaration compilationUnitDeclaration, String outputFileLocation) {
if (compilationUnitDeclaration.scope.topLevelTypes == null || compilationUnitDeclaration.scope.topLevelTypes.length == 0) {
return;
}
SourceTypeBinding[] types = compilationUnitDeclaration.scope.topLevelTypes;
if (types == null || types.length == 0) {
return;
}
// Process all types working from end to start as whatever we do (insert-wise) will affect locations later in the file
StringBuffer sourceContents = new StringBuffer();
// put the whole original file in the buffer
boolean changed = false;
sourceContents.append(compilationUnitDeclaration.compilationResult.compilationUnit.getContents());
for (int t = types.length - 1; t >= 0; t--) {
SourceTypeBinding sourceTypeBinding = compilationUnitDeclaration.scope.topLevelTypes[t];
if (!hasChanged(sourceTypeBinding)) {
if (debug) {
System.out.println(getName(compilationUnitDeclaration) + " has nothing applied");
}
continue;
}
changed = true;
int bodyEnd = sourceTypeBinding.scope.referenceContext.bodyEnd; // last '}' of the type
List<AbstractMethodDeclaration> declarations = newDeclarations.get(sourceTypeBinding);
if (declarations != null) {
for (AbstractMethodDeclaration md : declarations) {
RepresentationAndLocation ral = codeRepresentation.get(md);
if (ral != null) {
String s = ral.textualRepresentation;
sourceContents.insert(bodyEnd, "\n" + s + "\n");
if (includeLineNumberComments && ral.linenumber != -1) {
sourceContents.insert(bodyEnd, "\n // " + ral.linenumber);
}
}
}
}
// fix up declare parents - may need to attach them to existing ones
TypeReference sr = sourceTypeBinding.scope.referenceContext.superclass;
TypeReference[] trs = sourceTypeBinding.scope.referenceContext.superInterfaces;
List<ExactTypePattern> newParents = additionalParents.get(sourceTypeBinding);
StringBuffer extendsString = new StringBuffer();
StringBuffer implementsString = new StringBuffer();
if (newParents != null && newParents.size() > 0) {
for (ExactTypePattern newParent : newParents) {
ResolvedType newParentType = newParent.getExactType().resolve(world);
if (newParentType.isInterface()) {
if (implementsString.length() > 0) {
implementsString.append(",");
}
implementsString.append(newParentType.getName());
} else {
extendsString.append(newParentType.getName());
}
}
if (trs == null && sr == null) {
// nothing after the class declaration, let's insert what we need to
// Find the position just before the type opening '{'
int beforeOpeningCurly = sourceTypeBinding.scope.referenceContext.bodyStart - 1;
if (implementsString.length() != 0) {
implementsString.insert(0, "implements ");
implementsString.append(" ");
sourceContents.insert(beforeOpeningCurly, implementsString);
}
if (extendsString.length() != 0) {
extendsString.insert(0, "extends ");
extendsString.append(" ");
sourceContents.insert(beforeOpeningCurly, extendsString);
}
}
}
List<String> annos = additionalAnnotations.get(sourceTypeBinding);
if (annos != null && annos.size() > 0) {
for (String anno : annos) {
sourceContents.insert(sourceTypeBinding.scope.referenceContext.declarationSourceStart, anno + " ");
}
}
}
if (changed || (!changed && dumpUnchanged)) {
try {
if (debug) {
System.out.println("Pushed in output file being written to " + outputFileLocation);
System.out.println(sourceContents);
}
FileWriter fos = new FileWriter(new File(outputFileLocation));
fos.write(sourceContents.toString());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Encapsulates a text representation (source code) for a member and the line where it was declared.
*/
private static class RepresentationAndLocation {
String textualRepresentation;
int linenumber;
public RepresentationAndLocation(String textualRepresentation, int linenumber) {
this.textualRepresentation = textualRepresentation;
this.linenumber = linenumber;
}
}
public void recordInterTypeMethodDeclarationCode(AbstractMethodDeclaration md, String s, int line) {
codeRepresentation.put(md, new RepresentationAndLocation(s, line));
}
public void recordInterTypeFieldDeclarationCode(AbstractMethodDeclaration md, String s, int line) {
codeRepresentation.put(md, new RepresentationAndLocation(s, line));
}
public void recordInterTypeConstructorDeclarationCode(AbstractMethodDeclaration md, String s, int line) {
codeRepresentation.put(md, new RepresentationAndLocation(s, line));
}
// public void recordDeclareAnnotationDeclarationCode(AbstractMethodDeclaration md, String value) {
// codeRepresentation.put(md, new RepresentationAndLocation(value, -1));
// }
public void tagAsMunged(SourceTypeBinding sourceType, AbstractMethodDeclaration sourceMethod) {
if (sourceMethod == null) {
// seen when an ITD field is made onto an interface. It matches, but the sourceMethod is null.
// can be null for binary weave (there is no source method)
return;
}
List<AbstractMethodDeclaration> amds = newDeclarations.get(sourceType);
if (amds == null) {
amds = new ArrayList<AbstractMethodDeclaration>();
newDeclarations.put(sourceType, amds);
}
amds.add(sourceMethod);
}
public void tagAsMunged(SourceTypeBinding sourceType, String annotationString) {
List<String> annos = additionalAnnotations.get(sourceType);
if (annos == null) {
annos = new ArrayList<String>();
additionalAnnotations.put(sourceType, annos);
}
annos.add(annotationString);
}
public void dump(CompilationUnitDeclaration unit) {
String outputFile = getOutputFileFor(unit);
if (debug) {
System.out
.println("Output location is " + outputFile + " for " + new String(unit.scope.referenceContext.getFileName()));
}
dump(unit, outputFile);
}
private String getOutputFileFor(CompilationUnitDeclaration unit) {
StringBuffer sb = new StringBuffer();
// Create the directory portion of the output location
if (specifiedOutputDirectory != null) {
sb.append(specifiedOutputDirectory).append(File.separator);
} else {
String sss = outputFileNameProvider.getOutputClassFileName("A".toCharArray(), unit.compilationResult);
sb.append(sss, 0, sss.length() - 7);
}
// Create the subdirectory structure matching the package declaration
if (includePackageDirs) {
char[][] packageName = unit.compilationResult.packageName;
if (packageName != null) {
sb.append(CharOperation.concatWith(unit.compilationResult.packageName, File.separatorChar));
sb.append(File.separator);
}
}
new File(sb.toString()).mkdirs();
// Create the filename portion
String filename = new String(unit.getFileName()); // gives 'n:\A.java'
int index = filename.lastIndexOf('/');
int index2 = filename.lastIndexOf('\\');
if (index > index2) {
sb.append(filename.substring(index + 1));
} else if (index2 > index) {
sb.append(filename.substring(index2 + 1));
} else {
sb.append(filename);
}
// Add the suffix (may be an empty string)
sb.append(suffix);
return sb.toString();
}
public void tagAsMunged(SourceTypeBinding sourceType, TypePattern typePattern) {
if (typePattern instanceof ExactTypePattern) {
List<ExactTypePattern> annos = additionalParents.get(sourceType);
if (annos == null) {
annos = new ArrayList<ExactTypePattern>();
additionalParents.put(sourceType, annos);
}
annos.add((ExactTypePattern) typePattern);
}
}
/**
* Checks if the aspectj.pushin property is set - this is the main condition for triggering the creation of pushed-in source
* files. If not set just to 'true', the value of the property is processed as configuration. Configurable options are:
* <ul>
* <li>dir=XXXX - to set the output directory for the pushed in files
* <li>suffix=XXX - to set the suffix, can be blank to get just '.java'
* </ul>
*/
public static PushinCollector createInstance(World world) {
try {
String property = System.getProperty("aspectj.pushin");
if (property == null) {
return null;
}
Properties configuration = new Properties();
StringTokenizer tokenizer = new StringTokenizer(property, ",");
while (tokenizer.hasMoreElements()) {
String token = tokenizer.nextToken();
// Simplest thing to do is turn it on 'aspectj.pushin=true'
if (token.equalsIgnoreCase("true")) {
continue;
}
int positionOfEquals = token.indexOf("=");
if (positionOfEquals != -1) {
// it is an option
String optionName = token.substring(0, positionOfEquals);
String optionValue = token.substring(positionOfEquals + 1);
configuration.put(optionName, optionValue);
} else {
// it is a flag
configuration.put(token, "true");
}
}
return new PushinCollector(world, configuration);
} catch (Exception e) {
// unable to read system properties...
}
return null;
}
public void setOutputFileNameProvider(IOutputClassFileNameProvider outputFileNameProvider) {
this.outputFileNameProvider = outputFileNameProvider;
}
}