/**
* Optimus, framework for Model Transformation
*
* Copyright (C) 2013 Worldline or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors.
*
* 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 Software Foundation; either
* version 2.1 of the License.
*
* 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 A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* 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 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.atos.optimus.common.tools.ltk;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.atos.optimus.common.tools.Activator;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.TypeDeclaration;
/**
* Imports Generation Visitor tool. The role of this tool is to parse all the
* names located in a class, in order to extract imports and trim package names
* from fully qualified names
*
* @author Maxence Vanbésien (mvaawl@gmail.com)
* @since 1.0
*
*/
public class ImportsGenerationVisitor extends ASTVisitor {
/**
* True if the source has been modified, false otherwise
*/
private boolean hasModifications = false;
/**
* Contains all the resolved imports to add to the source code
*/
private Map<String, ImportDeclaration> resolvedImports = new LinkedHashMap<String, ImportDeclaration>();
/**
* Contains the Imports to be associated with Simple Named Type.
*
* e.g. List -> java.util.List or Generated -> javax.annotation.Generated.
*
* Used to avoid conflicts if two classes with same simple name and
* different packages are used.
*
*/
private Map<String, String> typesToPackageBinding = new LinkedHashMap<String, String>();
/**
* Name of the Package containing the currently processed element
*/
private String currentPackageName;
/**
* Name of the currently processed element
*/
private String currentTypeName;
/*
* Private Constructor
*/
private ImportsGenerationVisitor() {
super(true);
}
/**
* Applies the Import Generation Visitor on the Compilation Unit passed as
* parameter. Returns whether modifications were done, to tell the caller to
* persist the modifications.
*
* @param unit
* : Compilation Unit
* @return true if need to write modifications, false if nothing has
* changed.
*/
public static boolean apply(CompilationUnit unit) {
try {
ImportsGenerationVisitor visitor = new ImportsGenerationVisitor();
unit.accept(visitor);
visitor.addMissingImports(unit);
return visitor.hasModifications;
} catch (Exception e) {
Activator.getDefault().logError("Exception encountered while generating imports", e);
}
return false;
}
/**
* Adds all the missing imports detected by this visitor, to the compilation
* unit passed as parameter.
*
* @param unit
* : CompilationUnit
*/
@SuppressWarnings("unchecked")
private void addMissingImports(CompilationUnit unit) {
Collection<ImportDeclaration> values = resolvedImports.values();
for (ImportDeclaration value : values) {
String fullyQualifiedName = value.getName().getFullyQualifiedName();
boolean found = false;
Iterator<?> iterator = unit.imports().iterator();
while (iterator.hasNext() && !found) {
Object next = iterator.next();
if (next instanceof ImportDeclaration) {
String existingImport = ((ImportDeclaration) next).getName()
.getFullyQualifiedName();
if (fullyQualifiedName.equals(existingImport))
found = true;
}
}
if (!found)
unit.imports().add(value);
}
}
@Override
public boolean visit(CompilationUnit node) {
if(node.getPackage() != null && node.getPackage().getName() != null) {
this.currentPackageName = node.getPackage().getName().getFullyQualifiedName();
} else {
this.currentPackageName = "";
}
// Prefills the list with the imports that already exist in the list.
for (Object o : node.imports()) {
ImportDeclaration declaration = (ImportDeclaration) o;
IBinding resolvedBinding = declaration.resolveBinding();
if (resolvedBinding instanceof ITypeBinding) {
ITypeBinding resolvedTypeBinding = (ITypeBinding) resolvedBinding;
if (resolvedTypeBinding != null && !resolvedTypeBinding.isRecovered()) {
resolvedTypeBinding = resolvedTypeBinding.getErasure();
String typeName = resolvedTypeBinding.getName();
IPackageBinding packageBinding = resolvedTypeBinding.getPackage();
if (packageBinding != null) {
String packageName = packageBinding.getName();
this.typesToPackageBinding.put(typeName, packageName);
}
}
}
}
return true;
}
@Override
public boolean visit(TypeDeclaration node) {
if (this.currentTypeName == null)
this.currentTypeName = node.getName().getFullyQualifiedName();
if (this.currentTypeName != null && this.currentPackageName != null)
this.typesToPackageBinding.put(this.currentTypeName, this.currentPackageName);
return true;
}
@Override
public boolean visit(EnumDeclaration node) {
if (this.currentTypeName == null)
this.currentTypeName = node.getName().getFullyQualifiedName();
if (this.currentTypeName != null && this.currentPackageName != null)
this.typesToPackageBinding.put(this.currentTypeName, this.currentPackageName);
return true;
}
/**
* Checks Import Generation for Marker Annotation
*/
@Override
public boolean visit(MarkerAnnotation node) {
if (node == null)
return super.visit(node);
ITypeBinding typeBinding = node.resolveTypeBinding();
if (typeBinding == null)
return super.visit(node);
Name typeName = node.getTypeName();
this.checkType(node, typeName, typeBinding);
return super.visit(node);
}
/**
* Checks Import Generation for Normal Annotation
*/
@Override
public boolean visit(NormalAnnotation node) {
if (node == null)
return super.visit(node);
ITypeBinding typeBinding = node.resolveTypeBinding();
if (typeBinding == null)
return super.visit(node);
Name typeName = node.getTypeName();
this.checkType(node, typeName, typeBinding);
return super.visit(node);
}
/**
* Checks Import Generation for Single Member Annotation
*/
@Override
public boolean visit(SingleMemberAnnotation node) {
if (node == null)
return super.visit(node);
ITypeBinding typeBinding = node.resolveTypeBinding();
if (typeBinding == null)
return super.visit(node);
Name typeName = node.getTypeName();
this.checkType(node, typeName, typeBinding);
return super.visit(node);
}
/**
* Checks Import Generation for Simple Type (most common case)
*/
@Override
public boolean visit(SimpleType node) {
if (node == null)
return super.visit(node);
ITypeBinding typeBinding = node.resolveBinding();
if (typeBinding == null)
return super.visit(node);
Name typeName = node.getName();
this.checkType(node, typeName, typeBinding);
return super.visit(node);
}
/**
* Checks Import Generation for Qualified Name.
*
* This is a specific case, in order to deal with Enumerations & static
* method invocations.
*/
@Override
public boolean visit(QualifiedName node) {
// We are is an import. No need to trim anything !!!
if (node.getParent() instanceof ImportDeclaration)
return super.visit(node);
// We get the current binding
IBinding binding = node.resolveBinding();
if (binding != null) {
// We check if we are dealing with a type
// binding (Case when Enumerations)
if (binding.getKind() == IBinding.TYPE && !((ITypeBinding) binding).isRecovered()) {
checkQualifiedType(node.getParent(), node, (ITypeBinding) binding);
} else {
// We check if we are dealing with a Method Binding referencing
// a Type Binding (Case when static methods invocations)
if (node.getQualifier().isQualifiedName()
&& Modifier.isStatic(binding.getModifiers())) {
QualifiedName qualifier = (QualifiedName) node.getQualifier();
IBinding binding2 = qualifier.resolveBinding();
if (binding2 != null && binding2.getKind() == IBinding.TYPE
&& !((ITypeBinding) binding2).isRecovered()) {
checkQualifiedType(node, qualifier, (ITypeBinding) binding2);
}
}
}
}
return super.visit(node);
}
/**
* Checks Type, represented by its use in parent node, its name, and its
* type binding.
*
* @param node
* : Parent Node
* @param typeName
* : Type Name
* @param typeBinding
* : Type Binding
*/
private void checkType(ASTNode node, Name typeName, ITypeBinding typeBinding) {
if (typeName.isSimpleName())
this.checkSimpleType(node, (SimpleName) typeName, typeBinding);
else
this.checkQualifiedType(node, (QualifiedName) typeName, typeBinding);
}
/**
* Checks Qualified Type, represented by its use in parent node, its name,
* and its type binding.
*
* @param node
* : Parent Node
* @param qualifiedName
* : Qualified Name
* @param typeBinding
* : Type Binding
*/
@SuppressWarnings("unchecked")
private void checkQualifiedType(ASTNode node, QualifiedName qualifiedName,
ITypeBinding typeBinding) {
// At first we extract package name & type for Type, by :
// - Splitting the String if Type Binding has been deduced (recovered)
// - Taking it from Binding otherwise
String fullyQualifiedName = qualifiedName.getFullyQualifiedName();
String typeName = null;
String packageName = null;
if (typeBinding == null || typeBinding.isRecovered()) {
typeName = qualifiedName.getFullyQualifiedName().substring(
qualifiedName.getFullyQualifiedName().lastIndexOf(".") + 1);
packageName = qualifiedName.getFullyQualifiedName().substring(0,
qualifiedName.getFullyQualifiedName().lastIndexOf("."));
} else {
typeBinding = typeBinding.getErasure();
typeName = typeBinding.getName();
IPackageBinding packageBinding = typeBinding.getPackage();
if (packageBinding == null)
return;
packageName = packageBinding.getName();
}
// Checks if name should be trimmed (if class with same name but
// different package has already been registered), and trims it if
// needed
if (shouldTrimName(packageName, typeName)) {
StructuralPropertyDescriptor locationInParent = qualifiedName.getLocationInParent();
if (locationInParent == null)
return;
if (locationInParent.isChildListProperty()) {
ChildListPropertyDescriptor clpd = (ChildListPropertyDescriptor) locationInParent;
List<ASTNode> astNodes = (List<ASTNode>) node.getStructuralProperty(clpd);
astNodes.remove(qualifiedName);
astNodes.add(node.getAST().newName(typeName));
} else {
node.setStructuralProperty(locationInParent, node.getAST().newName(typeName));
}
hasModifications = true;
}
// Checks if import should be added (e.g. package is not java.lang) and
// does it if needed
if (shouldAddImport(node, typeName, packageName, fullyQualifiedName)) {
this.tryAddImport(node.getAST(), fullyQualifiedName);
if (!typesToPackageBinding.containsKey(typeName))
typesToPackageBinding.put(typeName, packageName);
} else if (this.currentPackageName.equals(packageName))
if (!typesToPackageBinding.containsKey(typeName))
typesToPackageBinding.put(typeName, packageName);
}
/**
* Checks Simple Type, represented by its use in parent node, its name, and
* its type binding.
*
* @param node
* : Parent Node
* @param simpleName
* : Simple Name
* @param typeBinding
* : Type Binding
*/
private void checkSimpleType(ASTNode node, SimpleName simpleName, ITypeBinding typeBinding) {
// If type binding is null, recovered or type corresponds to primitive:
// returns.
if (typeBinding == null || typeBinding.isRecovered() || typeBinding.getPackage() == null)
return;
// Extracts information from Type Binding.
String packageName = typeBinding.getErasure().getPackage().getName();
String typeName = typeBinding.getErasure().getName();
String fullyQualifiedName = typeBinding.getErasure().getQualifiedName();
// Checks if need to add associated import, and adds it if necessary
if (shouldAddImport(node, typeName, packageName, fullyQualifiedName)) {
this.tryAddImport(node.getAST(), fullyQualifiedName);
if (!typesToPackageBinding.containsKey(typeName))
typesToPackageBinding.put(typeName, packageName);
}
}
/**
* Creates and stores import declaration for FQN passed as parameter, is it
* has not been registered yet.
*
* @param ast
* : AST, needed to create the ImportDeclaration object itself
* @param fullyQualifiedName
* : Import FQN
*/
private void tryAddImport(AST ast, String fullyQualifiedName) {
if (!this.resolvedImports.containsKey(fullyQualifiedName)) {
ImportDeclaration importDeclaration = ast.newImportDeclaration();
importDeclaration.setName(ast.newName(fullyQualifiedName));
this.resolvedImports.put(fullyQualifiedName, importDeclaration);
}
hasModifications = true;
}
/**
* Determines whether an import should be added for the Type, which
* information are passed as parameter.
*
* @param node
* : Current Node
* @param typeName
* : Type Simple Name
* @param packageName
* : Type's Package Name
* @param fullyQualifiedName
* Types FQN
* @return true if need to add import, false otherwise
*/
private boolean shouldAddImport(ASTNode node, String typeName, String packageName,
String fullyQualifiedName) {
// Determines the package name of the current CU containing this node.
// This way, no import is added if I am in the same package.
String currentPackageName = getPackageDeclarationForNode(node) != null ? getPackageDeclarationForNode(
node).getName().getFullyQualifiedName()
: "";
// If there is a conflict with the current class name, we cannot add an
// import...
if (this.currentTypeName != null && this.currentPackageName != null)
if (this.currentTypeName.equals(typeName)
&& !this.currentPackageName.equals(packageName))
return false;
// Conditions to add a new import:
// - Package Name is not null
// - Package Name not yet registered
// - Package is not "java.lang"
// - Package is not the current CU's package
return packageName != null && !this.typesToPackageBinding.containsKey(typeName)
&& !this.resolvedImports.containsKey(fullyQualifiedName)
&& !packageName.equals("java.lang") && !packageName.equals(currentPackageName);
}
/**
* Determines if we should trim name for the current Type described by its
* simple name and package name.
*
* @param packageName
* @param typeName
* @return true if name can be trimmed, false otherwise
*/
private boolean shouldTrimName(String packageName, String typeName) {
// If there is a conflict with the current class name, we cannot trim
// name
if (this.currentTypeName != null && this.currentPackageName != null)
if (this.currentTypeName.equals(typeName)
&& !this.currentPackageName.equals(packageName))
return false;
// If the typeName is not yet registered with another package, we can
// trim it !
return this.typesToPackageBinding.get(typeName) == null
|| packageName.equals(this.typesToPackageBinding.get(typeName));
}
/**
* Returns Package declaration of the CU containing this node.
*
* @param node
* : Node
* @return Package Declaration.
*/
private PackageDeclaration getPackageDeclarationForNode(ASTNode node) {
ASTNode root = node.getRoot();
if (root instanceof CompilationUnit)
return ((CompilationUnit) root).getPackage();
return null;
}
}