/* * 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); } } }