/**
* 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.m2t.merger.java.core;
import static org.eclipse.jdt.core.dom.CompilationUnit.IMPORTS_PROPERTY;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import net.atos.optimus.common.tools.jdt.ASTParserFactory;
import net.atos.optimus.common.tools.jdt.JavaCodeHelper;
import net.atos.optimus.m2t.merger.java.core.internal.MergerLogger;
import net.atos.optimus.m2t.merger.java.core.internal.MergerLoggerMessages;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.TextElement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jface.text.Document;
import org.eclipse.text.edits.MalformedTreeException;
/**
* Merge java sources.
*
* @author Maxence Vanbésien (mvaawl@gmail.com)
* @since 1.0
*/
@SuppressWarnings("unchecked")
public abstract class JavaCodeMerger {
/**
* Constant used for @Generated comment argument
*/
public static final String GENERATED_COMMENT_ARGUMENT = "comments";
/**
* This set contains the list of predefined annotations. This set is used to
* perform specific treatments on each annotation (e.g. Remove an annotation
* when it's needed)
*/
private Set<String> preDefinedAnnotations = new HashSet<String>(5);
/**
* A merging strategy instance. This object is used to customize the merging
* process.
*/
private MergingStrategy ms;
/**
* Empty constructor
*/
public JavaCodeMerger() {
}
/**
* Constructor with a special annotations set as single argument.
*/
public JavaCodeMerger(Set<String> pda) {
super();
if (pda != null) {
preDefinedAnnotations = pda;
}
}
/**
* Merge 2 Java contents and return the result of the merge operation as
* String
*
* @param existingContent
* The initial content
* @param generatedContent
* Result of a generation process
* @return The merge operation result as String
* @throws JavaModelException
* If an error occurs during merge operation
*/
public String merge(String existingContent, String generatedContent) throws JavaModelException {
return merge("", existingContent, generatedContent);
}
/**
* Merge a CompilationUnit in packageName from this name with generated
* content.
*
* @param compilationUnitName
* The name of the CompilationUnit without java extension
* @param newContent
* The content to store in this CompilationUnit
* @param packageFragment
* The source package where this CompilationUnit must be store
* @return The merge operation result as String
* @throws JavaModelException
* If an error occurs during merge operation
*/
public String merge(String compilationUnitName, String newContent, IPackageFragment packageFragment)
throws JavaModelException {
// Create ICompilationUnit object associated with initial source
ICompilationUnit initialIcp = packageFragment.getCompilationUnit(compilationUnitName + ".java");
// REFACTORING managment:
// If a compilatioUnit is not found with the same name, we look for one
// with the same UniqueId (only in same package)
// boolean classRenamed = false;
// if(initialIcp == null || !initialIcp.exists()) {
// String uniqueId = getUniqueIdFromClassContent(newContent,
// compilationUnitName);
// initialIcp = findCorrespondingCUByUniqueId(packageFragment,
// uniqueId);
// if (initialIcp != null && initialIcp.exists()) {
// classRenamed = true;
// }
// }
if (initialIcp != null && initialIcp.exists()) {
// Merge initial source and generated source
newContent = merge(initialIcp.getJavaProject().getElementName(), initialIcp.getSource(), newContent);
}
// if(classRenamed) {
// try {
// initialIcp.getResource().delete(true, null);
// } catch (CoreException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
return newContent;
}
/**
*
*
* @param project
* @param existingContent
* @param generatedContent
* @return
* @throws JavaModelException
*/
public String merge(String project, String existingContent, String generatedContent) throws JavaModelException {
if (existingContent == null || existingContent.length() == 0) {
return generatedContent;
}
if (generatedContent == null || generatedContent.length() == 0) {
return existingContent;
}
// Create a Document object for the existing content
Document existingDocument = new Document(existingContent);
ASTParser existingContentParser = ASTParserFactory.INSTANCE.newParser();
existingContentParser.setSource(existingDocument.get().toCharArray());
existingContentParser.setCompilerOptions(JavaCodeHelper.compilerOptions);
CompilationUnit existingCU = (CompilationUnit) existingContentParser.createAST(null);
// Create an ASTRewrite object used to update the existing content
ASTRewrite resultRewriter = ASTRewrite.create(existingCU.getAST());
// Create a Document object for generated content
Document generatedDocument = new Document(generatedContent);
// Create an AST parser for generated content (use JSL3 to support JDK
// 1.5)
ASTParser generatedContentParser = ASTParserFactory.INSTANCE.newParser();
generatedContentParser.setSource(generatedDocument.get().toCharArray());
generatedContentParser.setCompilerOptions(JavaCodeHelper.compilerOptions);
CompilationUnit generatedContentCU = (CompilationUnit) generatedContentParser.createAST(null);
String pack = "";
if (existingCU.getPackage() != null) {
pack = existingCU.getPackage().getName().getFullyQualifiedName();
}
try {
/* Create a merger logger instance */
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_START.value(pack, JavaCodeHelper.getMainType(existingCU)
.getName().getFullyQualifiedName(), project));
// Merge imports
mergeImports(existingCU, generatedContentCU, resultRewriter);
// For each type defined in the generated source...
for (AbstractTypeDeclaration generatedType : (List<AbstractTypeDeclaration>) generatedContentCU.types()) {
/*
* ... call the merge process
*/
mergeTwoFragments(existingCU, JavaCodeHelper.getTypeDeclaration(existingCU,
((AbstractTypeDeclaration) existingCU.types().get(0)).getName().getFullyQualifiedName()),
// TODO: find a better way!
generatedType, generatedDocument, resultRewriter);
}
// For each type defined in the existing source...
for (AbstractTypeDeclaration td : (List<AbstractTypeDeclaration>) existingCU.types()) {
/*
* ... call the merge process only if the type in the generated
* type doesn't exist
*/
/* The generatedType is a class or an interface */
if (JavaCodeHelper.getType(generatedContentCU, JavaCodeHelper.getTypeName(td)) == null) {
mergeTwoFragments(existingCU, td, null, generatedDocument, resultRewriter);
}
}
// Execute merge operations on existing content
try {
resultRewriter.rewriteAST(existingDocument, null).apply(existingDocument);
} catch (MalformedTreeException mte) {
throw new JavaModelException(mte, 0);
} catch (org.eclipse.jface.text.BadLocationException ble) {
throw new JavaModelException(ble, 0);
}
// Return result of the merging process as String object
return existingDocument.get();
} finally {
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_END.value(pack, JavaCodeHelper.getMainType(existingCU)
.getName().getFullyQualifiedName(), project));
}
}
/**
*
* @param classContentString
* @param typeNameInContent
* @return
*/
/*
* private String getUniqueIdFromClassContent(String classContentString,
* String typeNameInContent) { // Create a Document object for generated
* content Document classDocument = new Document(classContentString);
*
* // Create an AST parser for generated content (use JSL3 to support JDK //
* 1.5) ASTParser generatedContentParser = ASTParser.newParser(AST.JLS3);
* generatedContentParser.setSource(classDocument.get().toCharArray());
* generatedContentParser.setCompilerOptions(compilerOptions);
* CompilationUnit classContentCU = (CompilationUnit)
* generatedContentParser.createAST(null); return
* ASTHelper.getUniqueIdFromGeneratedAnnotation
* (JavaCodeHelper.getType(classContentCU, typeNameInContent)); }
*
* private ICompilationUnit findCorrespondingCUByUniqueId(IPackageFragment
* packageFragmentToSearchIn, String uniqueIdTofind){
* if(packageFragmentToSearchIn != null && uniqueIdTofind != null &&
* uniqueIdTofind.length() > 0) { try { ICompilationUnit[] compilationUnits
* = packageFragmentToSearchIn.getCompilationUnits(); for(ICompilationUnit
* compilationUnitsInSamePackage : compilationUnits) { IType
* compilationUnitType = compilationUnitsInSamePackage.getType(
* compilationUnitsInSamePackage.getElementName().replace(".java", ""));
* String uniqueIdFromComment = getUniqueId(compilationUnitType);
* if(uniqueIdFromComment != null &&
* uniqueIdFromComment.equals(uniqueIdTofind)) { return
* compilationUnitsInSamePackage; } } } catch (JavaModelException jme) {
* Activator.getDefault().logError("Error trying to find UniqueId (" +
* uniqueIdTofind + ") in Classes from package: " +
* packageFragmentToSearchIn.getElementName() , jme); } } return null; }
*
* private String getUniqueId(IType compilationUnitType) throws
* JavaModelException { if(compilationUnitType != null &&
* compilationUnitType.exists()) { IAnnotation generatedAnnotation =
* compilationUnitType.getAnnotation("Generated"); if(generatedAnnotation !=
* null && generatedAnnotation.exists()) { IMemberValuePair[] valuePairs =
* generatedAnnotation.getMemberValuePairs(); for(IMemberValuePair valuePair
* : valuePairs) { if(valuePair.getMemberName().equals("comments") &&
* valuePair.getValueKind() == IMemberValuePair.K_STRING) { String comment =
* (String)valuePair.getValue(); return
* GeneratedAnnotationHelper.getUniqueIdFromComment(comment); } } } } return
* null; }
*/
/**
* Merge imports
*/
private void mergeImports(CompilationUnit initialContentCU, CompilationUnit generatedContentCU,
ASTRewrite resultRewriter) {
// If at least one of these three parameters is null, we cannot process
// imports.
if (initialContentCU == null || generatedContentCU == null || resultRewriter == null) {
return;
}
List<String> initialImportsName = null;
// Retrieve the list of imports from initial source.
List<?> initialImports = initialContentCU.imports();
// Create an imports list in String format.
if (initialImports != null) {
initialImportsName = new ArrayList<String>(5);
for (Object o : initialImports) {
initialImportsName.add(((ImportDeclaration) o).getName().getFullyQualifiedName());
}
}
/*
* Get a ListRewrite object used to enhance existing imports with new
* generated imports
*/
ListRewrite lw = resultRewriter.getListRewrite(initialContentCU, IMPORTS_PROPERTY);
// For each import of the generated source
for (Object o : generatedContentCU.imports()) {
ImportDeclaration id = (ImportDeclaration) o;
if (!initialImportsName.contains(id.getName().getFullyQualifiedName())) {
// This import is containing in the generated code but not in
// the existing code => Add this import in the merge result.
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_ADDIMPORT.value(id.getName().getFullyQualifiedName()));
lw.insertLast(id, null);
}
}
}
/**
* Perform two fragments merging. To do this, take an
* {@code existingFragment}, take a {@code generatedFragment} and enhance
* {@code astr} ASTRewrite instance. If a fragment must be inserted, the
* complete fragment is inserted. If a fragment must be updated, only the
* generated fragment is updated (for exemple, with a type declaration
* fragment, only the declaration is updated, not fields and methods). If a
* fragment must be deleted, the complete fragment is deleted.
*
* @param parent
* The fragment parent
* @param existingFragment
* An existing fragment
* @param generatedFragment
* A generated fragment
* @param astr
* An ASTRewrite instance used to enhance the merge result
*/
void mergeTwoFragments(ASTNode parent, BodyDeclaration existingFragment, BodyDeclaration generatedFragment,
Document generatedDoc, ASTRewrite astr) {
if (parent != null) {
if (existingFragment == null && generatedFragment == null) {
/*
* Case 1 : Nothing in existing code, nothing in generated code
* => Nothing to do.
*/
return;
}
/* Get the right merger instance according to the type to merge */
FragmentMerger fm = getMerger(existingFragment, generatedFragment, generatedDoc, astr);
if (existingFragment == null) {
// The element doesn't exist in existing code.
if (isGenerated(generatedFragment)) {
/*
* Case 2 : Nothing in existing code, element marked as
* generated in generated code => Copy generated element in
* existing code.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_GENERATED.value(
JavaCodeHelper.getName(generatedFragment),
JavaCodeHelper.getDescription(generatedFragment)));
fm.insert(parent, generatedFragment);
} else {
/*
* Case 3 : Nothing in existing code, element marked as not
* generated in generated code => Copy not generated element
* in existing code.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_NOTGENERATED.value(
JavaCodeHelper.getName(generatedFragment),
JavaCodeHelper.getDescription(generatedFragment)));
fm.insert(parent, generatedFragment);
}
/*
* These two previous steps perform the same operation. But to
* understand the complete merging process, if and else has been
* kept.
*/
}
if (generatedFragment == null) {
// The element doesn't exist in generated code
if (isGenerated(existingFragment)) {
/*
* Case 4 : Element marked as generated in existing code,
* nothing in generated code => Remove element from existing
* code.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_EMPTYGENERATED.value(
JavaCodeHelper.getName(existingFragment),
JavaCodeHelper.getDescription(existingFragment)));
fm.remove(existingFragment);
} else {
/*
* Case 5 : Element marked as not generated in existing
* code, nothing in generated code => Nothing to do.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_NOMERGE.value(
JavaCodeHelper.getName(existingFragment),
JavaCodeHelper.getDescription(existingFragment)));
}
}
// The element exist in existing code and generated code.
if (existingFragment != null && generatedFragment != null) {
if (isGenerated(existingFragment)) {
// The exisiting element is marked as generated.
if (isGenerated(generatedFragment)) {
/*
* Case 6 : Element marked as generated in existing
* code, element marked as generated in generated code
* => Merge existing element with generated element.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_CASE6.value(
JavaCodeHelper.getName(existingFragment),
JavaCodeHelper.getDescription(existingFragment)));
fm.merge(existingFragment, generatedFragment, preDefinedAnnotations);
} else {
/*
* Case 7 : Element marked as generated in existing
* code, element marked as not generated in generated
* code => Nothing to do. Remark : Very special case.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_CASE7.value(
JavaCodeHelper.getName(existingFragment),
JavaCodeHelper.getDescription(existingFragment)));
}
} else {
// The exisiting element is marked as not generated.
if (isGenerated(generatedFragment)) {
/*
* Case 8 : Element marked as not generated in existing
* code, element marked as generated in generated code
* => Nothing to do.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_CASE8.value(
JavaCodeHelper.getName(existingFragment),
JavaCodeHelper.getDescription(existingFragment)));
} else {
/*
* Case 9 : Element marked as not generated in existing
* code, element marked as not generated in generated
* code => Nothing to do.
*/
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_DOMERGE_CASE9.value(
JavaCodeHelper.getName(existingFragment),
JavaCodeHelper.getDescription(existingFragment)));
}
}
// Javadoc is merged
mergeJavadoc(existingFragment, generatedFragment, astr);
}
/* Merge sub fragments if needed */
fm.mergeSubFragments(existingFragment, generatedFragment, generatedDoc);
}
}
/**
* Merge javadoc comment from existingFragment and generatedFragment to an
* AST Rewrite object instance. This method is call only when
* {@code existingFragment} is marked as generated and
* {@code generatedFragment} exist.
*
* @param existingFragment
* The existing fragment
* @param generatedFragment
* The generated fragment
* @param astr
* The ASTRewrite object instance containing the merge result
*/
protected void mergeJavadoc(BodyDeclaration existingFragment, BodyDeclaration generatedFragment, ASTRewrite astr) {
boolean existingCommentIsGenerated = false;
boolean generatedCommentIsGenerated = false;
boolean existingCommentFound = true;
boolean generatedCommentFound = true;
try {
existingCommentIsGenerated = isJavadocCommentGenerated(existingFragment.getJavadoc());
} catch (IllegalArgumentException iae) {
existingCommentFound = false;
}
try {
generatedCommentIsGenerated = isJavadocCommentGenerated(generatedFragment.getJavadoc());
} catch (IllegalArgumentException iae) {
generatedCommentFound = false;
}
if (existingCommentIsGenerated) {
if (!generatedCommentFound) {
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_REMOVE_JAVADOC.value(
JavaCodeHelper.getName(existingFragment), JavaCodeHelper.getDescription(existingFragment)));
astr.remove(existingFragment.getJavadoc(), null);
} else if (generatedCommentIsGenerated) {
if (MergerLogger.enabled())
MergerLogger.log(MergerLoggerMessages.MERGER_REPLACE_JAVADOC.value(
JavaCodeHelper.getName(existingFragment), JavaCodeHelper.getName(generatedFragment)));
astr.replace(existingFragment.getJavadoc(), generatedFragment.getJavadoc(), null);
}
} else if (!existingCommentFound && generatedCommentIsGenerated) {
if (MergerLogger.enabled())
MergerLogger
.log(MergerLoggerMessages.MERGER_ADD_JAVADOC.value(JavaCodeHelper.getName(existingFragment)));
astr.set(existingFragment, existingFragment.getJavadocProperty(), generatedFragment.getJavadoc(), null);
}
}
/**
* Return the good merge instance according to the type of the fragment to
* merge.
*
* @param existing
* The first fragment to merge. This parameter is used to know
* wich type must be merged.
* @param generated
* The second fragment to merge. This parameter is used only if
* existing is null.
*
*/
protected FragmentMerger getMerger(BodyDeclaration existing, BodyDeclaration generated, Document generatedDoc,
ASTRewrite astr) {
BodyDeclaration bd = (existing == null) ? generated : existing;
BodyDeclarationMerger bdm = null;
/*
* Check the bd type and return the right merger instance according to
* this type.
*/
if (bd instanceof FieldDeclaration) {
// It's a field, return a FieldMerger
bdm = new FieldDeclarationMerger(astr);
} else if (bd instanceof MethodDeclaration) {
// It's a method, return a MethodMerger
bdm = new MethodDeclarationMerger(astr, generatedDoc);
} else if (bd instanceof TypeDeclaration) {
// It's a type, return a TypeChecker taken this as parameter
bdm = new TypeMerger(this, astr);
} else if (bd instanceof EnumDeclaration) {
// It's an enumeration, return a EnumMerger taken this as parameter
bdm = new EnumMerger(this, astr);
} else if (bd instanceof EnumConstantDeclaration) {
// It's an enumeration, return a EnumMerger taken this as parameter
bdm = new EnumConstantMerger(astr);
}
if (ms != null) {
bdm.setMergingStrategy(ms);
}
return bdm;
}
/**
* Return true if the BodyDeclaration object has been generated. Generated
* informations is annotated with.
*
* @param bd
* BodyDeclaration used as input test
* @return true if the BodyDeclaration object has been generated, false
* otherwise
*/
protected boolean isGenerated(BodyDeclaration bd) {
boolean isGenerated = false;
List<?> modifiers = bd.modifiers();
// Test if this BodyDeclaration contains modifiers
if (modifiers != null) {
Iterator<?> modifiersIterator = bd.modifiers().iterator();
// For each modifier, search for @Generated(<GENERATOR_NAME>) marker
// annotation
while ((!isGenerated) && modifiersIterator.hasNext()) {
IExtendedModifier modifier = (IExtendedModifier) modifiersIterator.next();
if (modifier.isAnnotation()) {
Annotation a = (Annotation) modifier;
String annotationType = a.getTypeName().toString();
if (annotationType.equals(JavaCodeHelper.GENERATED_CLASSNAME)
|| annotationType.equals(JavaCodeHelper.GENERATED_SIMPLECLASSNAME)) {
if (a.isSingleMemberAnnotation()) {
isGenerated = ((SingleMemberAnnotation) a).getValue().toString()
.contains(getGeneratorName());
} else if (((Annotation) modifier).isNormalAnnotation()) {
NormalAnnotation na = (NormalAnnotation) a;
List<MemberValuePair> values = na.values();
for (int inc = 0; inc < values.size() && !isGenerated; inc++) {
MemberValuePair mvp = values.get(inc);
if (mvp != null && mvp.getValue() != null) {
isGenerated = mvp.getValue().toString().contains(getGeneratorName());
}
}
}
}
}
}
}
return isGenerated;
}
/**
* Return if a javadoc comment is generated or not
*/
private boolean isJavadocCommentGenerated(Javadoc jd) {
if (jd == null) {
throw new IllegalArgumentException();
}
boolean found = false;
List<TagElement> tags = jd.tags();
if (tags != null) {
TagElement te = null;
for (int inc = 0; inc < tags.size() && !found; inc++) {
te = tags.get(inc);
if ("@generated".equals(te.getTagName())) {
List<ASTNode> fragments = te.fragments();
if (fragments != null && fragments.size() == 1) {
if (fragments.get(0) instanceof TextElement) {
String commentText = ((TextElement) fragments.get(0)).getText().trim();
found = commentText != null && commentText.startsWith(getGeneratorName());
}
}
}
}
} else {
return false;
}
return found;
}
/**
* Return the generator name. The generator name is the value used by the
* code merger to know if the merge must be done or not.
*
* @return The generator name used by the merger to identify of the merge
* must be done or not
*/
protected abstract String getGeneratorName();
/**
* Set the merging strategy. This method must be used to customize the
* merging process.
*
* @param ms
* A Merging Strategy instance
*/
public void setMergingStrategy(MergingStrategy ms) {
this.ms = ms;
}
}