blob: 4d20cc33a2e9c08b96f173247c94c673a4024136 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011 University of Illinois at Urbana-Champaign and others.
* 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:
* UIUC - Initial API and implementation
*******************************************************************************/
package org.eclipse.rephraserengine.core.refactorings.simple;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.rephraserengine.core.refactorings.IEditorRefactoring;
import org.eclipse.text.edits.ReplaceEdit;
/**
* This is an extremely simple {@link IEditorRefactoring} implementation intended for use in
* demonstrations/simple examples. Production code should inherit from
* <code>VPGEditorRefactoring</code> or another {@link IEditorRefactoring} implementation instead.
* <p>
* Subclasses of this class will be similar in appearance to production refactorings (e.g., they
* will call {@link #findSelected(Class, Object)} and {@link #fail(String)}), but they don't require
* as much code (e.g., the refactoring name is automatically computed, and there is no separation
* between precondition checking and change creation).
* <p>
* Subclasses must override {@link #perform()}. Typically, the entire refactoring is performed
* inside this method. They should invoke {@link #fail(String)} if the refactoring encounters an
* error.
* <p>
* Subclasses may also override {@link #beforeUserInput()}, which is invoked before the wizard
* dialog is displayed.
*
* @author Jeff Overbey
*/
public abstract class SimpleEditorRefactoring extends Refactoring implements IEditorRefactoring
{
@SuppressWarnings("serial")
protected static final class PreconditionFailure extends Exception
{
public PreconditionFailure(String message)
{
super(message);
}
}
protected IFile file;
protected ITextSelection selection;
protected TextFileChange change;
public final void initialize(IFile file, ITextSelection selection)
{
this.file = file;
this.selection = selection;
this.change = new TextFileChange(getName(), file);
}
@Override
public final RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
OperationCanceledException
{
RefactoringStatus status = new RefactoringStatus();
try
{
beforeUserInput();
}
catch (PreconditionFailure e)
{
status.addFatalError(e.getMessage());
}
catch (Exception e)
{
status.addFatalError(e.getClass().getSimpleName() + ": " + e.getMessage()); //$NON-NLS-1$
}
return status;
}
/**
* Callback method invoked before the refactoring wizard is displayed to the user.
* <p>
* Subclasses may override this method.
*
* @throws Exception
*/
protected void beforeUserInput() throws Exception
{
// Do nothing
}
@Override
public final RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException,
OperationCanceledException
{
RefactoringStatus status = new RefactoringStatus();
try
{
String newFileContents = perform();
change.initializeValidationData(pm);
change.setEdit(new ReplaceEdit(0, countCharsIn(file), newFileContents));
}
catch (PreconditionFailure e)
{
status.addFatalError(e.getMessage());
}
catch (Exception e)
{
status.addFatalError(e.getClass().getSimpleName() + ": " + e.getMessage()); //$NON-NLS-1$
}
return status;
}
private int countCharsIn(IFile file) throws CoreException, IOException
{
int size = 0;
Reader in = new BufferedReader(new InputStreamReader(file.getContents(true), file.getCharset()));
while (in.read() > -1)
size++;
in.close();
return size;
}
/**
* Performs the refactoring, returning the new contents of the file.
* <p>
* Subclasses must override this method.
*
* @return the entire, revised contents of the file resulting from the refactoring
*
* @throws PreconditionFailure to display an error message to the user
* @throws Exception to display the exception class name and an error message to the user
*/
protected abstract String perform() throws Exception;
/**
* Causes the refactoring to fail, displaying the given error message to the user.
*
* @param message the error message to display
*
* @throws PreconditionFailure
*/
protected final void fail(String message) throws PreconditionFailure
{
throw new PreconditionFailure(message);
}
/**
* (Utility method) Finds the least AST node of the given type encompassing the user's text
* selection in the editor.
* <p>
* This implementation assumes a Ludwig-style AST where interior nodes have
* <code>getChildren</code> and <code>findFirst/LastToken</code> methods, and tokens have
* <code>getOffset</code> and <code>getLength</code> methods. These are invoked through
* reflection, so this is by no means an efficient implementation.
* <p>
* In production code, an efficient implementation of this method is typically provided by the
* VPG or AST.
*/
@SuppressWarnings("unchecked")
protected <T> T findSelected(Class<T> nodeType, Object node)
{
try
{
Method getChildren = node.getClass().getMethod("getChildren"); //$NON-NLS-1$
for (Object child : (Iterable< ? >)getChildren.invoke(node))
{
T result = findSelected(nodeType, child);
if (result != null) return result;
}
if (nodeType.isAssignableFrom(node.getClass()))
{
Object firstToken = node.getClass().getMethod("findFirstToken").invoke(node); //$NON-NLS-1$
Object lastToken = node.getClass().getMethod("findLastToken").invoke(node); //$NON-NLS-1$
if (firstToken != null && lastToken != null)
{
int firstOffset = ((Integer)firstToken.getClass()
.getMethod("getOffset").invoke(firstToken)).intValue(); //$NON-NLS-1$
int lastOffset = ((Integer)lastToken.getClass()
.getMethod("getOffset").invoke(lastToken)).intValue(); //$NON-NLS-1$
lastOffset += ((Integer)lastToken.getClass()
.getMethod("getLength").invoke(lastToken)).intValue(); //$NON-NLS-1$
if (selection.getLength() == 0 && firstOffset <= selection.getOffset()
&& selection.getOffset() < lastOffset)
{
// This is the least node enclosing the cursor position; that's good enough
return (T)node;
}
else if (selection.getOffset() == firstOffset
&& selection.getLength() == (lastOffset - firstOffset))
{
// The selection exactly encloses this node
return (T)node;
}
}
}
return null;
}
catch (Exception e)
{
throw new Error(e);
}
}
@Override
public final Change createChange(IProgressMonitor pm) throws CoreException,
OperationCanceledException
{
return change;
}
/**
* Returns the name of this refactoring.
* <p>
* The name of the refactoring is determined by the class name.
* <ul>
* <li> "ExtractLocalVariableRefactoring" => "Extract Local Variable"
* <li> "RenameRefactoring" => "Rename"
* </ul>
* Subclasses may override this method to customize the name.
*/
@Override
public String getName()
{
String name = getClass().getSimpleName();
if (name.toLowerCase().endsWith("refactoring")) //$NON-NLS-1$
name = name.substring(0, name.length() - "refactoring".length()); //$NON-NLS-1$
StringBuilder sb = new StringBuilder(64);
for (int i = 0, len = name.length(); i < len; i++)
{
char c = name.charAt(i);
if (Character.isUpperCase(c) && i > 0)
sb.append(" " + c); //$NON-NLS-1$
else
sb.append(c);
}
return sb.toString();
}
}