blob: 39ed3a14f214554d14ff6be80a1141605ed51543 [file] [log] [blame]
/**
* Copyright (c) 2006-2012 IBM 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:
* IBM - Initial API and implementation
*/
package org.eclipse.emf.codegen.ecore.generator;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourceAttributes;
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.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IBufferChangedListener;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IOpenable;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.emf.codegen.ecore.CodeGenEcorePlugin;
import org.eclipse.emf.codegen.ecore.generator.Generator.Options;
import org.eclipse.emf.codegen.ecore.genmodel.GenBase;
import org.eclipse.emf.codegen.jet.JETCompiler;
import org.eclipse.emf.codegen.jet.JETEmitter;
import org.eclipse.emf.codegen.jet.JETException;
import org.eclipse.emf.codegen.merge.java.JControlModel;
import org.eclipse.emf.codegen.merge.java.JMerger;
import org.eclipse.emf.codegen.merge.java.facade.JCompilationUnit;
import org.eclipse.emf.codegen.merge.java.facade.JImport;
import org.eclipse.emf.codegen.merge.java.facade.JNode;
import org.eclipse.emf.codegen.merge.properties.PropertyMerger;
import org.eclipse.emf.codegen.util.CodeGenUtil;
import org.eclipse.emf.codegen.util.GIFEmitter;
import org.eclipse.emf.codegen.util.ImportManager;
import org.eclipse.emf.common.EMFPlugin;
import org.eclipse.emf.common.notify.impl.SingletonAdapterImpl;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.BasicMonitor;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.resource.impl.ContentHandlerImpl;
import org.eclipse.emf.ecore.resource.impl.ExtensibleURIConverterImpl;
import org.eclipse.emf.ecore.resource.impl.PlatformResourceURIHandlerImpl;
import org.eclipse.emf.ecore.xml.type.XMLTypeFactory;
import org.osgi.framework.BundleException;
/**
* A base <code>GeneratorAdapter</code> implementation. This base provides support for
* {@link org.eclipse.emf.codegen.jet.JETEmitter JET-based} generation of Java and other text artifacts, as well as
* {@link org.eclipse.emf.codegen.util.GIFEmitter GIFEmitter-based} colourization of icons, via the following methods:
* <ul>
* <li>{@link #generateJava(String, String, String, JETEmitter, Object[], Monitor)} for generating Java code, with
* {@link org.eclipse.emf.codegen.util.ImportManager import management},
* {@link org.eclipse.emf.codegen.merge.java.JMerger merging}, and
* {@link org.eclipse.jdt.core.formatter.CodeFormatter code formatting} capabilities.
* <li>{@link #generateProperties(String, JETEmitter, Object[], Monitor)} for generating property files, with
* {@link org.eclipse.emf.codegen.merge.properties.PropertyMerger merging} capability.
* <li>{@link #generateText(String, JETEmitter, Object[], boolean, String, Monitor)} for generating other text
* artifacts.
* <li>{@link #generateGIF(String, GIFEmitter, String, String, boolean, Monitor) generateGIF()} for generating icons by
* colourizing grey-scale images.
* </ul>
*
* <p>Code generation is supported in the Eclipse IDE or stand-alone. However, merging and formatting of Java code are
* not available in the latter scenario.
*
* <p>At a minimum, subclasses must implement {@link #canGenerate(Object, Object)}, to determine whether code can be
* generated for an object, and {@link #doGenerate(Object, Object, Monitor) doGenerate()}, to actually generate code
* for it.
*
* <p>They may also override {@link #getCanGenerateChildren(Object, Object)},
* {@link #getCanGenerateParent(Object, Object)}, {@link #getGenerateChildren(Object, Object)}, and
* {@link #getGenerateParent(Object, Object)}, to involve more objects these operations, as well as
* {@link #doPreGenerate(Object, Object)} and {@link #doPostGenerate(Object, Object)}, to perform setup and cleanup
* before and after code generation.
*
* <p>This class is designed to support singleton generator adapters, where a single adapter instance can be attached
* to multiple objects of the same type. State relevant to a single object is only cached during a call to
* {@link #preGenerate(Object, Object)}, {@link #generate(Object, Object, Monitor)}, or
* {@link #postGenerate(Object, Object)}.
*
* @since 2.2.0
*/
public abstract class AbstractGeneratorAdapter extends SingletonAdapterImpl implements GeneratorAdapter
{
protected final static String MANIFEST_ENCODING = "UTF-8";
protected final static String PROPERTIES_ENCODING = "ISO-8859-1";
protected GeneratorAdapterFactory adapterFactory;
/**
* The object for which this adapter is currently being used to generate code. This will only be set during
* {@link #preGenerate(Object, Object)}, {@link #generate(Object, Object, Monitor)}, and
* {@link #postGenerate(Object, Object)}.
*/
protected Object generatingObject;
/**
* The message describing what is being done. This can be set during {@link #doGenerate(Object, Object, Monitor)}
* and will be used in the diagnostic message if an exception is caught.
*/
protected String message;
/**
* All the <code>JETEmitter</code>s used by this adapter. This are cached so that they can be reused at least for
* different objects with the same adapter. When {@link Generator.Options#dynamicTemplates dynamic templateS} are
* not being used, they can actually be reused for multiple code generation invocations.
*/
protected JETEmitter[] jetEmitters;
/**
* All the <code>GIFEmitter</code>s used by this adapter. This are cached so that they can be reused at least for
* different objects with the same adapter. When {@link Generator.Options#dynamicTemplates dynamic templateS} are
* not being used, they can actually be reused for multiple code generation invocations.
*/
protected GIFEmitter[] gifEmitters;
/**
* The <code>ImportManager</code> currently being used in generating Java code. This should only be created and
* cleared via {@link #createImportManager(String, String)} and {@link #clearImportManager()}, so that subclasses may
* respond to such changes.
*/
protected ImportManager importManager;
/**
* The line delimiter currently being used in generating textual results. This should only be set
* via {@link #setLineDelimiter(String)} so that subclasses may respond to such changes.
* @since 2.3
*/
protected String lineDelimiter;
/**
* An appropriate <code>URIConverter</code> for use during code generation. This is usually applicable to the whole
* set of objects for which code is being generated, so it can be cached long-term.
*/
protected URIConverter uriConverter;
/**
* If this default constructor is used, the {@link #setAdapterFactory(GeneratorAdapterFactory) setAdapterFactory()}
* method must be called immediately to set the generator adapter factory that created this adapter.
*/
public AbstractGeneratorAdapter()
{
super();
}
public AbstractGeneratorAdapter(GeneratorAdapterFactory adapterFactory)
{
this.adapterFactory = adapterFactory;
}
public GeneratorAdapterFactory getAdapterFactory()
{
return adapterFactory;
}
public void setAdapterFactory(GeneratorAdapterFactory adapterFactory)
{
this.adapterFactory = adapterFactory;
}
/**
* Returns <code>true</code> when the type is this adapter's {@link #getAdapterFactory() factory}.
* This allows generator adapters from different factories to be attached to the same objects.
*/
@Override
public boolean isAdapterForType(Object type)
{
return type == adapterFactory;
}
/**
* Returns an empty collection, indicating that by default no children are involved in determining whether code can be
* generated for an object.
*/
public Collection<?> getCanGenerateChildren(Object object, Object projectType)
{
return Collections.EMPTY_LIST;
}
/**
* Returns null, indicating that by default no parent is involved in determining whether code can be generated for an
* object.
*/
public Object getCanGenerateParent(Object object, Object projectType)
{
return null;
}
public abstract boolean canGenerate(Object object, Object projectType);
/**
* Returns an empty collection, indicating that by default there are no children of an object for which code should
* be generated.
*/
public Collection<?> getGenerateChildren(Object object, Object projectType)
{
return Collections.EMPTY_LIST;
}
/**
* Returns null, indicating that by default there is no parent of an object for which could should be generated.
*/
public Object getGenerateParent(Object object, Object projectType)
{
return null;
}
/**
* Caches the object as {@link #generatingObject}, calls {@link #doPreGenerate(Object, Object)}, and clears it again.
* If {@link Generator.Options#dynamicTemplates dynamic templates} are enabled on the generator, any cached
* {@link #jetEmitters JETEmitter}s and {@link #gifEmitters GIFEmitter}s are also removed, so that templates
* will be recompiled during {@link #generate(Object, Object, Monitor)}.
*/
public final Diagnostic preGenerate(Object object, Object projectType)
{
try
{
generatingObject = object;
if (getGenerator().getOptions().dynamicTemplates)
{
jetEmitters = null;
gifEmitters = null;
}
return doPreGenerate(object, projectType);
}
finally
{
generatingObject = null;
}
}
/**
* Does nothing and returns {@link org.eclipse.emf.common.util.Diagnostic#OK_INSTANCE OK}. Override this to perform
* setup for code generation.
*/
protected Diagnostic doPreGenerate(Object object, Object projectType)
{
return Diagnostic.OK_INSTANCE;
}
/**
* If code can be generated for the object, as determined by {@link #canGenerate(Object, Object)}, delegates code
* generation to {@link #doGenerate(Object, Object, Monitor)}. Otherwise, simply returns
* {@link Diagnostic#OK_INSTANCE OK}. The object is cached as {@link #generatingObject} and the {@link #message} is
* cleared before calling {@link #doGenerate(Object, Object, Monitor)}; both are cleared again afterwards.
*
* @see #canGenerate(Object, Object)
* @see #doGenerate(Object, Object, Monitor)
*/
public final Diagnostic generate(Object object, Object projectType, Monitor monitor)
{
try
{
if (canGenerate(object, projectType))
{
generatingObject = object;
message = null;
return doGenerate(object, projectType, monitor);
}
return Diagnostic.OK_INSTANCE;
}
catch (Exception exception)
{
return toDiagnostic(exception, message);
}
finally
{
generatingObject = null;
message = null;
monitor.done();
}
}
/**
* Implement this to perform code generation of the given project type for the specified object. Use the monitor
* to update progress for this long-running operation. Any exceptions thrown will be converted into a diagnostic
* and returned by {@link #generate(Object, Object, Monitor) generate()}. If a {@link #message} is set, it will
* be used in this diagnostic.
*/
protected abstract Diagnostic doGenerate(Object object, Object projectType, Monitor monitor) throws Exception;
/**
* Caches the object as {@link #generatingObject}, calls {@link #doPostGenerate(Object, Object)}, and clears it again.
* If {@link Generator.Options#dynamicTemplates dynamic templates} are enabled on the generator, any cached
* {@link #jetEmitters JETEmitter} and {@link #gifEmitters GIFEmitters} are also removed, so that templates will be
* recompiled during the next {@link #generate(Object, Object, Monitor)}.
*/
public final Diagnostic postGenerate(Object object, Object projectType)
{
try
{
generatingObject = object;
if (getGenerator().getOptions().dynamicTemplates)
{
jetEmitters = null;
gifEmitters = null;
}
return doPostGenerate(object, projectType);
}
finally
{
generatingObject = null;
}
}
/**
* Does nothing and returns {@link org.eclipse.emf.common.util.Diagnostic#OK_INSTANCE OK}. Override this to perform
* cleanup from code generation.
*/
protected Diagnostic doPostGenerate(Object object, Object projectType)
{
return Diagnostic.OK_INSTANCE;
}
/**
* Converts the given exception to a <code>Diagnostic</code>. The <code>currentMessage</code>, if non-null, should
* describe what was being done when the exception occurred, and will be used in forming the diagnostic's message.
*/
protected Diagnostic toDiagnostic(Exception exception, String currentMessage)
{
if (!(exception instanceof OperationCanceledException))
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
currentMessage = currentMessage != null ?
CodeGenEcorePlugin.INSTANCE.getString("_UI_GenerateException_diagnostic", new Object[] { currentMessage }) :
CodeGenEcorePlugin.INSTANCE.getString("_UI_GenericGenerateException_diagnostic");
BasicDiagnostic diagnostic = new BasicDiagnostic(CodeGenEcorePlugin.ID, 0, currentMessage, null);
diagnostic.add(BasicDiagnostic.toDiagnostic(exception));
return diagnostic;
}
/**
* Returns the generator for this adapter's factory.
*/
protected Generator getGenerator()
{
return getAdapterFactory().getGenerator();
}
/**
* The information required to construct and initialize a {@link org.eclipse.emf.codegen.jet.JETEmitter JETEmitter},
* namely the file name of the template (relative to the template path) and the qualified name of the template class.
*/
protected static class JETEmitterDescriptor
{
public String templatePathName;
public String className;
public JETEmitterDescriptor(String templatePathName, String className)
{
this.templatePathName = templatePathName;
this.className = className;
}
}
/**
* Returns the <code>JETEmitter</code> for the <code>JETEmitterDescriptor</code> at the index specified by
* <code>id</code> in the given array. If the <code>JETEmitter</code> has not yet been created, it will be created,
* initialized, and cached at the same index in {@link #jetEmitters}.
*
* @param jetEmitterDescriptors an array of descriptors for all of the <code>JETEmitter</code>s used by this
* generator adapter.
* @param id the identifier for the desired <code>JETEmitter</code>, also the index of the descriptor in the array.
*/
protected JETEmitter getJETEmitter(JETEmitterDescriptor[] jetEmitterDescriptors, int id)
{
if (jetEmitters == null)
{
jetEmitters = new JETEmitter[jetEmitterDescriptors.length];
}
JETEmitter jetEmitter = jetEmitters[id];
if (jetEmitter == null)
{
jetEmitter = createJETEmitter(jetEmitterDescriptors[id]);
jetEmitters[id] = jetEmitter;
}
return jetEmitter;
}
/**
* Creates and initializes a <code>JETEmitter</code> according to the given descriptor.
*/
protected JETEmitter createJETEmitter(JETEmitterDescriptor jetEmitterDescriptor)
{
JETEmitter jetEmitter = new JETEmitter(getTemplatePath(), jetEmitterDescriptor.templatePathName, getClass().getClassLoader());
try
{
setStaticTemplateClass(jetEmitter, jetEmitterDescriptor.className);
addClasspathEntries(jetEmitter);
}
catch (JETException exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
return jetEmitter;
}
/*
* Returns the dynamic template path, an ordered list of URIs corresponding to locations under which to find
* templates.
*/
private String[] getTemplatePath()
{
// Consult the generator option for backwards compatibility.
//
@SuppressWarnings("deprecation")
String[] legacyPath = getGenerator().getOptions().templatePath;
if (legacyPath != null)
{
return legacyPath;
}
List<String> result = new ArrayList<String>(getUserTemplatePath());
result.addAll(getBaseTemplatePath());
return result.toArray(new String[result.size()]);
}
/**
* Returns the user-specified portion of the dynamic template path, an ordered list of URIs corresponding to locations
* under which to find templates. This implementation returns an empty list.
*
* <p>This method is only consulted if the generator's {@link Generator.Options#templatePath templatePath} option is
* set to null.
*
* @see Generator.Options#templatePath
* @see org.eclipse.emf.codegen.jet.JETEmitter#JETEmitter(String[], String)
* @see org.eclipse.emf.codegen.jet.JETCompiler#find(String[], String)
* @since org.eclipse.emf.codegen.ecore 2.2.2
*/
protected List<String> getUserTemplatePath()
{
return Collections.emptyList();
}
/*
* Returns the base portion of the dynamic template path.
*/
private List<String> getBaseTemplatePath()
{
List<String> result = new ArrayList<String>();
addBaseTemplatePathEntries(result);
return result;
}
/**
* Adds template locations to the base portion of the dynamic template path, an ordered list of URIs corresponding to
* locations under which to find templates. Order matters, so the pattern is to add local entries first, and then
* invoke the superclass implementation. This implementation does nothing.
*
* <p>This method is only consulted if the generator's {@link Generator.Options#templatePath templatePath} option is
* set to null.
*
* @see Generator.Options#templatePath
* @see org.eclipse.emf.codegen.jet.JETEmitter#JETEmitter(String[], String)
* @see org.eclipse.emf.codegen.jet.JETCompiler#find(String[], String)
* @since org.eclipse.emf.codegen.ecore 2.2.2
*/
protected void addBaseTemplatePathEntries(List<String> templatePath)
{
// Subclasses may override
}
protected static final Class<?>[] OBJECT_ARGUMENT = new Class[] { Object.class };
/**
* If {@link Generator.Options#dynamicTemplates dynamic templates} are not being used,
* attempts to set the emitter to use an existing, precompiled template class
* that has a method with signature <code>generate(Object)</code>.
* @see #setStaticTemplateClass(JETEmitter, String, String, Class[])
*/
protected void setStaticTemplateClass(JETEmitter jetEmitter, String className)
{
setStaticTemplateClass(jetEmitter, className, "generate", OBJECT_ARGUMENT);
}
/**
* If {@link Generator.Options#dynamicTemplates dynamic templates} are not being used,
* attempts to set the emitter to use an existing, precompiled template class
* that has the given method name and argument types.
* @since 2.5
*/
protected void setStaticTemplateClass(JETEmitter jetEmitter, String className, String methodName, Class<?>[] arguments)
{
if (!getGenerator().getOptions().dynamicTemplates)
{
try
{
Class<?> templateClass = getClass().getClassLoader().loadClass(className);
Method emitterMethod = templateClass.getDeclaredMethod(methodName, arguments);
jetEmitter.setMethod(emitterMethod);
}
catch (Exception exception)
{
// It's okay for there not be a precompiled template, so fail quietly.
}
}
}
/**
* Override this to {@link org.eclipse.emf.codegen.jet.JETEmitter#addVariable(String, String) add classpath variables}
* to the JETEmitter. These will be used to build and execute dynamic templates.
*
* @see org.eclipse.emf.codegen.jet.JETEmitter#addVariable(String, String)
*/
protected void addClasspathEntries(JETEmitter jetEmitter) throws JETException
{
if (getGenerator().getOptions().templateClasspath!= null)
{
for (String additionalClasspathEntry : getGenerator().getOptions().templateClasspath)
{
int index = additionalClasspathEntry.indexOf('=');
if (index == -1)
{
jetEmitter.addVariable(additionalClasspathEntry, additionalClasspathEntry);
}
else
{
jetEmitter.addVariable(additionalClasspathEntry.substring(0, index), additionalClasspathEntry.substring(index + 1));
}
}
}
}
/**
* Returns the <code>GIFEmitter</code> for the input path name at the index specified by <code>id</code> in the given
* array. If the <code>GIFEmitter</code> has not yet been created, it will be created and cached at the same index in
* {@link #gifEmitters}.
*
* @param inputPathNames an array of input path names for all the <code>GIFEmitter</code>s used by this generator
* adapter. These are the file names, relative to the template path, of grey-scale images to be colourized.
* @param id the identifier for the desired <code>GIFEmitter</code>, also the index of the input path name in the
* array.
*/
protected GIFEmitter getGIFEmitter(String[] inputPathNames, int id)
{
if (gifEmitters == null)
{
gifEmitters = new GIFEmitter[inputPathNames.length];
}
GIFEmitter gifEmitter = gifEmitters[id];
if (gifEmitter == null)
{
gifEmitter = createGIFEmitter(inputPathNames[id]);
gifEmitters[id] = gifEmitter;
}
return gifEmitter;
}
/**
* Creates a <code>GIFEmitter</code> based on the image at the give template-path-relative file name.
*/
protected GIFEmitter createGIFEmitter(String inputPathName)
{
return new GIFEmitter(JETCompiler.find(getTemplatePath(), inputPathName));
}
/**
* Generates an arbitrary text artifact using JET.
*
* @param targetPathName the path name of the target file. This should be a workspace path; when running stand-alone,
* it will be converted to a platform resource URI that should be mapped to a physical file URI by the
* {@link #getURIConverter() URIConverter}.
* @param jetEmitter the <code>JETEmitter</code> to use for generating the text.
* @param arguments the argument array to pass to the <code>JETEmitter</code>'s
* {@link JETEmitter#generate(Monitor, Object[]) generate(Monitor, Object[])} method. If null, an array will
* be constructed containing only the {@link #generatingObject object} for which code is being generated.
* @param overwrite whether an existing file should be overwritten.
* @param encoding an override of the default encoding. If "ISO-8859-1" is specified,
* {@link org.eclipse.emf.codegen.util.CodeGenUtil#unicodeEscapeEncode(String) Unicode escape encoding}
* is performed to represent non-Latin characters. The default encoding, when running under Eclipse, is
* determined from the workspace. Failing that, or in stand-alone, the platform default is used.
* @param monitor the <code>Monitor</code> through which to report progress.
*
* <p>This method also consults the following {@link Generator#getOptions() generator options}:
* <ul>
* <li>{@link Generator.Options#redirectionPattern redirectionPattern}
* <li>{@link Generator.Options#forceOverwrite forceOverwrite}
* <li>{@link Generator.Options#dynamicTemplates dynamicTemplates}
* <li>{@link Generator.Options#resourceSet resourceSet}
* </ul>
*/
protected void generateText
(String targetPathName,
JETEmitter jetEmitter,
Object[] arguments,
boolean overwrite,
String encoding,
Monitor monitor)
{
try
{
monitor.beginTask("", 3);
URI targetFile = toURI(targetPathName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_GeneratingFile_message", new Object[] { targetFile }));
URI targetDirectory = targetFile.trimSegments(1);
ensureContainerExists(targetDirectory, createMonitor(monitor, 1));
boolean exists = exists(targetFile);
if (!exists || overwrite)
{
if (arguments == null)
{
arguments = new Object[] { generatingObject };
}
setLineDelimiter(getLineDelimiter(targetFile, encoding));
String emitterResult = jetEmitter.generate(createMonitor(monitor, 1), arguments, getLineDelimiter());
if (PROPERTIES_ENCODING.equals(encoding))
{
emitterResult = CodeGenUtil.unicodeEscapeEncode(emitterResult);
}
if (encoding == null)
{
encoding = getEncoding(targetFile);
}
boolean changed = true;
if (exists)
{
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_ExaminingOld_message", new Object[] { targetFile }));
String oldContents = getContents(targetFile, encoding);
changed = !emitterResult.equals(oldContents);
if (changed)
{
if (targetPathName.endsWith("/plugin.xml"))
{
emitterResult = mergePluginXML(generatingObject instanceof GenBase ? ((GenBase)generatingObject).getGenModel().getPluginKey() : "", oldContents, emitterResult);
changed = !emitterResult.equals(oldContents);
}
else if (targetPathName.endsWith("/MANIFEST.MF"))
{
emitterResult = mergeManifest(oldContents, emitterResult);
changed = !emitterResult.equals(oldContents);
}
}
}
if (changed)
{
byte[] bytes = encoding == null ? emitterResult.toString().getBytes() : emitterResult.toString().getBytes(encoding);
// Apply a redirection pattern, if specified.
//
String redirection = getGenerator().getOptions().redirectionPattern;
boolean redirect = redirection != null && redirection.indexOf("{0}") != -1;
if (redirect)
{
String baseName = MessageFormat.format(redirection, new Object[] { targetFile.lastSegment() });
targetFile = targetDirectory.appendSegment(baseName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingAlternate_message", new Object[] { targetFile }));
}
if (isReadOnly(targetFile))
{
if (getGenerator().getOptions().forceOverwrite)
{
// If the target is read-only, we can ask the platform to release it.
//
validateEdit(targetFile, createMonitor(monitor, 1));
setWriteable(targetFile);
}
else
{
targetFile = targetDirectory.appendSegment("." + targetFile.lastSegment() + ".new");
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingDefaultAlternate_message", new Object[] { targetFile }));
}
}
OutputStream outputStream = createOutputStream(targetFile);
outputStream.write(bytes);
outputStream.close();
}
}
}
catch (Exception exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
finally
{
setLineDelimiter(null);
monitor.done();
}
}
/**
* Information about extensions in a plugin.xml.
* @since 2.9
*/
protected static final class ExtensionData
{
public String extensionPointID;
public String identifier;
public String generated;
public int start;
public int end;
public String content;
public String lineSeparator;
@Override
public boolean equals(Object object)
{
if (object == this)
{
return true;
}
else if (object instanceof ExtensionData)
{
ExtensionData extension = (ExtensionData)object;
return extension.extensionPointID.equals(extensionPointID) && (extension.identifier == null ? identifier == null : extension.identifier.equals(identifier));
}
else
{
return false;
}
}
@Override
public int hashCode()
{
return extensionPointID.hashCode() ^ (identifier == null ? 0 : identifier.hashCode());
}
@Override
public String toString()
{
return extensionPointID + ":" + identifier + ":" + generated;
}
}
/**
* A pattern for matching extensions.
* @since 2.9
*/
protected static final Pattern EXTENSION_POINT_PATTERN = Pattern.compile("[ \t]*<extension[^>]+point\\s*=['\"]([^'\"]+)['\"].*?(?<!<category\\s{1,40})(?:id|class)\\s*=\\s*['\"]([^'\"]+)['\"].*?</extension>[ \t]*(\n\r|\r\n|\n|\r)", Pattern.DOTALL);
/**
* A pattern for matching the <code>@generated</code> comment in an extension.
* @since 2.9
*/
protected static final Pattern GENERATED_PATTERN = Pattern.compile("<!-- *@generated *([^ ]+) *-->");
/**
* A pattern for matching the closing tag of a plugin.xml.
* @since 2.9
*/
protected static final Pattern PLUGIN_END_TAG_PATTERN = Pattern.compile(".*(\n\r|\r\n|\n|\r)?[ \t]*(</plugin>)");
/**
* A pattern for matching trailing blank lines.
* @since 2.9
*/
protected static final Pattern BLANK_LINES_PATTERN = Pattern.compile("([ \t]*(\n\r|\r\n|\n|\r))+");
/**
* Collect information about extensions in a plugin.xml's contents.
* @since 2.9
*/
protected List<ExtensionData> getExtensionData(String contents)
{
// Collect information about extensions by finding all matches of the pattern.
//
List<ExtensionData> extensions = new ArrayList<AbstractGeneratorAdapter.ExtensionData>();
Matcher matcher = EXTENSION_POINT_PATTERN.matcher(contents);
while (matcher.find())
{
ExtensionData extension = new ExtensionData();
extension.extensionPointID = matcher.group(1);
extension.identifier = matcher.group(2);
extension.start = matcher.start();
extension.end = matcher.end();
extension.content = matcher.group();
extension.lineSeparator = matcher.group(3);
extensions.add(extension);
// Look for the @generated comment.
//
Matcher generatedMatcher = GENERATED_PATTERN.matcher(extension.content);
if (generatedMatcher.find())
{
extension.generated = generatedMatcher.group(1);
}
}
return extensions;
}
/**
* Merge the contents of two plugin.xmls.
* @since 2.9
*/
protected String mergePluginXML(String generated, String oldContents, String newContents)
{
// Fetch the information about the extensions.
//
List<ExtensionData> oldExtensions = getExtensionData(oldContents);
List<ExtensionData> newExtensions = getExtensionData(newContents);
// Find the index of the closing tag and remember whether it was preceded by a line separator.
//
int end = -1;
Matcher matcher = PLUGIN_END_TAG_PATTERN.matcher(oldContents);
boolean isEmpty = false;
if (matcher.find())
{
isEmpty = matcher.group(1) == null;
end = matcher.start(2);
}
// Pull in the new extension content.
//
for (int i = 0, size = oldExtensions.size(); i < size; ++i)
{
// Determine the matching new extension.
//
ExtensionData oldExtension = oldExtensions.get(i);
int index = newExtensions.indexOf(oldExtension);
if (index == -1)
{
// If there is no match and this extension was generated...
//
if (generated.equals(oldExtension.generated))
{
// Set the old extension up to be swept, including trailing blank lines for removal.
//
oldExtension.content = "";
Matcher trailingMatching = BLANK_LINES_PATTERN.matcher(oldContents);
if (trailingMatching.find(oldExtension.end) && oldExtension.end == trailingMatching.start())
{
oldExtension.end = trailingMatching.end();
}
}
}
else
{
// If the new match has the same non-null generation key.
//
ExtensionData newExtension = newExtensions.get(index);
if (oldExtension.generated != null && oldExtension.generated.equals(newExtension.generated))
{
// Set up the old extension up to pull the new content.
//
oldExtension.content = newExtension.content;
}
// Set up the new extension to block it being pushed.
//
newExtension.content = null;
}
}
// Push in the new extension content, except for the ones blocked during pull analysis.
//
LOOP:
for (int i = 0, size = newExtensions.size(); i < size; ++i)
{
ExtensionData newExtension = newExtensions.get(i);
// If the new extension isn't blocked and it's marked for merging...
//
if (newExtension.content != null && newExtension.generated != null)
{
// To find an insertion point, look backward in the new extensions for a matching one in the old extensions after which to insert the new one.
//
for (int j = i - 1; j >= 0; --j)
{
int index = oldExtensions.indexOf(newExtensions.get(j));
if (index != -1)
{
ExtensionData oldExtension = oldExtensions.get(index);
oldExtensions.add(index + 1, newExtension);
newExtension.content = oldExtension.lineSeparator + newExtension.content;
newExtension.start = newExtension.end = oldExtension.end;
continue LOOP;
}
}
// Failing that, look forward in the new extensions for a matching one in the old extensions before which to insert the new one.
//
for (int j = i + 1; j < size; ++j)
{
int index = oldExtensions.indexOf(newExtensions.get(j));
if (index != -1)
{
ExtensionData oldExtension = oldExtensions.get(index);
oldExtensions.add(index, newExtension);
newExtension.content = newExtension.content + newExtension.lineSeparator;
newExtension.start = newExtension.end = oldExtension.start;
continue LOOP;
}
}
// Failing both those, insert it before the closing tag.
//
oldExtensions.add(newExtension);
newExtension.content = (isEmpty ? newExtension.lineSeparator : "") + newExtension.content + newExtension.lineSeparator;
newExtension.lineSeparator = "";
newExtension.start = newExtension.end = end;
}
}
// Build up the result...
//
StringBuilder result = new StringBuilder();
int index = 0;
for (int i = 0, size = oldExtensions.size(); i < size; ++i)
{
ExtensionData oldExtension = oldExtensions.get(i);
result.append(oldContents.substring(index, oldExtension.start));
result.append(oldExtension.content);
index = oldExtension.end;
}
result.append(oldContents.substring(index));
return result.toString();
}
/**
* Information about attributes in a manifest.
* @since 2.9
*/
protected static final class AttributeData
{
public String name;
public String value;
public String lineDelimiter;
public List<Element> elements;
public int start;
public int end;
@Override
public String toString()
{
if (elements != null)
{
StringBuilder result = new StringBuilder(name);
result.append(": ");
boolean previous = false;
for (Element element : elements)
{
if (previous)
{
result.append(',');
result.append(lineDelimiter);
result.append(' ');
}
else
{
previous = true;
}
result.append(element.toString(lineDelimiter));
}
return result.toString();
}
else
{
StringBuilder result = new StringBuilder(name);
result.append(": ");
int index = 0;
boolean previous = false;
int length = value.length();
for (int i = value.indexOf(','); i != -1; index = i + 1, i = value.indexOf(',', index))
{
if (previous)
{
result.append(',');
result.append(lineDelimiter);
result.append(' ');
}
else
{
previous = true;
}
if (i + 1 < length && Character.isDigit(value.charAt(i + 1)))
{
// The comma is immediately followed by a digit so is probably the comma in a range.
// So buffer out this substring and when we get to the next substring, don't start a new line.
//
result.append(value.substring(index, i + 1));
previous = false;
}
else
{
result.append(value.substring(index, i));
}
}
if (previous)
{
result.append(',');
result.append(lineDelimiter);
result.append(' ');
}
result.append(value.substring(index));
return result.toString();
}
}
@Override
public boolean equals(Object object)
{
return object instanceof AttributeData && name.equals(((AttributeData)object).name);
}
@Override
public int hashCode()
{
return name.hashCode();
}
protected final static class Element
{
public Set<String> valueComponents = new LinkedHashSet<String>();
public List<Directive> directives = new ArrayList<Directive>();
public List<Attribute> attributes = new ArrayList<Attribute>();
@Override
public boolean equals(Object object)
{
return object instanceof Element && valueComponents.equals(((Element)object).valueComponents);
}
@Override
public int hashCode()
{
return valueComponents.hashCode();
}
public String toString(String lineDelimiter)
{
StringBuilder result = new StringBuilder();
boolean previous = false;
for (String valueComponent : valueComponents)
{
if (previous)
{
result.append(';');
}
else
{
previous = true;
}
result.append(valueComponent);
}
for (Attribute attribute : attributes)
{
result.append(';');
result.append(attribute.toString(lineDelimiter));
}
for (Directive directive : directives)
{
result.append(';');
result.append(directive.toString(lineDelimiter));
}
return result.toString();
}
@Override
public String toString()
{
return toString("");
}
protected static final class Directive
{
public String key;
public String value;
public String toString(String lineDelimiter)
{
if ("x-friends".equals(key) || "uses".equals(key))
{
boolean hasLineDelimiter = lineDelimiter.length() != 0;
StringBuilder result = new StringBuilder();
if (hasLineDelimiter)
{
result.append(lineDelimiter);
result.append(" ");
}
result.append(key);
result.append(":=\"");
List<String> friends = XMLTypeFactory.eINSTANCE.createIDREFS(value.replace(',', ' '));
int size = friends.size();
if (size > 0)
{
result.append(friends.get(0));
for (int i = 1; i < size; ++i)
{
result.append(',');
if (lineDelimiter.length() != 0)
{
result.append(lineDelimiter);
result.append(" ");
}
result.append(friends.get(i));
}
}
result.append('"');
return result.toString();
}
else
{
return key + ":=" + quote(value);
}
}
@Override
public String toString()
{
return toString("");
}
@Override
public boolean equals(Object object)
{
return object instanceof Directive && key.equals(((Directive)object).key);
}
@Override
public int hashCode()
{
return key.hashCode();
}
}
protected final static class Attribute
{
public String key;
public String value;
public String toString(String lineDelimiter)
{
return key + "=" + quote(value);
}
@Override
public String toString()
{
return toString("");
}
@Override
public boolean equals(Object object)
{
return object instanceof Attribute && key.equals(((Attribute)object).key);
}
@Override
public int hashCode()
{
return key.hashCode();
}
}
}
private static String quote(String value)
{
for (int i = 0, length = value.length(); i < length; i++)
{
char c = value.charAt(i);
if (!Character.isLetter(c) && c != '_' && c != '-' && c != '.')
{
return '"' + value + '"';
}
}
return value;
}
}
/**
* A pattern for matching a manifest's version attribute.
* @since 2.9
*/
protected static final Pattern VERSION_PATTERN = Pattern.compile("Manifest-Version: *([0-9]+(?:\\.[0-9]+)*) *(\r\n|\n\r|\n|\r)?");
/**
* A pattern for matching manifest attribute headers.
* @since 2.9
*/
protected static final Pattern HEADER_PATTERN = Pattern.compile("([A-Za-z0-9][-_A-Za-z0-9]*): *([^\n\r]*)(\r\n|\n\r|\n|\r)?");
/**
* A pattern for matching manifest attribute continuations.
* @since 2.9
*/
protected static final Pattern CONTINUATION_PATTERN = Pattern.compile(" ([^\n\r]*)(\r\n|\n\r|\n|\r)?");
/**
* Collect information about attributes in a manifest's contents.
* If the contents are well formed, there should always be at least one entry for the manifest version.
* @since 2.9
*/
protected List<AttributeData> getAttributeData(String contents)
{
List<AttributeData> result = new ArrayList<AttributeData>();
// Look for the initial manifest version attribute.
//
Matcher versionMatcher = VERSION_PATTERN.matcher(contents);
if (versionMatcher.lookingAt())
{
// Construct the data for it.
//
AttributeData versionAttribute = new AttributeData();
versionAttribute.name = "Manifest-Version";
versionAttribute.value = versionMatcher.group(1);
versionAttribute.lineDelimiter = versionMatcher.group(2);
versionAttribute.end = versionMatcher.end();
result.add(versionAttribute);
// Look each header as well as continuations for that header starting after the version attribute...
//
Matcher headerMatcher = HEADER_PATTERN.matcher(contents);
Matcher continuationMatcher = CONTINUATION_PATTERN.matcher(contents);
for (int start = versionMatcher.end();;)
{
// Find a match, checking that it matches at the expected position...
//
if (headerMatcher.find(start) && headerMatcher.start() == start)
{
// Construct data for it.
//
AttributeData attribute = new AttributeData();
attribute.name = headerMatcher.group(1);
attribute.lineDelimiter = headerMatcher.group(3);
attribute.start = headerMatcher.start();
attribute.end = headerMatcher.end();
result.add(attribute);
// Build up the value from any continuations that start after the header...
//
StringBuilder value = new StringBuilder(headerMatcher.group(2));
start = headerMatcher.end();
for (;;)
{
// Look for the continuation, checking that it matches at the expected positions.
//
if (continuationMatcher.find(start) && continuationMatcher.start() == start)
{
// Build up the value, ignoring the line delimiter and the leading space.
//
value.append(continuationMatcher.group(1));
attribute.end = continuationMatcher.end();
start = continuationMatcher.end();
}
else
{
break;
}
}
// Set the attributes value from this computation.
//
attribute.value = value.toString();
// If we're running in Eclipse, compute the structured OSGi manifest element information from the value.
//
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
attribute.elements = EclipseHelper.getElements(attribute.name, attribute.value);
}
}
else
{
// Once we don't find a header, stop processing the rest of the manifest.
//
break;
}
}
}
return result;
}
/**
* Finds the first attribute with the given name.
*/
private static AttributeData getAttribute(List<AttributeData> attributes, String name)
{
for (int i = 0, size = attributes.size(); i < size; ++i)
{
AttributeData attribute = attributes.get(i);
if (name.equals(attribute.name))
{
return attribute;
}
}
return null;
}
/**
* Merge the contents of to manifests.
* @since 2.9
*/
protected String mergeManifest(String oldContents, String newContents)
{
List<AttributeData> oldAttributes = getAttributeData(oldContents);
List<AttributeData> newAttributes = getAttributeData(newContents);
// We should merge the translated attributes if the bundle localization changes.
//
AttributeData oldBundleLocalization = getAttribute(oldAttributes, "Bundle-Localization");
AttributeData newBundleLocalization = getAttribute(newAttributes, "Bundle-Localization");
boolean mergeTranslatedAttributes = oldBundleLocalization == null || (newBundleLocalization != null && !newBundleLocalization.value.equals(oldBundleLocalization.value));
// Pull in the appropriate new attribute content.
//
for (int i = 0, size = oldAttributes.size(); i < size; ++i)
{
// Determine the matching new attribute.
//
AttributeData oldAttributeData = oldAttributes.get(i);
int index = newAttributes.indexOf(oldAttributeData);
if (index != -1)
{
AttributeData newAttributeData = newAttributes.get(index);
// Pull in the contents for these specific attributes if the are merging translated attributes...
//
if ("Bundle-Name".equals(oldAttributeData.name) || "Bundle-Vendor".equals(oldAttributeData.name))
{
// If we should merge translated attributes...
//
if (mergeTranslatedAttributes)
{
if (oldAttributeData.elements == null)
{
oldAttributeData.value = newAttributeData.value;
}
else
{
oldAttributeData.elements = newAttributeData.elements;
}
}
}
// The bundle localization attribute must be what we generate because we must be able to find the properties in that generated properties file.
// Also the bundle activator class name can change, so we should use the one we generate.
//
else if ("Bundle-Localization".equals(oldAttributeData.name) || "Bundle-SymbolicName".equals(oldAttributeData.name) || "Bundle-Activator".equals(oldAttributeData.name))
{
if (oldAttributeData.elements == null)
{
oldAttributeData.value = newAttributeData.value;
}
else
{
oldAttributeData.elements = newAttributeData.elements;
}
}
// These attributes have structured content that we should merge...
//
else if ("Export-Package".equals(oldAttributeData.name) ||
"Import-Package".equals(oldAttributeData.name) ||
"Require-Bundle".equals(oldAttributeData.name) ||
"Require-Package".equals(oldAttributeData.name))
{
// If the new one has structured content...
//
if (newAttributeData.elements != null)
{
// If the old one has no structured content, just pull in the full value.
//
if (oldAttributeData.elements == null)
{
oldAttributeData.value = newAttributeData.value;
}
else
{
// Merge the elements...
//
LOOP:
for (int j = 0, elementSize = newAttributeData.elements.size(); j < elementSize; ++j)
{
AttributeData.Element element = newAttributeData.elements.get(j);
int elementIndex = oldAttributeData.elements.indexOf(element);
if (elementIndex != -1)
{
AttributeData.Element oldElement = oldAttributeData.elements.get(elementIndex);
INNER_LOOP:
for (int m = 0, attributeSize = element.attributes.size(); m < attributeSize; ++m)
{
AttributeData.Element.Attribute attribute = element.attributes.get(m);
int oldAttributeIndex = oldElement.attributes.indexOf(attribute);
if (oldAttributeIndex != -1)
{
// Transfer the attribute value;
//
AttributeData.Element.Attribute oldAttribute = oldElement.attributes.get(oldAttributeIndex);
oldAttribute.value = attribute.value;
}
else
{
// Look backward for an appropriate place after which to insert the new one.
//
for (int n = m - 1; n >= 0; --n)
{
int targetIndex = oldElement.attributes.indexOf(element.attributes.get(n));
if (targetIndex != -1)
{
oldElement.attributes.add(targetIndex + 1, attribute);
continue INNER_LOOP;
}
}
// Failing that, look forward for an appropriate place before which to insert the new one.
//
for (int n = m + 1; n < attributeSize; ++n)
{
int targetIndex = oldElement.attributes.indexOf(element.attributes.get(n));
if (targetIndex != -1)
{
oldElement.attributes.add(targetIndex, attribute);
continue INNER_LOOP;
}
}
// Failing both of those, add the end.
//
oldElement.attributes.add(attribute);
}
}
INNER_LOOP:
for (int m = 0, directiveSize = element.directives.size(); m < directiveSize; ++m)
{
AttributeData.Element.Directive directive = element.directives.get(m);
int oldAttributeIndex = oldElement.directives.indexOf(directive);
if (oldAttributeIndex != -1)
{
// Transfer the directive value;
//
AttributeData.Element.Directive oldDirective = oldElement.directives.get(oldAttributeIndex);
oldDirective.value = directive.value;
}
else
{
// Look backward for an appropriate place after which to insert the new one.
//
for (int n = m - 1; n >= 0; --n)
{
int targetIndex = oldElement.directives.indexOf(element.directives.get(n));
if (targetIndex != -1)
{
oldElement.directives.add(targetIndex + 1, directive);
continue INNER_LOOP;
}
}
// Failing that, look forward for an appropriate place before which to insert the new one.
//
for (int n = m + 1; n < directiveSize; ++n)
{
int targetIndex = oldElement.directives.indexOf(element.directives.get(n));
if (targetIndex != -1)
{
oldElement.directives.add(targetIndex, directive);
continue INNER_LOOP;
}
}
// Failing both of those, add the end.
//
oldElement.directives.add(directive);
}
}
}
else
{
// Look backward for an appropriate element after which to insert the new one.
//
for (int k = j - 1; k >= 0; --k)
{
int targetIndex = oldAttributeData.elements.indexOf(newAttributeData.elements.get(k));
if (targetIndex != -1)
{
oldAttributeData.elements.add(targetIndex + 1, element);
continue LOOP;
}
}
// Failing that, look forward for an appropriate element before which to insert the new one.
//
for (int k = j + 1; k < elementSize; ++k)
{
int targetIndex = oldAttributeData.elements.indexOf(newAttributeData.elements.get(k));
if (targetIndex != -1)
{
oldAttributeData.elements.add(targetIndex, element);
continue LOOP;
}
}
// Failing both of those, add the new element at the end.
//
oldAttributeData.elements.add(element);
}
}
}
}
else
{
// Pull in just the new value if there is no structured content.
//
// oldAttribute.value = newAttribute.value;
//
// In stand alone mode, it's safer not to overwrite the content.
}
}
// Mark the attribute so it's not pushed in later.
//
newAttributeData.value = null;
}
}
// Push in the new attribute content, except for the ones blocked during pull analysis.
//
LOOP:
for (int i = 0, size = newAttributes.size(); i < size; ++i)
{
AttributeData newAttribute = newAttributes.get(i);
// If the new attribute isn't blocked...
//
if (newAttribute.value != null)
{
// To find an insertion point, look backward in the new attributes for a matching one in the old attributes after which to insert the new one.
//
for (int j = i - 1; j >= 0; --j)
{
int index = oldAttributes.indexOf(newAttributes.get(j));
if (index != -1)
{
AttributeData oldAttribute = oldAttributes.get(index);
oldAttributes.add(index + 1, newAttribute);
newAttribute.start = newAttribute.end = oldAttribute.end;
continue LOOP;
}
}
// Failing that, look forward in the new attributes for a matching one in the old attributes before which to insert the new one.
//
for (int j = i + 1; j < size; ++j)
{
int index = oldAttributes.indexOf(newAttributes.get(j));
if (index != -1)
{
AttributeData oldAttribute = oldAttributes.get(index);
oldAttributes.add(index, newAttribute);
newAttribute.start = newAttribute.end = oldAttribute.start;
continue LOOP;
}
}
// Failing both those, insert it after the last old attribute.
//
oldAttributes.add(newAttribute);
newAttribute.start = newAttribute.end = oldAttributes.get(oldAttributes.size() - 1).end;
}
}
// Build up the result...
//
StringBuilder result = new StringBuilder();
int index = 0;
for (int i = 0, size = oldAttributes.size(); i < size; ++i)
{
AttributeData oldAttribute = oldAttributes.get(i);
result.append(oldContents.substring(index, oldAttribute.start));
result.append(oldAttribute);
result.append(oldAttribute.lineDelimiter);
index = oldAttribute.end;
}
result.append(oldContents.substring(index));
return result.toString();
}
/**
* Generates a properties file using JET, with {@link org.eclipse.emf.codegen.merge.properties.PropertyMerger merging}
* capability.
*
* <p>The encoding used for the generated file is "ISO-8859-1".
* {@link org.eclipse.emf.codegen.util.CodeGenUtil#unicodeEscapeEncode(String) Unicode escape encoding} is
* performed to represent non-Latin characters.
*
* @param targetPathName the path name of the target file. This should be a workspace path; when running stand-alone,
* it will be converted to a platform resource URI that should be mapped to a physical file URI by the
* {@link #getURIConverter() URIConverter}.
* @param jetEmitter the <code>JETEmitter</code> to use for generating the text.
* @param arguments the argument array to pass to the <code>JETEmitter</code>'s
* {@link JETEmitter#generate(Monitor, Object[]) generate(Monitor, Object[])} method. If null, an array will
* be constructed containing only the {@link #generatingObject object} for which code is being generated.
* @param monitor the <code>Monitor</code> through which to report progress.
*
* <p>This method also consults the following {@link Generator#getOptions() generator options}:
* <ul>
* <li>{@link Generator.Options#redirectionPattern redirectionPattern}
* <li>{@link Generator.Options#forceOverwrite forceOverwrite}
* <li>{@link Generator.Options#dynamicTemplates dynamicTemplates}
* <li>{@link Generator.Options#resourceSet resourceSet}
* </ul>
*/
protected void generateProperties(String targetPathName, JETEmitter jetEmitter, Object[] arguments, Monitor monitor)
{
try
{
monitor.beginTask("", 3);
URI targetFile = toURI(targetPathName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_GeneratingFile_message", new Object[] { targetFile }));
URI targetDirectory = targetFile.trimSegments(1);
ensureContainerExists(targetDirectory, createMonitor(monitor, 1));
boolean changed = false;
if (arguments == null)
{
arguments = new Object[] { generatingObject };
}
setLineDelimiter(getLineDelimiter(targetFile, PROPERTIES_ENCODING));
String emitterResult = CodeGenUtil.unicodeEscapeEncode(jetEmitter.generate(createMonitor(monitor, 1), arguments, getLineDelimiter()));
byte[] bytes = emitterResult.toString().getBytes(PROPERTIES_ENCODING);
if (shouldMerge(targetFile) && exists(targetFile))
{
// Merge with an existing file.
//
PropertyMerger propertyMerger = new PropertyMerger();
propertyMerger.setSourceProperties(emitterResult);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_ExaminingOld_message", new Object[] { targetFile }));
String oldProperties = propertyMerger.createPropertiesForInputStream(createInputStream(targetFile));
propertyMerger.setTargetProperties(oldProperties);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_PreparingNew_message", new Object[] { targetFile }));
propertyMerger.merge();
String mergedResult = propertyMerger.getTargetProperties();
changed = !mergedResult.equals(oldProperties);
if (changed)
{
// If the target is read-only, we can ask the platform to release it, and it may be updated in the process.
//
if (isReadOnly(targetFile) && validateEdit(targetFile, createMonitor(monitor, 1)))
{
propertyMerger.setTargetProperties(propertyMerger.createPropertiesForInputStream(createInputStream(targetFile)));
propertyMerger.merge();
mergedResult = propertyMerger.getTargetProperties();
}
bytes = mergedResult.getBytes(PROPERTIES_ENCODING);
}
}
else
{
changed = true;
monitor.worked(1);
}
if (changed)
{
// Apply a redirection pattern, if specified.
//
String redirection = getGenerator().getOptions().redirectionPattern;
boolean redirect = redirection != null && redirection.indexOf("{0}") != -1;
if (redirect)
{
String baseName = MessageFormat.format(redirection, new Object[] { targetFile.lastSegment() });
targetFile = targetDirectory.appendSegment(baseName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingAlternate_message", new Object[] { targetFile }));
}
if (isReadOnly(targetFile))
{
if (getGenerator().getOptions().forceOverwrite)
{
setWriteable(targetFile);
}
else
{
targetFile = targetDirectory.appendSegment("." + targetFile.lastSegment() + ".new");
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingDefaultAlternate_message", new Object[] { targetFile }));
}
}
OutputStream outputStream = createOutputStream(targetFile);
outputStream.write(bytes);
outputStream.close();
}
}
catch (Exception exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
finally
{
setLineDelimiter(null);
monitor.done();
}
}
/**
* Generates an icon using a {@link org.eclipse.emf.codegen.util.GIFEmitter GIFEmitter} to colourize a grey-scale GIF
* image. The colours to use are calculated from one or, optionally, two text keys.
*
* @param targetPathName the path name of the target file. This should be a workspace path; when running stand-alone,
* it will be converted to a platform resource URI that should be mapped to a physical file URI by the
* {@link #getURIConverter() URIConverter}.
* @param gifEmitter the <code>GIFEmitter</code> to use for generating the icon.
* @param parentKey the key used to determine the first colour set.
* @param childKey the key used to determine the second colour set. If null, this key is ignored.
* @param overwrite whether an existing file should be overwritten.
* @param monitor the <code>Monitor</code> through which to report progress.
*
* <p>This method also consults the following {@link Generator#getOptions() generator options}:
* <ul>
* <li>{@link Generator.Options#redirectionPattern redirectionPattern}
* <li>{@link Generator.Options#forceOverwrite forceOverwrite}
* <li>{@link Generator.Options#resourceSet resourceSet}
* </ul>
*/
protected void generateGIF
(String targetPathName,
GIFEmitter gifEmitter,
String parentKey,
String childKey,
boolean overwrite,
Monitor monitor)
{
try
{
monitor.beginTask("", 3);
URI targetFile = toURI(targetPathName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_GeneratingImage_message", new Object[] { targetFile }));
URI targetDirectory = targetFile.trimSegments(1);
ensureContainerExists(targetDirectory, createMonitor(monitor, 1));
boolean exists = exists(targetFile);
if (!exists || overwrite)
{
byte[] emitterResult = gifEmitter.generateGIF(parentKey, childKey);
monitor.worked(1);
// Apply a redirection pattern, if specified.
//
String redirection = getGenerator().getOptions().redirectionPattern;
boolean redirect = redirection != null && redirection.indexOf("{0}") != -1;
if (redirect)
{
String baseName = MessageFormat.format(redirection, new Object[] { targetFile.lastSegment() });
targetFile = targetDirectory.appendSegment(baseName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingAlternate_message", new Object[] { targetFile }));
}
if (isReadOnly(targetFile))
{
if (getGenerator().getOptions().forceOverwrite)
{
// If the target is read-only, we can ask the platform to release it.
//
validateEdit(targetFile, createMonitor(monitor, 1));
setWriteable(targetFile);
}
else
{
targetFile = targetDirectory.appendSegment("." + targetFile.lastSegment() + ".new");
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingDefaultAlternate_message", new Object[] { targetFile }));
}
}
OutputStream outputStream = createOutputStream(targetFile);
outputStream.write(emitterResult);
outputStream.close();
}
}
catch (Exception exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
finally
{
monitor.done();
}
}
/**
* Whether {@link #generateJava(String, String, String, JETEmitter, Object[], Monitor) generateJava) or
* {@link #generateProperties(String, JETEmitter, Object[], Monitor) generateProperties}
* should merge the newly generated contents with the existing contents
* or should simply overwrite the old contents.
* This implementation always returns true.
* @since 2.9
*/
protected boolean shouldMerge(URI workspacePath)
{
return true;
}
/**
* Generates a Java source file using JET, with {@link org.eclipse.emf.codegen.util.ImportManager import management}
* and, when running under Eclipse, {@link org.eclipse.emf.codegen.merge.java.JMerger merging} and
* {@link org.eclipse.jdt.core.formatter.CodeFormatter code formatting} capabilities.
*
* <p>When running under Eclipse, the encoding for the file is determined from the workspace. Failing that, or in
* stand-alone, the platform default is used.
*
* @param targetPath the workspace path of the directory in or under which the file will be created, depending on the
* specified package name. When running stand-alone, this path will be converted to a platform resource URI
* that should be mapped to a physical file URI by the {@link #getURIConverter() URIConverter}.
* @param packageName the package name for the generated compilation unit.
* @param className the name of the public class in the generated compilation unit.
* @param jetEmitter the <code>JETEmitter</code> to use for generating the code.
* @param arguments the argument array to pass to the <code>JETEmitter</code>'s
* {@link JETEmitter#generate(Monitor, Object[]) generate(Monitor, Object[])} method. If null, an array will
* be constructed containing only the {@link #generatingObject object} for which code is being generated.
* @param monitor the <code>Monitor</code> through which to report progress.
*
* <p>This method also consults the following {@link Generator#getOptions() generator options}:
* <ul>
*
* <li>{@link Generator.Options#redirectionPattern redirectionPattern}
* <li>{@link Generator.Options#forceOverwrite forceOverwrite}
* <li>{@link Generator.Options#dynamicTemplates dynamicTemplates}
* <li>{@link Generator.Options#mergerFacadeHelperClass mergerFacadeHelperClass}
* <li>{@link Generator.Options#mergeRulesURI mergeRulesURI}
* <li>{@link Generator.Options#codeFormatting codeFormatting}
* <li>{@link Generator.Options#codeFormatterOptions codeFormatterOptions}
* <li>{@link Generator.Options#resourceSet resourceSet}
* </ul>
*/
protected void generateJava
(String targetPath,
String packageName,
String className,
JETEmitter jetEmitter,
Object[] arguments,
Monitor monitor)
{
try
{
monitor.beginTask("", 4);
URI targetDirectory = toURI(targetPath).appendSegments(packageName.split("\\."));
URI targetFile = targetDirectory.appendSegment(className + ".java");
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_Generating_message", new Object[] { targetFile }));
ensureContainerExists(targetDirectory, createMonitor(monitor, 1));
if (arguments == null)
{
arguments = new Object[] { generatingObject };
}
JControlModel jControlModel = getGenerator().getJControlModel();
JMerger jMerger = null;
if (jControlModel.canMerge())
{
jMerger = new JMerger(jControlModel);
}
createImportManager(packageName, className);
String targetFileContents = null;
String targetFileEncoding = getEncoding(targetFile);
if (shouldMerge(targetFile) && exists(targetFile) && jMerger != null)
{
// Prime the import manager with the existing imports of the target.
//
jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(createInputStream(targetFile), targetFileEncoding));
JCompilationUnit targetCompilationUnit = jMerger.getTargetCompilationUnit();
ImportManager importManager = getImportManager();
for (JNode node : targetCompilationUnit.getChildren())
{
if (node instanceof JImport)
{
JImport jImport = (JImport)node;
String qualifiedName = jImport.getQualifiedName();
if (!qualifiedName.endsWith(".*"))
{
importManager.addImport(qualifiedName);
}
}
}
targetFileContents = jMerger.getTargetCompilationUnitContents();
}
setLineDelimiter(getLineDelimiter(targetFile, targetFileEncoding));
String emitterResult = jetEmitter.generate(createMonitor(monitor, 1), arguments, getLineDelimiter());
boolean changed = true;
String newContents = emitterResult;
Options options = getGenerator().getOptions();
if (jMerger != null)
{
jMerger.setFixInterfaceBrace(jControlModel.getFacadeHelper().fixInterfaceBrace());
try
{
jMerger.setSourceCompilationUnit(jMerger.createCompilationUnitForContents(emitterResult));
}
catch (RuntimeException runtimeException)
{
if (targetFileContents != null)
{
throw runtimeException;
}
else
{
jMerger = null;
}
}
if (jMerger != null)
{
// Create a code formatter for this compilation unit, if needed.
//
Object codeFormatter = options.codeFormatting ?
createCodeFormatter(options.codeFormatterOptions, targetFile) : null;
if (targetFileContents != null)
{
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_ExaminingOld_message", new Object[] { targetFile }));
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_PreparingNew_message", new Object[] { targetFile }));
jMerger.merge();
newContents = formatCode(jMerger.getTargetCompilationUnitContents(), codeFormatter, options.commentFormatting);
if (options.importOrganizing)
{
newContents = organizeImports(targetFile.toString(), newContents);
}
changed = !targetFileContents.equals(newContents);
// If the target is read-only, we can ask the platform to release it, and it may be updated in the process.
//
if (changed && isReadOnly(targetFile) && validateEdit(targetFile, createMonitor(monitor, 1)))
{
jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(createInputStream(targetFile), targetFileEncoding));
jMerger.remerge();
newContents = formatCode(jMerger.getTargetCompilationUnitContents(), codeFormatter, options.commentFormatting);
if (options.importOrganizing)
{
newContents = organizeImports(targetFile.toString(), newContents);
}
}
}
else
{
changed = true;
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_PreparingNew_message", new Object[] { targetFile }));
jMerger.merge();
newContents = formatCode(jMerger.getTargetCompilationUnitContents(), codeFormatter, options.commentFormatting);
}
if (jControlModel.getFacadeHelper() != null)
{
jControlModel.getFacadeHelper().reset();
}
}
}
if (jMerger == null)
{
newContents =
CodeGenUtil.convertFormat(jControlModel.getLeadingTabReplacement(), jControlModel.convertToStandardBraceStyle(), emitterResult);
if (targetFileContents != null)
{
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_ExaminingOld_message", new Object[] { targetFile }));
changed = !targetFileContents.equals(newContents);
}
else
{
changed = true;
}
}
monitor.worked(1);
if (changed)
{
String encoding = targetFileEncoding;
byte[] bytes = encoding == null ? newContents.getBytes() : newContents.getBytes(encoding);
// Apply a redirection pattern, if specified.
//
String redirection = options.redirectionPattern;
boolean redirect = redirection != null && redirection.indexOf("{0}") != -1;
if (redirect)
{
String baseName = MessageFormat.format(redirection, new Object[] { className + ".java" });
targetFile = targetDirectory.appendSegment(baseName);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingAlternate_message", new Object[] { targetFile }));
}
if (isReadOnly(targetFile))
{
if (options.forceOverwrite)
{
setWriteable(targetFile);
}
else
{
targetFile = targetDirectory.appendSegment("." + className + ".java.new");
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_UsingDefaultAlternate_message", new Object[] { targetFile }));
}
}
OutputStream outputStream = createOutputStream(targetFile);
outputStream.write(bytes);
outputStream.close();
}
}
catch (Exception e)
{
throw e instanceof RuntimeException ? (RuntimeException)e : new WrappedException(e);
}
finally
{
clearImportManager();
setLineDelimiter(null);
monitor.done();
}
}
/**
* Converts the given workspace path to a <code>URI</code>. No encoding is performed, so the URI may contain invalid
* characters. Such a URI is only used to easily access and manipulate parts of the workspace path. It can then be
* converted back to a string and an {@link org.eclipse.core.runtime.IPath IPath} for use in the workspace, or to an
* encoded {@link #toPlatformResourceURI(URI) platform resource URI} for direct use with the EMF persistence
* framework.
*/
protected URI toURI(String pathName)
{
return URI.createURI(pathName);
}
/**
* Converts the given workspace path URI to an absolute, platform resource URI, with encoding to eliminate any
* invalid characters.
*/
protected URI toPlatformResourceURI(URI uri)
{
return URI.createPlatformResourceURI(uri.toString(), true);
}
/**
* Creates and returns a sub-monitor for the given progress monitor. When running stand-alone, the same monitor is
* actually returned.
*
* @param monitor the parent monitor
* @param ticks the number of work ticks allocated from the parent monitor
*/
protected Monitor createMonitor(Monitor monitor, int ticks)
{
return CodeGenUtil.createMonitor(monitor, ticks);
}
/**
* Creates and caches an {@link org.eclipse.emf.codegen.util.ImportManager ImportManager} for use in generating Java
* code.
*/
protected void createImportManager(String packageName, String className)
{
importManager = new ImportManager(packageName);
importManager.addMasterImport(packageName, className);
}
/**
* Clears the cached {@link org.eclipse.emf.codegen.util.ImportManager ImportManager}.
*/
protected void clearImportManager()
{
importManager = null;
}
/**
* Returns the {@link org.eclipse.emf.codegen.util.ImportManager ImportManager} that is currently in use for
* generating Java code, or null if there is none.
*/
protected ImportManager getImportManager()
{
return importManager;
}
/**
* Sets the current line delimiter used for generating textual results.
* @param lineDelimiter
* @since 2.3
*/
protected void setLineDelimiter(String lineDelimiter)
{
this.lineDelimiter = lineDelimiter;
}
/**
* Returns the current line delimiter used for generating textual results.
* @since 2.3
*/
protected String getLineDelimiter()
{
return lineDelimiter;
}
/**
* Ensures that a project, corresponding to the first segment in the specified workspace path, exists. If the project
* does not exist, a default project will be created. If it does exist and <code>force</code> is true, it will be
* reconfigured to match the default configuration. The remainder of the path suggests the folder under which source
* will be generated.
*
* <p>When running stand-alone, this method does nothing, since simply opening a stream via a <code>URIConverter</code>
* will automatically create the necessary directories.
*/
protected void ensureProjectExists(String workspacePath, Object object, Object projectType, boolean force, Monitor monitor)
{
try
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
EclipseHelper.ensureProjectExists(workspacePath, object, projectType, force, monitor);
}
}
finally
{
monitor.done();
}
}
/**
* Ensures that a container corresponding to the specified relative URI exists. The URI represents a workspace
* path for which the project must already exist, since this method doesn't have the necessary information to
* set up a project. This method will create nested folders within the project, if possible.
*
* <p>When running stand-alone, this method does nothing, since simply opening a stream via a <code>URIConverter</code>
* will automatically create the necessary directories.
*/
protected void ensureContainerExists(URI workspacePath, Monitor monitor)
{
try
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
EclipseHelper.ensureContainerExists(workspacePath.toString(), monitor);
}
}
finally
{
monitor.done();
}
}
/**
* Returns an appropriate <code>URIConverter</code> for use during code generation.
*/
protected URIConverter getURIConverter()
{
ResourceSet resourceSet = getGenerator().getOptions().resourceSet;
URIConverter result = resourceSet != null ? resourceSet.getURIConverter() : null;
if (result != null)
{
return result;
}
if (uriConverter == null)
{
uriConverter = new ExtensibleURIConverterImpl();
}
return uriConverter;
}
/**
* @since 2.3
*/
public String getLineDelimiter(URI workspacePath, String encoding)
{
InputStream inputStream = null;
try
{
inputStream = createInputStream(workspacePath);
String lineDelimiter = ContentHandlerImpl.getLineDelimiter(inputStream, encoding);
if (lineDelimiter != null)
{
return lineDelimiter;
}
}
catch (Exception exception)
{
// If we can't determine it by reading the file,
// look at the preferences instead.
}
finally
{
if (inputStream != null)
{
try
{
inputStream.close();
}
catch (IOException exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
}
}
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return PlatformResourceURIHandlerImpl.WorkbenchHelper.getLineDelimiter(workspacePath.toString(), null);
}
return System.getProperty(Platform.PREF_LINE_SEPARATOR);
}
/**
* Determines whether a given workspace path URI represents a file that already exists.
*/
protected boolean exists(URI workspacePath)
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return PlatformResourceURIHandlerImpl.WorkbenchHelper.exists(workspacePath.toString(), null);
}
return getURIConverter().exists(toPlatformResourceURI(workspacePath), null);
}
/**
* Determines whether a given workspace path URI represents a read-only file.
*/
protected boolean isReadOnly(URI workspacePath)
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return EclipseHelper.isReadOnly(workspacePath.toString());
}
URI uri = getURIConverter().normalize(toPlatformResourceURI(workspacePath));
if ("file".equalsIgnoreCase(uri.scheme()))
{
File file = new File(uri.toFileString());
return file.exists() && !file.canWrite();
}
else
{
return false;
}
}
/**
* Sets the file represented by a workspace path URI to be writable. When running stand-alone, this actually
* <em>deletes</em> the file, since there is no Java platform API for making a file writable.
*/
protected void setWriteable(URI workspacePath) throws Exception
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
EclipseHelper.setWriteable(workspacePath.toString());
return;
}
URI uri = getURIConverter().normalize(toPlatformResourceURI(workspacePath));
if ("file".equalsIgnoreCase(uri.scheme()))
{
new File(uri.toFileString()).delete();
}
}
/**
* When running under Eclipse, performs an
* {@link org.eclipse.core.resources.IWorkspace#validateEdit(IFile[], Object) IWorkspace.validateEdit(IFile[], Object)}
* for the file identified by the given workspace path URI. This notifies the workspace that the file will be edited,
* providing it the opportunity to prepare the files if required. When running stand-alone, does nothing.
*/
protected boolean validateEdit(URI workspacePath, Monitor monitor)
{
try
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return EclipseHelper.validateEdit(workspacePath.toString(), monitor);
}
return false;
}
finally
{
monitor.done();
}
}
/**
* Creates an <code>InputStream</code> for the file identified by the given workspace path URI.
*/
protected InputStream createInputStream(URI workspacePath) throws Exception
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return EclipseHelper.createInputStream(workspacePath.toString());
}
return getURIConverter().createInputStream(toPlatformResourceURI(workspacePath), null);
}
/**
* Creates an <code>OutputStream</code> for the file identified by the given workspace path URI.
* Calls back to the generator to indicate output was {@link Generator#generatedOutput(URI) generated}.
*/
protected OutputStream createOutputStream(URI workspacePath) throws Exception
{
OutputStream result;
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
result = URIConverter.INSTANCE.createOutputStream(toPlatformResourceURI(workspacePath), null);
}
else
{
result = getURIConverter().createOutputStream(toPlatformResourceURI(workspacePath), null);
}
getGenerator().generatedOutput(workspacePath);
return result;
}
/**
* Returns the contents of the file identified by the given workspace path URI, as read using the specified encoding.
*/
protected String getContents(URI workspacePath, String encoding) throws Exception
{
BufferedInputStream bufferedInputStream = new BufferedInputStream(createInputStream(workspacePath));
byte[] input = new byte[bufferedInputStream.available()];
bufferedInputStream.read(input);
bufferedInputStream.close();
return encoding == null ? new String(input) : new String(input, encoding);
}
/**
* When running under Eclipse, queries the workspace to determine the correct encoding for the file identified by
* the given workspace path URI. When running stand-alone, returns null.
*/
protected String getEncoding(URI workspacePath)
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return EclipseHelper.getEncoding(workspacePath.toString());
}
return null;
}
/**
* When running under Eclipse, returns a code formatter; when stand-alone, returns null. If <code>options</code> is
* non-null, the code formatting options it specifies are used to create the formatter. Otherwise, the project is
* obtained from the given workspace path URI, and its default formatting options are used.
*
* @return the created code formatter. If non-null, this will be an instance of
* {@link org.eclipse.jdt.core.formatter.CodeFormatter CodeFormatter}; however, it is not statically typed
* as such to avoid failure when running stand-alone.
*/
protected Object createCodeFormatter(Map<?, ?> options, URI workspacePath)
{
if (EMFPlugin.IS_ECLIPSE_RUNNING)
{
return EclipseHelper.createCodeFormatter(options, workspacePath.toString());
}
return null;
}
/**
* If non-null, use the specified code formatter to format the given compilation unit contents.
* Clients overriding this method should change to overrides {@link #formatCode(String, Object, boolean)} instead.
*
* @return the formatted version of the contents. If the code formatter is null or when running stand-alone, the
* contents are returned unchanged.
* @deprecated
*/
@Deprecated
protected String formatCode(String contents, Object codeFormatter)
{
return EMFPlugin.IS_ECLIPSE_RUNNING ? EclipseHelper.formatCode(contents, codeFormatter, getLineDelimiter(), false) : contents;
}
/**
* If non-null, use the specified code formatter to format the given compilation unit contents.
*
* @return the formatted version of the contents. If the code formatter is null or when running stand-alone, the
* contents are returned unchanged.
* @since 2.8
*/
protected String formatCode(String contents, Object codeFormatter, boolean formatComments)
{
return
formatComments && EMFPlugin.IS_ECLIPSE_RUNNING ?
EclipseHelper.formatCode(contents, codeFormatter, getLineDelimiter(), formatComments) :
formatCode(contents, codeFormatter);
}
/**
* When running under Eclipse, returns the contents with unused imports removed; when stand-alone, returns the original contents.
* @return the contents with unused imports removed, or when running stand-alone, the unchanged contents.
* Since 2.9
*/
protected String organizeImports(String path, String contents)
{
return EMFPlugin.IS_ECLIPSE_RUNNING ? EclipseHelper.organizeImports(path, contents) : contents;
}
/*
* All Eclipse-dependent operations are delegated to this class. This pattern avoids any runtime failure due to
* missing dependencies in the stand-alone case.
*/
private static class EclipseHelper
{
public static boolean ensureProjectExists(String workspacePath, Object object, Object projectType, boolean force, Monitor monitor)
{
try
{
IPath path = new Path(workspacePath);
if (path.isAbsolute())
{
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IProject project = workspace.getRoot().getProject(path.segment(0));
if (!project.exists() || force)
{
IPath javaSource = path.uptoSegment(1).append("src");
org.eclipse.emf.codegen.ecore.Generator.createEMFProject
(javaSource,
null,
Collections.<IProject>emptyList(),
monitor,
org.eclipse.emf.codegen.ecore.Generator.EMF_PLUGIN_PROJECT_STYLE);
}
return workspace.getRoot().getProject(path.segment(0)).exists();
}
}
catch (Exception exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
return false;
}
public static boolean ensureContainerExists(String workspacePath, Monitor monitor)
{
IPath path = new Path(workspacePath);
IContainer container = null;
try
{
monitor.beginTask("", path.segmentCount() + 1);
monitor.subTask(CodeGenEcorePlugin.INSTANCE.getString("_UI_OpeningFolder_message", new Object[] { path }));
if (path.isAbsolute())
{
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IProject project = workspace.getRoot().getProject(path.segment(0));
if (project.exists())
{
if (!project.isOpen())
{
project.open(BasicMonitor.toIProgressMonitor(CodeGenUtil.createMonitor(monitor, 1)));
}
else
{
monitor.worked(1);
}
container = project;
for (int i = 1, length = path.segmentCount(); i < length; i++)
{
IFolder folder = container.getFolder(new Path(path.segment(i)));
if (!folder.exists())
{
folder.create(false, true, BasicMonitor.toIProgressMonitor(CodeGenUtil.createMonitor(monitor, 1)));
}
container = folder;
}
}
}
}
catch (Exception exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
finally
{
monitor.done();
}
return container != null && container.getFullPath().equals(path);
}
public static boolean isReadOnly(String workspacePath)
{
return ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(workspacePath)).isReadOnly();
}
public static void setWriteable(String workspacePath) throws Exception
{
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(workspacePath));
ResourceAttributes resourceAttributes = file.getResourceAttributes();
if (resourceAttributes != null)
{
resourceAttributes.setReadOnly(false);
file.setResourceAttributes(resourceAttributes);
}
}
public static boolean validateEdit(String workspacePath, Monitor monitor)
{
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(workspacePath));
return file.getWorkspace().validateEdit(new IFile [] { file }, IWorkspace.VALIDATE_PROMPT).isOK();
}
public static InputStream createInputStream(String workspacePath) throws Exception
{
return ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(workspacePath)).getContents(true);
}
public static String getEncoding(String workspacePath)
{
try
{
return ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(workspacePath)).getCharset();
}
catch (CoreException exception)
{
return null;
}
}
public static Object createCodeFormatter(Map<?, ?> options, String workspacePath)
{
if (options == null)
{
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(new Path(workspacePath).segment(0));
if (project != null)
{
IJavaProject javaProject = JavaCore.create(project);
if (javaProject != null)
{
options = javaProject.getOptions(true);
}
}
}
return ToolFactory.createCodeFormatter(options);
}
public static String formatCode(String contents, Object codeFormatter, String lineDelimiter, boolean formatComments)
{
if (codeFormatter instanceof CodeFormatter)
{
IDocument doc = new Document(contents);
TextEdit edit = ((CodeFormatter)codeFormatter).format(CodeFormatter.K_COMPILATION_UNIT | (formatComments ? CodeFormatter.F_INCLUDE_COMMENTS : 0), doc.get(), 0, doc.get().length(), 0, lineDelimiter);
if (edit != null)
{
try
{
edit.apply(doc);
contents = doc.get();
}
catch (Exception exception)
{
CodeGenEcorePlugin.INSTANCE.log(exception);
}
}
}
return contents;
}
public static String organizeImports(String workspacePath, String contents)
{
// Keep the result in an array so we can update if we successfully remove imports.
//
final String [] result = new String[] { contents };
// We must be able to determine this Java project for this target path to proceed...
//
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(workspacePath));
IProject project = file.getProject();
if (project != null)
{
IJavaProject javaProject = JavaCore.create(project);
if (javaProject != null)
{
// All is good so we can create a compilation for this path.
//
ICompilationUnit compilationUnit = JavaCore.createCompilationUnitFrom(file);
// Create a specialized working copy owner that creates a specialized buffer to access the contents passed to this method.
//
WorkingCopyOwner workingCopyOwner =
new WorkingCopyOwner()
{
@Override
public IBuffer createBuffer(final ICompilationUnit workingCopy)
{
return
new IBuffer()
{
public IOpenable getOwner()
{
return workingCopy;
}
public String getText(int offset, int length) throws IndexOutOfBoundsException
{
return result[0].substring(offset, offset + length);
}
public int getLength()
{
return result[0].length();
}
public String getContents()
{
return result[0];
}
public char[] getCharacters()
{
return result[0].toCharArray();
}
public char getChar(int position)
{
return result[0].charAt(position);
}
public boolean isReadOnly()
{
return true;
}
public boolean isClosed()
{
return false;
}
public boolean hasUnsavedChanges()
{
return false;
}
public IResource getUnderlyingResource()
{
return null;
}
public void close()
{
// Ignore
}
public void save(IProgressMonitor progress, boolean force) throws JavaModelException
{
throw new UnsupportedOperationException();
}
public void setContents(String contents)
{
throw new UnsupportedOperationException();
}
public void setContents(char[] contents)
{
throw new UnsupportedOperationException();
}
public void replace(int position, int length, String text)
{
throw new UnsupportedOperationException();
}
public void replace(int position, int length, char[] text)
{
throw new UnsupportedOperationException();
}
public void append(String text)
{
throw new UnsupportedOperationException();
}
public void append(char[] text)
{
throw new UnsupportedOperationException();
}
public void addBufferChangedListener(IBufferChangedListener listener)
{
// Ignore
}
public void removeBufferChangedListener(IBufferChangedListener listener)
{
// Ignore
}
};
}
};
try
{
// Create a working copy that yields the contents passed to this method as the compilation unit's current content.
//
ICompilationUnit workingCopy = compilationUnit.getWorkingCopy(workingCopyOwner, null);
// Create a parser that will produce errors for unused imports.
//
ASTParser astParser = CodeGenUtil.EclipseUtil.newASTParser();
astParser.setCompilerOptions(Collections.singletonMap(JavaCore.COMPILER_PB_UNUSED_IMPORT, JavaCore.ERROR));
astParser.setResolveBindings(true);
astParser.setProject(javaProject);
// Create a visitor that will handle the compiled content.
//
ASTRequestor unusedImportRemover =
new ASTRequestor()
{
@Override
public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit compiledUnit)
{
// Visit all the compiler problems looking for unused imports.
//
final Set<String> unusedImports = new HashSet<String>();
IProblem[] problems = compiledUnit.getProblems();
boolean onlyUnusedImportErrors = true;
for (IProblem problem : problems)
{
int id = problem.getID();
if (id == IProblem.UnusedImport)
{
unusedImports.add(problem.getArguments()[0]);
}
else
{
// If there are other errors, we can't rely on there being unused import errors because they're optional and aren't produced when non-optional errors are present.
//
onlyUnusedImportErrors = false;
break;
}
}
// If there are other errors, we need to do our own detailed analysis to find unused imports...
//
if (!onlyUnusedImportErrors)
{
// Build up the set up all imported names, ignoring static and on-demand imports.
//
@SuppressWarnings({ "unchecked", "cast" })
List<? extends ImportDeclaration> imports = (List<? extends ImportDeclaration>)compiledUnit.imports();
for (ImportDeclaration importDeclaration : imports)
{
if (!importDeclaration.isStatic() && !importDeclaration.isOnDemand())
{
unusedImports.add(importDeclaration.getName().getFullyQualifiedName());
}
}
// Walk the AST to determine references to the imported names.
//
compiledUnit.accept
(new ASTVisitor(true)
{
@Override
public boolean visit(ImportDeclaration node)
{
// Ignore the import declarations themselves.
//
return false;
}
@Override
public boolean visit(QualifiedName node)
{
// Determine the root qualifier, visit that simple name, and don't visit the children.
//
Name name = node.getQualifier();
while (name.isQualifiedName())
{
name= ((QualifiedName) name).getQualifier();
}
visit((SimpleName)name);
return false;
}
@Override
public boolean visit(SimpleName node)
{
// If the binding is a type binding, process it, and don't visit the children.
//
IBinding binding = node.resolveBinding();
if (binding instanceof ITypeBinding)
{
// If there is a type binding, ensure it has an erasure and then remove the qualified name of its erasure from the unused imports.
//
ITypeBinding erasure = ((ITypeBinding)binding).getErasure();
if (erasure != null)
{
unusedImports.remove(erasure.getQualifiedName());
return false;
}
}
// If we can't resolve it at all, or the erasure is null, better assume it should resolve to an import...
//
String suffix = "." + node.getIdentifier();
for (String unusedImport : unusedImports)
{
if (unusedImport.endsWith(suffix))
{
unusedImports.remove(unusedImport);
break;
}
}
return false;
}
});
// If there are imports that aren't resolvable, we have to be careful that we don't just remove them as if they are unused because they might be needed somewhere.
// So we look for errors about unresolved types and unresolved variables and remove the corresponding unused import, if there is one.
//
for (IProblem problem : problems)
{
int id = problem.getID();
if (id == IProblem.UndefinedType || id == IProblem.FieldRelated + 83) // This should be IProblem.UnresolvedVariable which was added in 3.6 so isn't available for 3.5 compatibility.
{
String suffix = "." + problem.getArguments()[0];
for (String unusedImport : unusedImports)
{
if (unusedImport.endsWith(suffix))
{
unusedImports.remove(unusedImport);
break;
}
}
}
}
}
// If there are imports that should be removed...
//
if (!unusedImports.isEmpty())
{
// Create an import rewriter if possible.
//
ImportRewrite importRewrite;
try
{
importRewrite = ImportRewrite.create(sourceUnit, true);
}
catch (JavaModelException e)
{
// In this case, we can't fix them, so just return.
//
return;
}
for (String unusedImport : unusedImports)
{
// Try to remove the problematic import, and failing that, try to remove the wildcard import.
//
boolean removed = importRewrite.removeImport(unusedImport);
if (!removed)
{
importRewrite.removeImport(unusedImport + ".*");
}
}
try
{
// Apply the text edits to the original contents and update the result.
//
TextEdit textEdits = importRewrite.rewriteImports(null);
Document document = new Document(result[0]);
textEdits.apply(document);
result[0] = document.get();
}
catch (BadLocationException exception)
{
// We failed unexpectedly, so just return.
//
return;
}
catch (CoreException exception)
{
// We failed unexpectedly, so just return.
//
return;
}
}
}
};
// Compile the working copy source, applying the unused import remover.
//
astParser.createASTs(new ICompilationUnit[] { workingCopy }, new String[0], unusedImportRemover, null);
}
catch (JavaModelException exception)
{
// Ignore all problems and just return the original contents.
}
}
}
return result[0];
}
public static final Set<String> OSGI_ATTRIBUTES =
new HashSet<String>
(Arrays.asList
(new String[]
{
"Bundle-ManifestVersion",
"Bundle-SymbolicName",
"Bundle-Version",
"Bundle-Localization",
"Bundle-Name",
"Bundle-Vendor",
"Bundle-ClassPath",
"Bundle-Activator",
"Bundle-ActivationPolicy",
"Bundle-RequiredExecutionEnvironment",
"Require-Bundle",
"Import-Package",
"Export-Package"
}));
public static List<AttributeData.Element> getElements(String name, String value)
{
// If we fail, or it's not appropriate to compute structured content, we just return null.
//
ArrayList<AttributeData.Element> elements = null;
if (OSGI_ATTRIBUTES.contains(name))
{
try
{
// Parse the manifest elements from the value.
//
ManifestElement[] header = ManifestElement.parseHeader(name, value);
if (header != null)
{
// Create a list to hold the result.
//
elements = new ArrayList<AttributeData.Element>();
// Look at all the elements...
//
for (ManifestElement manifestElement : header)
{
// Create a structure to hold the information.
//
AttributeData.Element element = new AttributeData.Element();
elements.add(element);
// Record the value components.
//
String[] valueComponents = manifestElement.getValueComponents();
for (String valueComponent : valueComponents)
{
element.valueComponents.add(valueComponent);
}
// Record the directives.
//
Enumeration<String> directiveKeys = manifestElement.getDirectiveKeys();
if (directiveKeys != null)
{
while (directiveKeys.hasMoreElements())
{
AttributeData.Element.Directive directive = new AttributeData.Element.Directive();
directive.key = directiveKeys.nextElement();
directive.value = manifestElement.getDirective(directive.key);
element.directives.add(directive);
}
}
// Record the attributes.
//
Enumeration<String> attributeKeys = manifestElement.getKeys();
if (attributeKeys != null)
{
while (attributeKeys.hasMoreElements())
{
AttributeData.Element.Attribute attribute2 = new AttributeData.Element.Attribute();
attribute2.key = attributeKeys.nextElement();
attribute2.value = manifestElement.getAttribute(attribute2.key);
element.attributes.add(attribute2);
}
}
}
}
}
catch (BundleException e)
{
// Ignore
}
}
return elements;
}
}
}