package org.codehaus.groovy.eclipse.refactoring.core.rename;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.codehaus.groovy.eclipse.core.GroovyCore;
import org.codehaus.groovy.eclipse.core.search.ISearchRequestor;
import org.codehaus.groovy.eclipse.core.search.SyntheticAccessorSearchRequestor;
import org.codehaus.jdt.groovy.model.GroovyNature;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.SourceRange;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.jdt.internal.corext.refactoring.SearchResultGroup;
import org.eclipse.jdt.internal.corext.refactoring.base.JavaStatusContext;
import org.eclipse.jdt.internal.corext.refactoring.base.ReferencesInBinaryContext;
import org.eclipse.jdt.internal.corext.refactoring.rename.RenameFieldProcessor;
import org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
/**
* A rename refactoring participant for renaming synthetic groovy properties and
* accessors.
*
* Renames calls to synthetic getters, setters and issers in groovy and java
* files for
* groovy properties
*
* Renames accesses to synthetic groovy properties that are backed by a getter,
* setter, and/or isser.
*
* @author andrew
* @created Oct 31, 2012
*/
public class SyntheticAccessorsRenameParticipant extends RenameParticipant {
private IMember renameTarget;
private List<SearchMatch> matches;
@Override
public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) throws OperationCanceledException {
RefactoringStatus status = new RefactoringStatus();
try {
if (shouldUpdateReferences()) {
matches = findExtraReferences(SubMonitor.convert(pm, "Finding synthetic Groovy references", 10));
} else {
matches = Collections.emptyList();
}
checkForBinaryRefs(matches, status);
SearchResultGroup[] grouped = convert(matches);
Checks.excludeCompilationUnits(grouped, status);
status.merge(Checks.checkCompileErrorsInAffectedFiles(grouped));
checkForPotentialRefs(matches, status);
} catch (CoreException e) {
GroovyCore.logException(e.getLocalizedMessage(), e);
return RefactoringStatus.createFatalErrorStatus(e.getLocalizedMessage());
}
return status;
}
private boolean shouldUpdateReferences() {
RefactoringProcessor processor = getProcessor();
if (processor instanceof RenameFieldProcessor) {
return ((RenameFieldProcessor) processor).getUpdateReferences();
} else if (processor instanceof RenameMethodProcessor) {
return ((RenameMethodProcessor) processor).getUpdateReferences();
}
// shouldn't get here
return true;
}
private void checkForPotentialRefs(List<SearchMatch> toCheck, RefactoringStatus status) {
for (SearchMatch match : toCheck) {
if (match.getAccuracy() == SearchMatch.A_INACCURATE) {
final RefactoringStatusEntry entry = new RefactoringStatusEntry(RefactoringStatus.WARNING,
RefactoringCoreMessages.RefactoringSearchEngine_potential_matches,
JavaStatusContext.create(JavaCore.createCompilationUnitFrom((IFile) match.getResource()), new SourceRange(match.getOffset(), match.getLength())));
status.addEntry(entry);
}
}
}
private void checkForBinaryRefs(List<SearchMatch> toCheck, RefactoringStatus status) throws JavaModelException {
ReferencesInBinaryContext binaryRefs = new ReferencesInBinaryContext(
"Elements containing binary references to refactored element ''" + renameTarget.getElementName() + "''");
for (Iterator<SearchMatch> iter = toCheck.iterator(); iter.hasNext();) {
SearchMatch match = iter.next();
if (isBinaryElement(match.getElement())) {
if (match.getAccuracy() == SearchMatch.A_ACCURATE) {
// binary classpaths are often incomplete -> avoiding false
// positives from inaccurate matches
binaryRefs.add(match);
}
iter.remove();
}
}
binaryRefs.addErrorIfNecessary(status);
}
private boolean isBinaryElement(Object element) throws JavaModelException {
if (element instanceof IMember) {
return ((IMember) element).isBinary();
} else if (element instanceof ICompilationUnit) {
return true;
} else if (element instanceof IClassFile) {
return false;
} else if (element instanceof IPackageFragment) {
return isBinaryElement(((IPackageFragment) element).getParent());
} else if (element instanceof IPackageFragmentRoot) {
return ((IPackageFragmentRoot) element).getKind() == IPackageFragmentRoot.K_BINARY;
}
return false;
}
private SearchResultGroup[] convert(List<SearchMatch> toGroup) {
Map<IResource, List<SearchMatch>> groups = new HashMap<IResource, List<SearchMatch>>(toGroup.size());
for (SearchMatch searchMatch : toGroup) {
if (searchMatch.getResource() == null) {
// likely a binary match. These are handled elsewhere
continue;
}
List<SearchMatch> group = groups.get(searchMatch.getResource());
if (group == null) {
group = new ArrayList<SearchMatch>();
groups.put(searchMatch.getResource(), group);
}
group.add(searchMatch);
}
SearchResultGroup[] results = new SearchResultGroup[groups.size()];
int i = 0;
for (Entry<IResource, List<SearchMatch>> group : groups.entrySet()) {
results[i++] = new SearchResultGroup(group.getKey(), group.getValue().toArray(new SearchMatch[0]));
}
return results;
}
@Override
public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
CompositeChange change = new CompositeChange(getName());
createMatchedChanges(matches, change, getNameMap());
if (change.getChildren().length > 0) {
return change;
}
return null;
}
@Override
public String getName() {
return "Rename Groovy synthetic getters and setters.";
}
/**
* Only activate participant if this is a method or field rename in a Groovy
* project. Must be source.
*/
@Override
protected boolean initialize(Object element) {
if (element instanceof IMethod || element instanceof IField) {
renameTarget = (IMember) element;
if (!renameTarget.isReadOnly()
&& GroovyNature.hasGroovyNature(renameTarget.getJavaProject().getProject())) {
return true;
}
}
return false;
}
private String accessorName(String prefix, String name) {
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
private void addChange(CompositeChange finalChange, IMember enclosingElement, int offset, int length, String newName) {
CompilationUnitChange existingChange = findOrCreateChange(enclosingElement, finalChange);
TextEditChangeGroup[] groups = existingChange.getTextEditChangeGroups();
TextEdit occurrenceEdit = new ReplaceEdit(offset, length, newName);
boolean isOverlapping = false;
for (TextEditChangeGroup group : groups) {
if (group.getTextEdits()[0].covers(occurrenceEdit)) {
isOverlapping = true;
break;
}
}
if (isOverlapping) {
// don't step on someone else's feet
return;
}
existingChange.addEdit(occurrenceEdit);
existingChange.addChangeGroup(new TextEditChangeGroup(existingChange, new TextEditGroup(
"Update synthetic Groovy accessor",
occurrenceEdit)));
}
private String basename(String fullName) {
int baseStart;
if (fullName.startsWith("is") && fullName.length() > 2 && Character.isUpperCase(fullName.charAt(2))) {
baseStart = 2;
} else if ((fullName.startsWith("get") || fullName.startsWith("set")) && fullName.length() > 3
&& Character.isUpperCase(fullName.charAt(3))) {
baseStart = 3;
} else {
baseStart = -1;
}
if (baseStart > 0) {
return Character.toLowerCase(fullName.charAt(baseStart)) + fullName.substring(baseStart + 1);
} else {
return fullName;
}
}
private void createMatchedChanges(List<SearchMatch> references, CompositeChange finalChange, Map<String, String> nameMap)
throws JavaModelException {
for (SearchMatch searchMatch : references) {
Object elt = searchMatch.getElement();
if (elt instanceof IMember) {
String oldName = findMatchName(searchMatch, nameMap.keySet());
if (oldName != null) {
String newName = nameMap.get(oldName);
addChange(finalChange, (IMember) elt, searchMatch.getOffset(), oldName.length(), newName);
}
}
}
}
private List<SearchMatch> findExtraReferences(IProgressMonitor pm) throws CoreException {
SyntheticAccessorSearchRequestor synthRequestor = new SyntheticAccessorSearchRequestor();
final List<SearchMatch> matches = new ArrayList<SearchMatch>();
synthRequestor.findSyntheticMatches(renameTarget, new ISearchRequestor() {
public void acceptMatch(SearchMatch match) {
matches.add(match);
}
}, SubMonitor.convert(pm, "Find synthetic accessors", 10));
return matches;
}
private String findMatchName(SearchMatch searchMatch, Set<String> keySet) throws JavaModelException {
IJavaElement element = JavaCore.create(searchMatch.getResource());
if (element.getElementType() == IJavaElement.COMPILATION_UNIT) {
ICompilationUnit unit = (ICompilationUnit) element;
String matchedText = unit.getBuffer().getText(searchMatch.getOffset(), searchMatch.getLength());
for (String oldName : keySet) {
if (matchedText.startsWith(oldName)) {
return oldName;
}
}
}
return null;
}
private CompilationUnitChange findOrCreateChange(IMember accessor, CompositeChange finalChange) {
TextChange textChange = getTextChange(accessor.getCompilationUnit());
CompilationUnitChange existingChange = null;
if (textChange instanceof CompilationUnitChange) {
// check to see if change exists from some other part of the
// refactoring
existingChange = (CompilationUnitChange) textChange;
} else {
// check to see if we have already touched this file
Change[] children = finalChange.getChildren();
for (Change change : children) {
if (change instanceof CompilationUnitChange) {
if (((CompilationUnitChange) change).getCompilationUnit().equals(accessor.getCompilationUnit())) {
existingChange = (CompilationUnitChange) change;
break;
}
}
}
}
if (existingChange == null) {
// nope...must create a new change
existingChange = new CompilationUnitChange("Synthetic Groovy Accessor changes for "
+ accessor.getCompilationUnit().getElementName(), accessor.getCompilationUnit());
existingChange.setEdit(new MultiTextEdit());
finalChange.add(existingChange);
}
return existingChange;
}
private Map<String, String> getNameMap() {
Map<String, String> nameMap = new HashMap<String, String>();
String newBaseName = basename(getArguments().getNewName());
String oldBaseName = basename(renameTarget.getElementName());
nameMap.put(oldBaseName, newBaseName);
nameMap.put(accessorName("is", oldBaseName), accessorName("is", newBaseName));
nameMap.put(accessorName("get", oldBaseName), accessorName("get", newBaseName));
nameMap.put(accessorName("set", oldBaseName), accessorName("set", newBaseName));
return nameMap;
}
}