blob: bf3bb5a678016229fe324df12534d7888d81c691 [file] [log] [blame]
/*
* Copyright (c) 2005, 2010 Borland Software Corporation 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:
* Artem Tikhomirov (Borland) - initial API and implementation
*/
package org.eclipse.gmf.internal.common.codegen;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.emf.codegen.util.CodeGenUtil;
import org.eclipse.emf.codegen.util.CodeGenUtil.EclipseUtil;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.DiagnosticException;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gmf.common.UnexpectedBehaviourException;
import org.eclipse.gmf.common.codegen.ImportAssistant;
import org.eclipse.gmf.internal.common.Activator;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaProject;
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.formatter.CodeFormatter;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.text.edits.TextEdit;
/**
* XXX do I really need refreshLocal in doGenerate[Binary]File? Guess, not.
* @author artem
*/
public abstract class GeneratorBase implements Runnable {
private CodeFormatter myCodeFormatter;
private final CodeFormatterFactory myCodeFormatterFactory;
private OrganizeImportsPostprocessor myImportsPostprocessor;
private IProgressMonitor myProgress = new NullProgressMonitor();
// myDestRoot.getJavaProject().getElementName() == myDestProject.getName()
private IPackageFragmentRoot myDestRoot;
private IProject myDestProject;
private final List<IStatus> myExceptions;
private IStatus myRunStatus = Status.CANCEL_STATUS;
private TextMerger myMerger;
private final boolean isToRestoreExistingImports = true;
protected abstract void customRun() throws InterruptedException, UnexpectedBehaviourException;
protected abstract void setupProgressMonitor();
public GeneratorBase() {
this(CodeFormatterFactory.DEFAULT);
}
public GeneratorBase(CodeFormatterFactory codeFormatterFactory) {
myCodeFormatterFactory = codeFormatterFactory;
myExceptions = new LinkedList<IStatus>();
}
public void run(IProgressMonitor progress) throws InterruptedException {
setProgressMonitor(progress);
clearExceptionsList();
doRun();
}
public void run() {
clearExceptionsList();
try {
doRun();
} catch (InterruptedException ex) {
myRunStatus = new Status(IStatus.CANCEL, Activator.getID(), 0, Messages.interrupted, ex);
}
}
/**
* Provides information about success/failures during {@link #run()}
* @return state of the generator run, or CANCEL if generator was not yet run.
*/
public IStatus getRunStatus() {
return myRunStatus;
}
/**
* Optionally, specify progressMonitor to use. Should be called prior to {@link #run()}
* @param progress
*/
public void setProgressMonitor(IProgressMonitor progress) {
myProgress = progress;
}
protected final void handleException(CoreException ex) {
handleException(ex.getStatus());
}
protected final void handleException(IStatus status) {
myExceptions.add(status);
}
protected final void handleException(Throwable ex) {
if (ex instanceof DiagnosticException) {
// unwind invocation target exceptions as much as possible
final Diagnostic diagnostic = ((DiagnosticException) ex).getDiagnostic();
if (diagnostic.getException() instanceof InvocationTargetException) {
Throwable originalEx = ((InvocationTargetException) diagnostic.getException()).getCause();
handleException(newStatus(originalEx));
} else {
handleException(BasicDiagnostic.toIStatus(diagnostic));
}
} else {
handleException(newStatus(ex));
}
}
/**
* by default, process as ordinary exception
*/
protected void handleUnexpected(UnexpectedBehaviourException ex) {
handleException(ex);
}
protected static IStatus newStatus(Throwable ex) {
return newStatus(IStatus.ERROR, ex);
}
protected static IStatus newStatus(int severity, Throwable ex) {
final String msg = ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage();
return new Status(severity, Activator.getID(), 0, Messages.bind(Messages.exception, msg), ex);
}
protected final IProject getDestProject() {
return myDestProject;
}
protected final IProgressMonitor getProgress() {
return myProgress;
}
/**
* @param task optional string to be shown in the progress dialog
* @param total estimation of number of activities to happen
*/
protected final void setupProgressMonitor(String task, int total) {
if (myProgress == null) {
myProgress = new NullProgressMonitor();
return;
// no need to set it up
}
myProgress.beginTask(task == null ? Messages.start : task, total);
}
protected final IProgressMonitor getNextStepMonitor() throws InterruptedException {
if (myProgress.isCanceled()) {
throw new InterruptedException();
}
return new SubProgressMonitor(myProgress, 1);
}
/**
* @see #initializeEditorProject(String, IPath, List)
*/
protected final void initializeEditorProject(String pluginId, IPath projectLocation) throws UnexpectedBehaviourException, InterruptedException {
initializeEditorProject(pluginId, projectLocation, Collections.<IProject> emptyList());
}
/**
* Delegates to {@link #initializeEditorProject(IPath, IPath, List)}, using plug-in id as workspace project name and
* 'src' as Java sources location.
* @param pluginId both name of workspace project and plug-in id
* @param projectLocation {@link IPath} to folder where <code>.project</code> file would reside. Use <code>null</code> to use default workspace location.
* @param referencedProjects collection of {@link IProject}
*
*/
protected final void initializeEditorProject(String pluginId, IPath projectLocation, List<IProject> referencedProjects) throws UnexpectedBehaviourException, InterruptedException {
// not sure if there's any reason to get project's name via IProject (not use pluginId directly), this is just how it was done from 1.1.
IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(pluginId);
initializeEditorProject(new Path('/' + p.getName() + "/src"), projectLocation, referencedProjects);
}
/**
* @param javaSource workspace absolute path to java source folder of the generated project, e.g. '/org.sample.aaa/sources'.
* @param projectLocation {@link IPath} to folder where <code>.project</code> file would reside. Use <code>null</code> to use default workspace location.
* @param referencedProjects collection of {@link IProject}
* @throws UnexpectedBehaviourException something goes really wrong
* @throws InterruptedException user canceled operation
*/
protected final void initializeEditorProject(IPath javaSource, IPath projectLocation, List<IProject> referencedProjects) throws UnexpectedBehaviourException, InterruptedException {
myDestProject = ResourcesPlugin.getWorkspace().getRoot().getProject(javaSource.segment(0));
final int style = org.eclipse.emf.codegen.ecore.Generator.EMF_PLUGIN_PROJECT_STYLE;
// pluginVariables is NOT used when style is EMF_PLUGIN_PROJECT_STYLE
final List<?> pluginVariables = null;
final IProgressMonitor pm = getNextStepMonitor();
setProgressTaskName(Messages.initproject);
org.eclipse.emf.codegen.ecore.Generator.createEMFProject(javaSource, projectLocation, referencedProjects, pm, style, pluginVariables);
try {
final IJavaProject jp = JavaCore.create(myDestProject);
myDestRoot = jp.findPackageFragmentRoot(javaSource);
// createEMFProject doesn't create source entry in case project exists and has some classpath entries already,
// though the folder gets created.
if (myDestRoot == null) {
IClasspathEntry[] oldCP = jp.getRawClasspath();
IClasspathEntry[] newCP = new IClasspathEntry[oldCP.length + 1];
System.arraycopy(oldCP, 0, newCP, 0, oldCP.length);
newCP[oldCP.length] = JavaCore.newSourceEntry(javaSource);
jp.setRawClasspath(newCP, new NullProgressMonitor());
myDestRoot = jp.findPackageFragmentRoot(javaSource);
}
} catch (JavaModelException ex) {
throw new UnexpectedBehaviourException(ex.getMessage());
}
if (myDestRoot == null) {
throw new UnexpectedBehaviourException("no source root can be found");
}
}
/**
* Generate ordinary file.
* @param emitter template to use
* @param filePath - project-relative path to file, e.g. META-INF/MANIFEST.MF
* @param param TODO
* @throws InterruptedException
*/
protected final void doGenerateFile(TextEmitter emitter, IPath filePath, Object... param) throws InterruptedException {
assert !myDestProject.getName().equals(filePath.segment(0));
IProgressMonitor pm = getNextStepMonitor();
try {
setProgressTaskName(filePath.lastSegment());
pm.beginTask(null, 5);
IPath containerPath = myDestProject.getFullPath().append(filePath.removeLastSegments(1));
EclipseUtil.findOrCreateContainer(containerPath, false, (IPath) null, new SubProgressMonitor(pm, 1));
String genText = emitter.generate(new SubProgressMonitor(pm, 1), param);
IFile f = myDestProject.getFile(filePath);
final boolean propertyFile = "properties".equals(filePath.getFileExtension());
String charset = propertyFile ? "ISO-8859-1" : "UTF-8";
if (propertyFile) {
genText = Conversions.escapeUnicode(genText);
}
String oldText = null;
if (f.exists()) {
oldText = FileServices.getFileContents(f);
}
if (oldText != null) {
genText = mergePlainText(oldText, genText, f, new SubProgressMonitor(pm, 1));
if (!oldText.equals(genText)) {
f.setContents(new ByteArrayInputStream(genText.getBytes(charset)), true, true, new SubProgressMonitor(pm, 1));
} else {
pm.worked(1);
}
} else {
f.create(new ByteArrayInputStream(genText.getBytes(charset)), true, new SubProgressMonitor(pm, 2));
}
} catch (InvocationTargetException ex) {
handleException(ex.getCause());
} catch (UnexpectedBehaviourException ex) {
handleUnexpected(ex);
} catch (CoreException ex) {
handleException(ex);
} catch (UnsupportedEncodingException ex) {
handleException(ex);
} finally {
pm.done();
}
}
/**
* Inspired by GenBaseImpl.EclipseUtil.findOrCreateContainer
* Although later (with EMF API adopting Platform changes) we might need to return URI here
* @return path suitable for IProjectDescription, or <code>null</code> to indicate use of default
*/
protected final IPath guessNewProjectLocation(Path examplaryProjectPath, String newProjectName) {
assert newProjectName != null;
try {
if (ResourcesPlugin.getWorkspace().getRoot().getProject(newProjectName).exists()) {
// just use whatever already specified.
// Returned value doesn't make sense in this case -
// oee.codegen.ecore.Generator#EclipseHelper#createEMFProject doesn't use it then.
return null;
}
if (examplaryProjectPath == null || !examplaryProjectPath.isAbsolute()) {
return null;
}
IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(examplaryProjectPath.segment(0));
if (!p.exists()) {
return null;
}
java.net.URI locationURI = p.getDescription().getLocationURI();
// org.eclipse.core.internal.utils.FileUtil#toPath
if (locationURI == null) {
return null;
}
if (locationURI.getScheme() != null && !"file".equals(locationURI.getScheme())) {
return null;
}
return new Path(locationURI.getSchemeSpecificPart()).removeLastSegments(1).append(newProjectName);
} catch (CoreException ex) {
handleException(newStatus(IStatus.WARNING, ex));
return null;
}
}
protected final ImportAssistant createImportAssistant(String packageName, String className) {
return new ImportUtil(packageName, className, myDestRoot);
}
protected final void doGenerate(JavaClassEmitter emitter, Object... input) throws InterruptedException, UnexpectedBehaviourException {
if (emitter != null) {
doGenerateJavaClass(emitter, emitter.getQualifiedClassName(input), input);
}
}
protected final void doGenerateJavaClass(TextEmitter emitter, String qualifiedClassName, Object... input) throws InterruptedException {
if (emitter != null) {
doGenerateJavaClass(emitter, CodeGenUtil.getPackageName(qualifiedClassName), CodeGenUtil.getSimpleClassName(qualifiedClassName), input);
}
}
/**
* NOTE: potential problem - packageName and className should match those specified in
* the template. Besides, getQualifiedXXX helpers in diagram GenModel should also correctly
* return qualified class names.
*/
protected final void doGenerateJavaClass(TextEmitter emitter, String packageName, String className, Object... input) throws InterruptedException {
IProgressMonitor pm = getNextStepMonitor();
setProgressTaskName(className);
pm.beginTask(null, 7);
if (emitter == null) {
pm.done();
return;
}
try {
String genText = emitter.generate(new SubProgressMonitor(pm, 2), input);
IPackageFragment pf = myDestRoot.createPackageFragment(packageName, true, new SubProgressMonitor(pm, 1));
ICompilationUnit cu = pf.getCompilationUnit(className + ".java"); //$NON-NLS-1$
if (cu.exists()) {
ICompilationUnit workingCopy = null;
try {
workingCopy = cu.getWorkingCopy(new SubProgressMonitor(pm, 1));
final String oldContents = workingCopy.getSource();
IImportDeclaration[] declaredImports = workingCopy.getImports();
workingCopy.getBuffer().setContents(genText);
workingCopy.reconcile(ICompilationUnit.NO_AST, false, null, null);
try {
//Since we do organizeImports prior to merge, we must ensure imports added manually are known to OrganizeImportsProcessor
String[] declaredImportsAsStrings = new String[declaredImports.length];
for (int i = 0; i < declaredImports.length; i++) {
declaredImportsAsStrings[i] = declaredImports[i].getElementName();
}
getImportsPostrocessor().organizeImports(workingCopy, declaredImportsAsStrings, new SubProgressMonitor(pm, 1));
} catch (CoreException e) {
workingCopy.commitWorkingCopy(true, new SubProgressMonitor(pm, 1)); // save to investigate contents
throw e;
}
genText = mergeJavaCode(oldContents, workingCopy.getSource(), new SubProgressMonitor(pm, 1));
genText = formatCode(genText);
if (!genText.equals(oldContents)) {
workingCopy.getBuffer().setContents(genText);
workingCopy.reconcile(ICompilationUnit.NO_AST, false, null, null);
workingCopy.commitWorkingCopy(true, new SubProgressMonitor(pm, 1));
} else {
// discard changes - would happen in finally, nothing else to do
pm.worked(1);
}
} finally {
workingCopy.discardWorkingCopy();
}
} else {
cu = pf.createCompilationUnit(cu.getElementName(), genText, true, new SubProgressMonitor(pm, 1));
getImportsPostrocessor().organizeImports(cu, null, new SubProgressMonitor(pm, 1));
String newContents = formatCode(cu.getSource());
cu.getBuffer().setContents(newContents);
cu.save(new SubProgressMonitor(pm, 2), true);
}
} catch (NullPointerException ex) {
handleException(ex);
} catch (InvocationTargetException ex) {
handleException(ex.getCause());
} catch (UnexpectedBehaviourException ex) {
handleUnexpected(ex);
} catch (CoreException ex) {
handleException(ex);
} finally {
pm.done();
}
}
protected final void doGenerateBinaryFile(BinaryEmitter emitter, Path outputPath, Object[] params) throws InterruptedException, UnexpectedBehaviourException {
IProgressMonitor pm = getNextStepMonitor();
setProgressTaskName(outputPath.lastSegment());
IFile f = getDestProject().getFile(outputPath);
if (f.exists()) {
// Follow EMF's policy and do not overwrite file if exists
return;
}
try {
pm.beginTask(null, 4);
IPath containerPath = getDestProject().getFullPath().append(outputPath.removeLastSegments(1));
EclipseUtil.findOrCreateContainer(containerPath, false, (IPath) null, new SubProgressMonitor(pm, 1));
byte[] contents = emitter.generate(new SubProgressMonitor(pm, 1), params);
f.create(new ByteArrayInputStream(contents), true, new SubProgressMonitor(pm, 1));
f.getParent().refreshLocal(IResource.DEPTH_ONE, new SubProgressMonitor(pm, 1));
} catch (InvocationTargetException ex) {
handleException(ex.getCause());
} catch (CoreException ex) {
handleException(ex);
} finally {
pm.done();
}
}
protected String mergeJavaCode(String oldContents, String generatedText, IProgressMonitor pm) throws JavaModelException {
pm.beginTask(Messages.merge, 1);
try {
return getMergeService().mergeJava(oldContents, generatedText);
} finally {
pm.done();
}
}
protected String mergePlainText(String oldText, String genText, IFile oldRes, IProgressMonitor pm) {
pm.beginTask(Messages.merge, 1);
try {
return getMergeService().process(oldRes.getName(), oldText, genText);
} finally {
pm.done();
}
}
private TextMerger getMergeService() {
if (myMerger == null) {
myMerger = createMergeService();
assert myMerger != null;
}
return myMerger;
}
/**
* By default, provides facility that doesn't perform any merge at all.
* @return facility to perform merges, should never return null.
*/
protected TextMerger createMergeService() {
return new TextMerger();
}
protected void setProgressTaskName(String text) {
myProgress.subTask(text);
}
protected final String formatCode(String text) {
IDocument doc = new Document(text);
TextEdit edit = getCodeFormatter().format(CodeFormatter.K_COMPILATION_UNIT, doc.get(), 0, doc.get().length(), 0, null);
try {
// check if text formatted successfully
if (edit != null) {
edit.apply(doc);
text = doc.get();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return text;
}
private void doRun() throws InterruptedException {
try {
setupProgressMonitor();
ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
public void run(IProgressMonitor monitor) throws CoreException {
try {
customRun();
myRunStatus = getExceptionsStatus();
// XXX consider catching CCE and provide "programming error"
// to help users with their templates
} catch (NullPointerException ex) {
myRunStatus = new Status(IStatus.ERROR, Activator.getID(), 0, NullPointerException.class.getName(), ex);
} catch (UnexpectedBehaviourException ex) {
myRunStatus = new Status(Status.ERROR, Activator.getID(), 0, Messages.unexpected, ex);
} catch (InterruptedException ex) {
myRunStatus = new Status(IStatus.CANCEL, Activator.getID(), 0, Messages.interrupted, ex);
}
}
}, null);
if (myRunStatus.getSeverity() == IStatus.CANCEL && myRunStatus.getException() instanceof InterruptedException) {
throw (InterruptedException) myRunStatus.getException();
}
} catch (CoreException ex) {
myRunStatus = ex.getStatus();
} finally {
getProgress().done();
clearExceptionsList();
}
}
private CodeFormatter getCodeFormatter() {
if (myCodeFormatter == null) {
myCodeFormatter = myCodeFormatterFactory.createCodeFormatter();
}
return myCodeFormatter;
}
private OrganizeImportsPostprocessor getImportsPostrocessor() {
if (myImportsPostprocessor == null) {
myImportsPostprocessor = new OrganizeImportsPostprocessor(isToRestoreExistingImports);
}
return myImportsPostprocessor;
}
private final void clearExceptionsList() {
myExceptions.clear();
}
private final IStatus getExceptionsStatus() {
if (myExceptions == null || myExceptions.isEmpty()) {
return Status.OK_STATUS;
} else {
IStatus[] s = myExceptions.toArray(new IStatus[myExceptions.size()]);
return new MultiStatus(Activator.getID(), 0, s, Messages.problems, null);
}
}
public static final class Counter {
private final HashMap<EClass, Integer> myCounters = new HashMap<EClass, Integer>();
private final HashMap<EClass, Integer> myCache = new HashMap<EClass, Integer>();
private final Integer CACHE_MISS = new Integer(0);
public Counter() {
}
public void registerFactor(EClass eClass, int count) {
myCounters.put(eClass, count);
}
public int getTotal(EObject from) {
int total = process(from);
for (Iterator<EObject> it = from.eAllContents(); it.hasNext();) {
total += process(it.next());
}
return total;
}
protected int process(EObject next) {
final EClass nextKey = next.eClass();
Integer cachedValue = checkCached(nextKey);
if (cachedValue != null) {
return cachedValue;
}
LinkedList<EClass> checkQueue = new LinkedList<EClass>();
checkQueue.add(nextKey);
do {
EClass key = checkQueue.removeFirst();
if (myCounters.containsKey(key)) {
final Integer value = myCounters.get(key);
cache(nextKey, value);
return value;
} else {
// add immeditate superclasses to check first
checkQueue.addAll(key.getESuperTypes());
}
} while (!checkQueue.isEmpty());
cache(nextKey, CACHE_MISS);
return 0;
}
private Integer checkCached(EClass nextKey) {
return myCache.get(nextKey);
}
private void cache(EClass nextKey, Integer value) {
myCache.put(nextKey, value);
}
}
}