| /******************************************************************************* |
| * Copyright (c) 2008, 2011 Obeo. |
| * 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: |
| * Obeo - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.acceleo.engine.internal.evaluation; |
| |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.Maps; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileReader; |
| import java.io.FilterOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| |
| import org.eclipse.acceleo.common.IAcceleoConstants; |
| import org.eclipse.acceleo.common.preference.AcceleoPreferences; |
| import org.eclipse.acceleo.common.utils.AcceleoASTNodeAdapter; |
| import org.eclipse.acceleo.common.utils.CircularArrayDeque; |
| import org.eclipse.acceleo.common.utils.Deque; |
| import org.eclipse.acceleo.engine.AcceleoEngineMessages; |
| import org.eclipse.acceleo.engine.AcceleoEnginePlugin; |
| import org.eclipse.acceleo.engine.AcceleoEvaluationException; |
| import org.eclipse.acceleo.engine.AcceleoRuntimeException; |
| import org.eclipse.acceleo.engine.event.AcceleoTextGenerationEvent; |
| import org.eclipse.acceleo.engine.event.IAcceleoTextGenerationListener; |
| import org.eclipse.acceleo.engine.generation.strategy.IAcceleoGenerationStrategy; |
| import org.eclipse.acceleo.engine.generation.writers.AbstractAcceleoWriter; |
| import org.eclipse.acceleo.engine.generation.writers.AcceleoFileWriter; |
| import org.eclipse.acceleo.model.mtl.Block; |
| import org.eclipse.acceleo.model.mtl.Module; |
| import org.eclipse.acceleo.model.mtl.ModuleElement; |
| import org.eclipse.acceleo.model.mtl.ProtectedAreaBlock; |
| import org.eclipse.emf.common.EMFPlugin; |
| import org.eclipse.emf.common.notify.Adapter; |
| import org.eclipse.emf.common.util.BasicMonitor; |
| import org.eclipse.emf.common.util.Monitor; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| import org.eclipse.ocl.expressions.OCLExpression; |
| import org.eclipse.ocl.utilities.ASTNode; |
| |
| /** |
| * This will hold all necessary variables for the evaluation of an Acceleo module. |
| * |
| * @param <C> |
| * This should be EClassifier for ecore, Class for UML. |
| * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> |
| */ |
| public class AcceleoEvaluationContext<C> { |
| /** Default size to be used for new buffers. */ |
| private static final int DEFAULT_BUFFER_SIZE = 1024; |
| |
| /** This is the tag we will look for to determine if a file has to be passed through JMerge. */ |
| private static final String JMERGE_TAG = "@generated"; //$NON-NLS-1$ |
| |
| /** DOS line separators. */ |
| private static final String DOS_LINE_SEPARATOR = "\r\n"; //$NON-NLS-1$ |
| |
| /** Unix line separators. */ |
| private static final String UNIX_LINE_SEPARATOR = "\n"; //$NON-NLS-1$ |
| |
| /** Mac line separators. */ |
| private static final String MAC_LINE_SEPARATOR = "\r"; //$NON-NLS-1$ |
| |
| /** Holds the generation preview in the form of mappings filePath => fileContent. */ |
| protected final Map<String, Writer> generationPreview = new HashMap<String, Writer>(); |
| |
| /** |
| * This will hold the list of all listeners registered for notification on text generation from this |
| * engine. |
| * |
| * @since 1.0 |
| */ |
| protected final List<IAcceleoTextGenerationListener> listeners = new ArrayList<IAcceleoTextGenerationListener>( |
| 3); |
| |
| /** |
| * This will be set to true if one of the registered generation listener is interested in generation end |
| * notifications. |
| * |
| * @since 3.0 |
| */ |
| protected final boolean notifyOnGenerationEnd; |
| |
| /** This will maintain the stack trace of expression evaluations. */ |
| private Deque<OCLExpression<C>> expressionStack = new CircularArrayDeque<OCLExpression<C>>(); |
| |
| /** References the file which is to be used as the root for all generated files. */ |
| private final File generationRoot; |
| |
| /** The state of his boolean will be changed while reading files prior to generation. */ |
| private boolean hasJMergeTag; |
| |
| /** This will be initialized with this generation's progress monitor. */ |
| private final Monitor progressMonitor; |
| |
| /** The current generation strategy. */ |
| private final IAcceleoGenerationStrategy strategy; |
| |
| /** This will keep a reference to all user code blocks of a given File. */ |
| private final Map<Writer, Map<String, String>> userCodeBlocks = new HashMap<Writer, Map<String, String>>(); |
| |
| /** This will hold the buffer stack. */ |
| private final Deque<Writer> writers = new CircularArrayDeque<Writer>(); |
| |
| /** |
| * If we try and generate something out of any context (for example, an "if" block outside of any Template |
| * or File), we'll use this "default" writer in order not to lose the generated text. |
| */ |
| private StringWriter defaultWriter; |
| |
| /** |
| * This will allow us to determine whether a given generation tried to generate one or more file(s) more |
| * than once. |
| */ |
| private Map<String, Integer> generateFiles = new HashMap<String, Integer>(); |
| |
| /** |
| * Instantiates an evaluation context given the root of the to-be-generated files. |
| * |
| * @param root |
| * Root of all files that will be generated. |
| * @param listeners |
| * The list of all listeners that are to be notified for text generation from this context. |
| * @param generationStrategy |
| * The generation strategy that's to be used by this context. |
| * @param monitor |
| * This will be used as the progress monitor for the generation. |
| */ |
| public AcceleoEvaluationContext(File root, List<IAcceleoTextGenerationListener> listeners, |
| IAcceleoGenerationStrategy generationStrategy, Monitor monitor) { |
| generationRoot = root; |
| strategy = generationStrategy; |
| this.listeners.addAll(listeners); |
| if (monitor != null) { |
| progressMonitor = monitor; |
| } else { |
| progressMonitor = new BasicMonitor(); |
| } |
| |
| boolean temp = false; |
| for (IAcceleoTextGenerationListener listener : listeners) { |
| if (listener.listensToGenerationEnd()) { |
| temp = true; |
| break; |
| } |
| } |
| notifyOnGenerationEnd = temp; |
| } |
| |
| /** |
| * Appends the given string to the last buffer of the context stack. This will notify all text generation |
| * listeners along the way. |
| * |
| * @param string |
| * String that is to be appended to the current buffer. |
| * @param sourceBlock |
| * The block for which this text has been generated. |
| * @param source |
| * The Object for which was generated this text. |
| * @param fireEvent |
| * Tells us whether we should fire generation events. |
| * @throws AcceleoEvaluationException |
| * Thrown if we cannot append to the current buffer. |
| */ |
| public void append(String string, Block sourceBlock, EObject source, boolean fireEvent) |
| throws AcceleoEvaluationException { |
| try { |
| if (!writers.isEmpty()) { |
| final Writer currentWriter = writers.getLast(); |
| currentWriter.append(string); |
| if (fireEvent && string.length() > 0) { |
| fireTextGenerated(new AcceleoTextGenerationEvent(string, sourceBlock, source)); |
| } |
| } else { |
| final String message = AcceleoEngineMessages |
| .getString("AcceleoEvaluationVisitor.PossibleEmptyFileName"); //$NON-NLS-1$ |
| if (!EMFPlugin.IS_ECLIPSE_RUNNING || AcceleoPreferences.isDebugMessagesEnabled()) { |
| AcceleoEnginePlugin.log(message, false); |
| } |
| if (defaultWriter == null) { |
| defaultWriter = new StringWriter(DEFAULT_BUFFER_SIZE); |
| } |
| defaultWriter.append(string); |
| } |
| } catch (final IOException e) { |
| throw new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.AppendError"), e); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Adds the given expression at the end of the expression stack. |
| * |
| * @param expression |
| * Expression that is to be appended to the expression stack trace. |
| */ |
| public void addToStack(OCLExpression<C> expression) { |
| expressionStack.add(expression); |
| } |
| |
| /** |
| * Allows clients to await for the lost file creation to end. |
| * |
| * @throws InterruptedException |
| * This will be thrown if the lost files creation is interrupted somehow. |
| */ |
| public void awaitCompletion() throws InterruptedException { |
| strategy.awaitCompletion(); |
| } |
| |
| /** |
| * This will create and return an evaluation exception with a custom stack trace filled in for the given |
| * block. The <em>messageKey</em> should map to an actual message in |
| * <em>org/eclipse/acceleo/engine/acceleoenginemessages.properties</em>. |
| * |
| * @param node |
| * Node from which the failure originated. |
| * @param messageKey |
| * This should map to the message that is to be retrieved for this exception. |
| * @param currentSelf |
| * The last recorded value of the <em>self</em> variable. |
| * @return An evaluation exception for the given block. |
| */ |
| public AcceleoEvaluationException createAcceleoException(ASTNode node, String messageKey, |
| Object currentSelf) { |
| return createAcceleoException(node, null, messageKey, currentSelf); |
| } |
| |
| /** |
| * This will create and return an evaluation exception with a custom stack trace filled in for the given |
| * block. The <em>messageKey</em> should map to an actual message in |
| * <em>org/eclipse/acceleo/engine/acceleoenginemessages.properties</em>. |
| * |
| * @param node |
| * Node from which the failure originated. |
| * @param expression |
| * if the actual failure was caused by a subexpression of <em>block</em>, pass it here. |
| * @param messageKey |
| * This should map to the message that is to be retrieved for this exception. |
| * @param currentSelf |
| * The last recorded value of the <em>self</em> variable. |
| * @return An evaluation exception for the given block. |
| */ |
| public AcceleoEvaluationException createAcceleoException(ASTNode node, OCLExpression<C> expression, |
| String messageKey, Object currentSelf) { |
| Adapter adapter = EcoreUtil.getAdapter(node.eAdapters(), AcceleoASTNodeAdapter.class); |
| int line = 0; |
| if (adapter instanceof AcceleoASTNodeAdapter) { |
| line = ((AcceleoASTNodeAdapter)adapter).getLine(); |
| } |
| String moduleName = ((Module)EcoreUtil.getRootContainer(node)).getName(); |
| String message = AcceleoEngineMessages.getString(messageKey, Integer.valueOf(line), moduleName, node |
| .toString(), currentSelf, expression); |
| |
| AcceleoFileWriter acceleoFileWriter = this.getAcceleoFileWriterFromContext(); |
| if (acceleoFileWriter != null) { |
| message += " " + AcceleoEngineMessages.getString("AcceleoEvaluationContext.FileException", //$NON-NLS-1$ //$NON-NLS-2$ |
| acceleoFileWriter.getTargetPath()); |
| } |
| |
| final AcceleoEvaluationException exception = new AcceleoEvaluationException(message); |
| exception.setStackTrace(createAcceleoStackTrace()); |
| return exception; |
| } |
| |
| /** |
| * Returns the first Acceleo writer found in the context or <code>null</code> otherwise. |
| * |
| * @return The first Acceleo writer found in the context or <code>null</code> otherwise. |
| */ |
| private AcceleoFileWriter getAcceleoFileWriterFromContext() { |
| for (int i = writers.size() - 1; i >= 0; i--) { |
| Writer writer = writers.get(i); |
| if (writer instanceof AcceleoFileWriter) { |
| return (AcceleoFileWriter)writer; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Wraps the given throwable inside a custom Acceleo Exception. |
| * |
| * @param cause |
| * Actual cause of the failure. |
| * @return The created exception. Could be <code>null</code> if this context has already been disposed. |
| */ |
| public AcceleoRuntimeException createAcceleoRuntimeException(Throwable cause) { |
| AcceleoRuntimeException exception = new AcceleoRuntimeException(cause); |
| if (expressionStack.size() > 0) { |
| StackTraceElement[] traceElements = createAcceleoStackTrace(); |
| if (traceElements.length > 0) { |
| exception.setStackTrace(traceElements); |
| } |
| } |
| return exception; |
| } |
| |
| /** |
| * This will create a stack trace according to the current evaluation stack as recorded in |
| * {@link #expressionStack}. |
| * |
| * @return Stack trace that can be used with {@link Exception#setStackTrace(StackTraceElement[])}. |
| */ |
| public StackTraceElement[] createAcceleoStackTrace() { |
| StackTraceElement[] stackTrace = new StackTraceElement[expressionStack.size()]; |
| for (int i = expressionStack.size() - 1; i >= 0; i--) { |
| OCLExpression<C> expression = expressionStack.get(i); |
| |
| EObject rootContainer = EcoreUtil.getRootContainer(expression); |
| if (rootContainer instanceof Module) { |
| Module containingModule = (Module)rootContainer; |
| String moduleFile; |
| if (containingModule.eResource() != null && containingModule.eResource().getURI() != null) { |
| moduleFile = containingModule.eResource().getURI().trimFileExtension().lastSegment() |
| + '.' + IAcceleoConstants.MTL_FILE_EXTENSION; |
| } else { |
| moduleFile = containingModule.getName() + '.' + IAcceleoConstants.MTL_FILE_EXTENSION; |
| } |
| EObject containingModuleElement = expression; |
| while (!(containingModuleElement instanceof ModuleElement)) { |
| containingModuleElement = containingModuleElement.eContainer(); |
| } |
| Adapter adapter = EcoreUtil.getAdapter(expression.eAdapters(), AcceleoASTNodeAdapter.class); |
| int line = 0; |
| if (adapter instanceof AcceleoASTNodeAdapter) { |
| line = ((AcceleoASTNodeAdapter)adapter).getLine(); |
| } |
| stackTrace[expressionStack.size() - i - 1] = new StackTraceElement( |
| containingModule.getName(), containingModuleElement.toString(), moduleFile, line); |
| } else if (rootContainer instanceof ProtectedAreaBlock) { |
| // Let's not handle this now... |
| stackTrace = new StackTraceElement[0]; |
| } |
| } |
| return stackTrace; |
| } |
| |
| /** |
| * Closes the last writer of the stack and returns its result if it was a StringWriter. This is a |
| * convenience methode to close contexts that were opened for other than file blocks. |
| * |
| * @return Result held by the last writer of the stack. |
| * @throws AcceleoEvaluationException |
| * This will be thrown if the last writer of the stack cannot be flushed and closed. |
| */ |
| public String closeContext() throws AcceleoEvaluationException { |
| return closeContext(null, null); |
| } |
| |
| /** |
| * Closes the last writer of the stack and returns its result if it was a StringWriter. The empty String |
| * will be returned for FileWriters. |
| * |
| * @param sourceBlock |
| * The source block that first created this context. Only used when closing a file context. |
| * @param source |
| * The source EObject for this block. Only used when closing a file context. |
| * @return Result held by the last writer of the stack. |
| * @throws AcceleoEvaluationException |
| * This will be thrown if the last writer of the stack cannot be flushed and closed. |
| */ |
| public String closeContext(Block sourceBlock, EObject source) throws AcceleoEvaluationException { |
| if (writers.isEmpty()) { |
| final String message = AcceleoEngineMessages |
| .getString("AcceleoEvaluationVisitor.PossibleEmptyFileName"); //$NON-NLS-1$ |
| if (!EMFPlugin.IS_ECLIPSE_RUNNING && AcceleoPreferences.isDebugMessagesEnabled()) { |
| AcceleoEnginePlugin.log(message, false); |
| } |
| return ""; //$NON-NLS-1$ |
| } |
| |
| final Writer last = writers.removeLast(); |
| final String result; |
| try { |
| if (last instanceof AbstractAcceleoWriter) { |
| final String filePath = ((AbstractAcceleoWriter)last).getTargetPath(); |
| final Map<String, String> lostCode = userCodeBlocks.get(last); |
| if (lostCode.size() > 0) { |
| Map<String, StringWriter> lostFiles = strategy.createLostFile(filePath, lostCode); |
| if (lostFiles != null) { |
| for (Map.Entry<String, StringWriter> lostFile : lostFiles.entrySet()) { |
| generationPreview.put(lostFile.getKey(), lostFile.getValue()); |
| } |
| } |
| } |
| strategy.flushWriter(filePath, last); |
| fireFileGenerated(filePath, sourceBlock, source); |
| result = ""; //$NON-NLS-1$ |
| } else if (last instanceof OutputStreamWriter) { |
| last.close(); |
| result = ""; //$NON-NLS-1$ |
| } else { |
| // others are plain StringWriters. Close has no effect on those. |
| // Note that we'll never be here for file blocks : these always are AcceleoWriterDecorators |
| result = last.toString(); |
| } |
| return result; |
| } catch (final IOException e) { |
| throw new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.WriteError"), e); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * This will be used to dispose of all created buffers and caches. |
| * |
| * @throws AcceleoEvaluationException |
| * Thrown if the disposal of the old writers fails. |
| */ |
| public void dispose() throws AcceleoEvaluationException { |
| AcceleoEvaluationException exception = null; |
| try { |
| try { |
| awaitCompletion(); |
| } catch (InterruptedException e) { |
| exception = new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.CleanUpError"), e); //$NON-NLS-1$ |
| } |
| try { |
| for (final Writer writer : writers) { |
| writer.close(); |
| } |
| } catch (final IOException e) { |
| exception = new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.CleanUpError"), e); //$NON-NLS-1$ |
| } |
| } finally { |
| generationPreview.clear(); |
| listeners.clear(); |
| userCodeBlocks.clear(); |
| writers.clear(); |
| expressionStack.clear(); |
| } |
| if (exception != null) { |
| throw exception; |
| } |
| } |
| |
| /** |
| * Notifies the context that a file at the given <em>filePath</em> will be generated. |
| * |
| * @param filePath |
| * Path to the file. |
| */ |
| public void generateFile(String filePath) { |
| Integer timesGenerated = generateFiles.get(filePath); |
| if (timesGenerated == null) { |
| timesGenerated = Integer.valueOf(1); |
| } else { |
| timesGenerated = Integer.valueOf(timesGenerated.intValue() + 1); |
| } |
| generateFiles.put(filePath, timesGenerated); |
| } |
| |
| /** |
| * This will return the indentation of the very last line of the very last file writer in context. |
| * |
| * @return indentation of the very last line in context. |
| */ |
| public String getLastFileIndentation() { |
| Writer writer = null; |
| for (int i = writers.size() - 1; i >= 0 && !(writer instanceof AbstractAcceleoWriter); i--) { |
| writer = writers.get(i); |
| } |
| if (writer != null) { |
| return ((AbstractAcceleoWriter)writer).getCurrentLineIndentation(); |
| } |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Walks up the expression stack and returns the last visited Block. |
| * |
| * @return The last visited Block. |
| */ |
| public Block getLastVisitedBlock() { |
| if (expressionStack.isEmpty()) { |
| return null; |
| } |
| final ListIterator<OCLExpression<C>> expressionIterator = expressionStack |
| .listIterator(expressionStack.size()); |
| OCLExpression<C> previous; |
| do { |
| previous = expressionIterator.previous(); |
| } while (!(previous instanceof Block) && expressionIterator.hasPrevious()); |
| |
| Block lastBlock = null; |
| if (previous instanceof Block) { |
| lastBlock = (Block)previous; |
| } |
| return lastBlock; |
| } |
| |
| public Deque<OCLExpression<C>> getExpressionStack() { |
| return expressionStack; |
| } |
| |
| /** |
| * This will return the indentation of the very last line of the very last opened writer in context. |
| * |
| * @return indentation of the very last line in context. |
| */ |
| public String getCurrentLineIndentation() { |
| StringBuffer currentIndentation = new StringBuffer(); |
| if (!writers.isEmpty()) { |
| Writer writer = writers.getLast(); |
| if (writer instanceof AbstractAcceleoWriter) { |
| return ((AbstractAcceleoWriter)writer).getCurrentLineIndentation(); |
| } |
| // Only String writers remain |
| String content = writer.toString(); |
| int newLineIndex = -1; |
| if (content.contains(DOS_LINE_SEPARATOR)) { |
| newLineIndex = content.lastIndexOf(DOS_LINE_SEPARATOR) + DOS_LINE_SEPARATOR.length(); |
| } else if (content.contains(UNIX_LINE_SEPARATOR)) { |
| newLineIndex = content.lastIndexOf(UNIX_LINE_SEPARATOR) + UNIX_LINE_SEPARATOR.length(); |
| } else if (content.contains(MAC_LINE_SEPARATOR)) { |
| newLineIndex = content.lastIndexOf(MAC_LINE_SEPARATOR) + MAC_LINE_SEPARATOR.length(); |
| } |
| |
| if (newLineIndex == -1) { |
| newLineIndex = 0; |
| } |
| for (int i = newLineIndex; i < content.length(); i++) { |
| if (Character.isWhitespace(content.charAt(i))) { |
| currentIndentation.append(content.charAt(i)); |
| } else { |
| break; |
| } |
| } |
| } |
| return currentIndentation.toString(); |
| } |
| |
| /** |
| * Returns the text that has been appended to the default writer, if any. |
| * |
| * @return The text that has been appended to the default writer, <code>null</code> if none. |
| */ |
| public String getDefaultText() { |
| if (defaultWriter != null) { |
| defaultWriter.flush(); |
| String text = defaultWriter.toString(); |
| defaultWriter = null; |
| return text; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the file that would be created for the given filePath according to the current generation root. |
| * |
| * @param filePath |
| * path of the file that will be generated. |
| * @return The File that would be created for the given filePath. |
| */ |
| public File getFileFor(String filePath) { |
| final File generatedFile; |
| if (filePath.startsWith("file:")) { //$NON-NLS-1$ |
| generatedFile = new File(filePath); |
| } else { |
| generatedFile = new File(generationRoot, filePath); |
| } |
| return generatedFile; |
| } |
| |
| /** |
| * Returns the preview of the generation handled by this context. |
| * |
| * @return The generation preview. |
| */ |
| public Map<String, String> getGenerationPreview() { |
| return new HashMap<String, String>(strategy.preparePreview(generationPreview)); |
| } |
| |
| /** |
| * This will return the current progress monitor. |
| * |
| * @return The current progress monitor. |
| */ |
| public Monitor getProgressMonitor() { |
| return progressMonitor; |
| } |
| |
| /** |
| * This will return the content of the protected area associated with the given marker in the current |
| * context. |
| * |
| * @param marker |
| * Marker of the sought protected area content. |
| * @return Content of the protected area associated with the given marker. <code>null</code> if no content |
| * can be found. |
| */ |
| public String getProtectedAreaContent(String marker) { |
| // Seeks out the last opened file writer |
| Writer writer = null; |
| for (int i = writers.size() - 1; i >= 0 && !(writer instanceof AbstractAcceleoWriter); i--) { |
| writer = writers.get(i); |
| } |
| |
| final Map<String, String> areas = userCodeBlocks.get(writer); |
| if (areas != null) { |
| return areas.remove(marker); |
| } |
| return null; |
| } |
| |
| /** |
| * This will be called by the generation engine once all evaluations are finished for this generation. It |
| * will be used to call for the current generation strategy's global handlers. |
| */ |
| public void hookGenerationEnd() { |
| final Map<String, Map<String, String>> lostCode = new HashMap<String, Map<String, String>>(); |
| for (Map.Entry<Writer, Map<String, String>> entry : userCodeBlocks.entrySet()) { |
| if (!entry.getValue().isEmpty()) { |
| final String filePath = ((AbstractAcceleoWriter)entry.getKey()).getTargetPath(); |
| lostCode.put(filePath, entry.getValue()); |
| } |
| } |
| if (!lostCode.isEmpty()) { |
| strategy.createLostFiles(lostCode); |
| } |
| try { |
| strategy.flushWriters(generationPreview); |
| } catch (IOException e) { |
| throw new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.WriteError"), e); //$NON-NLS-1$ |
| } |
| |
| Map<String, Integer> filteredFiles = Maps.filterEntries(generateFiles, |
| new Predicate<Map.Entry<String, Integer>>() { |
| public boolean apply(Map.Entry<String, Integer> input) { |
| return input.getValue().intValue() > 1; |
| } |
| }); |
| |
| if (!filteredFiles.isEmpty()) { |
| final StringBuilder message = new StringBuilder(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.OverrodeFiles")); //$NON-NLS-1$ |
| message.append('\n').append('\n'); |
| for (Map.Entry<String, Integer> file : filteredFiles.entrySet()) { |
| message.append(file.getKey() + " : " + file.getValue().toString() + " times" + '\n'); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| AcceleoEnginePlugin.log(message.toString(), false); |
| } |
| } |
| |
| /** |
| * Creates a new writer and appends it to the end of the stack. |
| * |
| * @throws AcceleoEvaluationException |
| * Thrown if the precedent buffer cannot be flushed. |
| */ |
| public void openNested() throws AcceleoEvaluationException { |
| try { |
| if (!writers.isEmpty()) { |
| writers.getLast().flush(); |
| } |
| } catch (final IOException e) { |
| throw new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.FlushError"), e); //$NON-NLS-1$ |
| } |
| writers.add(new StringWriter(DEFAULT_BUFFER_SIZE)); |
| } |
| |
| /** |
| * Create a new writer for the file located at the given path under <tt>generationRoot</tt> and appends it |
| * to the end of the stack. |
| * <p> |
| * "file" schemes are handled as absolute paths and will ignore the <tt>generationRoot</tt>. |
| * </p> |
| * |
| * @param generatedFile |
| * File that is to be created. |
| * @param fileBlock |
| * The file block which asked for this context. Only used for generation events. |
| * @param source |
| * The source EObject for this file block. Only used for generation events. |
| * @param appendMode |
| * If <code>false</code>, the file will be replaced by a new one. |
| * @param charset |
| * Charset of the target file. |
| * @throws AcceleoEvaluationException |
| * Thrown if the file cannot be created. |
| */ |
| public void openNested(File generatedFile, Block fileBlock, EObject source, boolean appendMode, |
| String charset) throws AcceleoEvaluationException { |
| fireFilePathComputed(new AcceleoTextGenerationEvent(generatedFile.getPath(), fileBlock, source)); |
| try { |
| if (!writers.isEmpty()) { |
| writers.getLast().flush(); |
| } |
| final Map<String, String> savedCodeBlocks = new HashMap<String, String>(); |
| if (generatedFile.exists()) { |
| savedCodeBlocks.putAll(saveProtectedAreas(generatedFile)); |
| } |
| // If the current preview contains overlapping blocks, give them priority |
| if (generationPreview.containsKey(generatedFile.getPath())) { |
| savedCodeBlocks.putAll(saveProtectedAreas(generationPreview.get(generatedFile.getPath()) |
| .toString())); |
| } |
| // We checked for JMerge tags when saving protected areas. we'll use this information here. |
| final AbstractAcceleoWriter writer; |
| if (charset != null) { |
| writer = strategy.createWriterFor(generatedFile, (AbstractAcceleoWriter)generationPreview |
| .get(generatedFile.getPath()), appendMode, hasJMergeTag, charset); |
| } else { |
| writer = strategy.createWriterFor(generatedFile, (AbstractAcceleoWriter)generationPreview |
| .get(generatedFile.getPath()), appendMode, hasJMergeTag); |
| } |
| generationPreview.put(generatedFile.getPath(), writer); |
| // reset the jmerge state for the following file blocks |
| hasJMergeTag = false; |
| userCodeBlocks.put(writer, savedCodeBlocks); |
| writers.add(writer); |
| } catch (final IOException e) { |
| throw new AcceleoEvaluationException(AcceleoEngineMessages.getString( |
| "AcceleoEvaluationContext.FileCreationError", generatedFile.getPath()), e); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Create a new writer directed at the given {@link OutputStream}. This is mainly used for fileBlocks with |
| * "stdout" URI. |
| * |
| * @param stream |
| * Stream to which writing will be directed. |
| */ |
| public void openNested(OutputStream stream) { |
| try { |
| if (!writers.isEmpty()) { |
| writers.getLast().flush(); |
| } |
| } catch (final IOException e) { |
| throw new AcceleoEvaluationException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.FlushError"), e); //$NON-NLS-1$ |
| } |
| writers.add(new OutputStreamWriter(new AcceleoFilterOutputStream(stream))); |
| } |
| |
| /** |
| * Create a new writer for the file located at the given path under <tt>generationRoot</tt> and appends it |
| * to the end of the stack. |
| * <p> |
| * "file" schemes are handled as absolute paths and will ignore the <tt>generationRoot</tt>. |
| * </p> |
| * |
| * @param filePath |
| * Path of the file around which we need a FileWriter. The file will be created under the |
| * generationRoot if needed. |
| * @param fileBlock |
| * The file block which asked for this context. Only used for generation events. |
| * @param source |
| * The source EObject for this file block. Only used for generation events. |
| * @param appendMode |
| * If <code>false</code>, the file will be replaced by a new one. |
| * @throws AcceleoEvaluationException |
| * Thrown if the file cannot be created. |
| */ |
| public void openNested(String filePath, Block fileBlock, EObject source, boolean appendMode) |
| throws AcceleoEvaluationException { |
| openNested(getFileFor(filePath), fileBlock, source, appendMode, null); |
| } |
| |
| /** |
| * Removes the last added expression from the expression stack trace. |
| */ |
| public void removeFromStack() { |
| if (!expressionStack.isEmpty()) { |
| expressionStack.removeLast(); |
| } |
| } |
| |
| /** |
| * Notifies all listeners that a file has just been generated. |
| * |
| * @param filePath |
| * Path of the generated file. |
| * @param fileBlock |
| * File block which generation just ended. |
| * @param source |
| * The Object for which was generated this file. |
| */ |
| protected void fireFileGenerated(String filePath, Block fileBlock, EObject source) { |
| AcceleoTextGenerationEvent event = new AcceleoTextGenerationEvent(filePath, fileBlock, source); |
| for (IAcceleoTextGenerationListener listener : listeners) { |
| listener.fileGenerated(event); |
| } |
| } |
| |
| /** |
| * Notifies all listeners that a file is going to be created. |
| * |
| * @param event |
| * The generation event that is to be sent to registered listeners. |
| */ |
| private void fireFilePathComputed(AcceleoTextGenerationEvent event) { |
| for (IAcceleoTextGenerationListener listener : listeners) { |
| listener.filePathComputed(event); |
| } |
| } |
| |
| /** |
| * Notifies all listeners that text has been generated. |
| * |
| * @param event |
| * The generation event that is to be sent to registered listeners. |
| */ |
| private void fireTextGenerated(AcceleoTextGenerationEvent event) { |
| for (IAcceleoTextGenerationListener listener : listeners) { |
| listener.textGenerated(event); |
| } |
| } |
| |
| /** |
| * This will return the list of protected areas the given file contains. |
| * |
| * @param reader |
| * Reader which content is to be searched through for protected areas. |
| * @return The list of saved protected areas. |
| * @throws IOException |
| * Thrown if we cannot read through the provided reader. |
| */ |
| private Map<String, String> internalSaveProtectedAreas(LineReader reader) throws IOException { |
| final Map<String, String> protectedAreas = new HashMap<String, String>(); |
| final String usercodeStart = AcceleoEngineMessages.getString("usercode.start"); //$NON-NLS-1$ |
| final String usercodeEnd = AcceleoEngineMessages.getString("usercode.end"); //$NON-NLS-1$ |
| String line = reader.readLine(); |
| while (line != null) { |
| if (!hasJMergeTag && line.contains(JMERGE_TAG)) { |
| hasJMergeTag = true; |
| } |
| if (line.contains(usercodeStart)) { |
| final String marker = line.substring(line.indexOf(usercodeStart) + usercodeStart.length()) |
| .trim(); |
| final StringBuffer areaContent = new StringBuffer(DEFAULT_BUFFER_SIZE); |
| final int start = line.indexOf(usercodeStart); |
| // Everything preceding the start of user code doesn't need to be saved |
| areaContent.append(line.substring(start)); |
| |
| /* |
| * TODO If there is no "end of user code", or if the protected content is too large, this will |
| * fail in OutOfMemoryErrors. Could we use a temp File (java.nio?) instead of a StringBuffer? |
| */ |
| String lastEOF = reader.getLastEOLSequence(); |
| areaContent.append(lastEOF); |
| |
| line = reader.readLine(); |
| lastEOF = reader.getLastEOLSequence(); |
| while (line != null) { |
| if (!hasJMergeTag && line.contains(JMERGE_TAG)) { |
| hasJMergeTag = true; |
| } |
| // Everything following the end of user code marker doesn't need to be saved |
| if (line.contains(usercodeEnd)) { |
| final int endOffset = line.indexOf(usercodeEnd) + usercodeEnd.length(); |
| areaContent.append(line.substring(0, endOffset)); |
| break; |
| } |
| areaContent.append(line); |
| areaContent.append(lastEOF); |
| |
| line = reader.readLine(); |
| lastEOF = reader.getLastEOLSequence(); |
| } |
| protectedAreas.put(marker, areaContent.toString()); |
| } |
| line = reader.readLine(); |
| } |
| return protectedAreas; |
| } |
| |
| /** |
| * This will return the list of protected areas the given file contains. <b>Note</b> that we will use this |
| * occasion to look for {@value #JMERGE_TAG} throughout the file. |
| * |
| * @param file |
| * File which protected areas are to be saved. |
| * @return The list of saved protected areas. |
| * @throws IOException |
| * Thrown if we cannot read through <tt>file</tt>. |
| */ |
| private Map<String, String> saveProtectedAreas(File file) throws IOException { |
| Map<String, String> protectedAreas = new HashMap<String, String>(); |
| LineReader reader = null; |
| try { |
| reader = new LineReader(new FileReader(file)); |
| protectedAreas = internalSaveProtectedAreas(reader); |
| } catch (final FileNotFoundException e) { |
| // cannot be thrown here, we were called after testing that the file indeed existed. |
| AcceleoEnginePlugin.log(e, true); |
| } finally { |
| if (reader != null) { |
| reader.close(); |
| } |
| } |
| return protectedAreas; |
| } |
| |
| /** |
| * This will return the list of protected areas the given String contains. <b>Note</b> that we will use |
| * this occasion to look for {@value #JMERGE_TAG} throughout the file. |
| * |
| * @param buffer |
| * String (file content) which protected areas are to be saved. |
| * @return The list of saved protected areas. |
| */ |
| private Map<String, String> saveProtectedAreas(String buffer) { |
| Map<String, String> protectedAreas = new HashMap<String, String>(); |
| LineReader reader = null; |
| try { |
| reader = new LineReader(new StringReader(buffer)); |
| protectedAreas = internalSaveProtectedAreas(reader); |
| } catch (IOException e) { |
| // Cannot happen here |
| AcceleoEnginePlugin.log(e, true); |
| } finally { |
| if (reader != null) { |
| try { |
| reader.close(); |
| } catch (IOException e) { |
| // This should never happen with a String Reader |
| AcceleoEnginePlugin.log(e, true); |
| } |
| } |
| } |
| return protectedAreas; |
| } |
| |
| /** |
| * This implementation of a FilterOutputStream will avoid closing the standard output if it is the |
| * underlying stream. |
| * |
| * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> |
| */ |
| private final class AcceleoFilterOutputStream extends FilterOutputStream { |
| /** |
| * Constructs an output stream redirecting all calls to the given {@link OutputStream}. |
| * |
| * @param out |
| * The decorated output stream. |
| */ |
| public AcceleoFilterOutputStream(OutputStream out) { |
| super(out); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.FilterOutputStream#close() |
| */ |
| @Override |
| public void close() throws IOException { |
| try { |
| flush(); |
| } catch (IOException e) { |
| // Ignored exception |
| } |
| if (out != System.out) { |
| out.close(); |
| } |
| } |
| } |
| |
| /** |
| * This implementation of a Reader will allow us to read lines while still giving us access to the eol |
| * sequence. |
| * <p> |
| * Portions of this class have been copied from BufferedReader. |
| * </p> |
| * |
| * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> |
| */ |
| public final class LineReader extends Reader { |
| /** Size of our read buffer. */ |
| private static final int BUFFER_SIZE = 8192; |
| |
| /** Character buffer in which we'll read. */ |
| private char[] characterBuffer = new char[BUFFER_SIZE]; |
| |
| /** Our underlying stream. */ |
| private Reader input; |
| |
| /** Number of chars in the local buffer. */ |
| private int nChars; |
| |
| /** Next character to read from the local buffer. */ |
| private int nextChar; |
| |
| /** Last EOL sequence encountered by {@link #readLine(boolean)}. */ |
| private String lastEOL = DOS_LINE_SEPARATOR; |
| |
| /** |
| * The gap until the next line. |
| */ |
| private int gap; |
| |
| /** |
| * Constructs our buffered reader given its underlying reader. |
| * |
| * @param in |
| * The reader from which to retrieve input. |
| */ |
| public LineReader(Reader in) { |
| super(in); |
| this.input = in; |
| nChars = 0; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.Reader#markSupported() |
| */ |
| @Override |
| public boolean markSupported() { |
| return false; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.Reader#read() |
| */ |
| @Override |
| public int read() throws IOException { |
| synchronized(lock) { |
| ensureOpen(); |
| if (nextChar >= nChars) { |
| fill(); |
| if (nextChar >= nChars) { |
| return -1; |
| } |
| } |
| return characterBuffer[nextChar++]; |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.Reader#close() |
| */ |
| @Override |
| public void close() throws IOException { |
| synchronized(lock) { |
| if (input == null) { |
| return; |
| } |
| input.close(); |
| input = null; |
| characterBuffer = null; |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.Reader#read(char[], int, int) |
| */ |
| @Override |
| public int read(char[] cbuf, int off, int len) throws IOException { |
| synchronized(lock) { |
| ensureOpen(); |
| if (off < 0 || len < 0 || (off + len) > cbuf.length || (off + len) < 0) { |
| throw new IndexOutOfBoundsException(); |
| } else if (len == 0) { |
| return 0; |
| } |
| |
| int n = internalRead(cbuf, off, len); |
| if (n > 0) { |
| while (n < len && input.ready()) { |
| int n1 = internalRead(cbuf, off + n, len - n); |
| if (n1 <= 0) { |
| break; |
| } |
| n += n1; |
| } |
| } |
| return n; |
| } |
| } |
| |
| /** |
| * Reads a line of text. A line is considered to be terminated by either a line feed ('\n'), a |
| * carriage return ('\r') or a carriage return followed immediately by a line feed ("\r\n"). The line |
| * termination sequence will be omitted. |
| * |
| * @return A string containing the content of the line, or <code>null</code> if the end of the stream |
| * has been reached. |
| * @throws IOException |
| * Thrown if the stream is closed or an I/O operation fails. |
| */ |
| public String readLine() throws IOException { |
| StringBuilder lineBuffer = null; |
| int startChar; |
| |
| String line = null; |
| synchronized(lock) { |
| ensureOpen(); |
| |
| while (line == null) { |
| int previousGap = gap; |
| if (nextChar >= nChars) { |
| fill(); |
| } |
| if (nextChar >= nChars) { |
| // Reached the end of the stream |
| if (lineBuffer != null && lineBuffer.length() > 0) { |
| line = lineBuffer.toString(); |
| } |
| break; |
| } |
| boolean eol = false; |
| char c = 0; |
| int i; |
| |
| for (i = nextChar + gap; i < nChars; i++) { |
| c = characterBuffer[i]; |
| if (c == '\n' || c == '\r') { |
| eol = true; |
| break; |
| } |
| } |
| |
| startChar = nextChar; |
| nextChar = i; |
| |
| if (eol) { |
| if (lineBuffer == null) { |
| line = new String(characterBuffer, startChar, i - startChar); |
| } else { |
| lineBuffer.append(characterBuffer, startChar, i - startChar); |
| line = lineBuffer.toString(); |
| } |
| if (c == '\n') { |
| lastEOL = "\n"; //$NON-NLS-1$ |
| nextChar++; |
| } else if (c == '\r') { |
| final int max = 8191; |
| if (nextChar != max && characterBuffer.length >= nextChar |
| && characterBuffer[nextChar + 1] == '\n') { |
| lastEOL = DOS_LINE_SEPARATOR; |
| nextChar += 2; |
| gap = 0; |
| } else if (nextChar != max) { |
| lastEOL = "\r"; //$NON-NLS-1$ |
| nextChar++; |
| gap = 0; |
| } else if (nextChar == max && DOS_LINE_SEPARATOR.equals(lastEOL)) { |
| nextChar += 2; |
| gap = 1; |
| } else if (nextChar == max) { |
| nextChar++; |
| gap = 0; |
| } |
| } |
| } |
| |
| if (lineBuffer == null) { |
| lineBuffer = new StringBuilder(); |
| } |
| lineBuffer |
| .append(characterBuffer, startChar + previousGap, i - (startChar + previousGap)); |
| } |
| } |
| return line; |
| } |
| |
| /** |
| * Returns the last EOL sequence encountered by {@link #readLine()}. |
| * |
| * @return The last EOL sequence encountered by {@link #readLine()}. May be <code>null</code>. |
| */ |
| public String getLastEOLSequence() { |
| return lastEOL; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.Reader#ready() |
| */ |
| @Override |
| public boolean ready() throws IOException { |
| synchronized(lock) { |
| ensureOpen(); |
| |
| return nextChar < nChars || input.ready(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see java.io.Reader#skip(long) |
| */ |
| @Override |
| public long skip(long n) throws IOException { |
| if (n < 0L) { |
| throw new IllegalArgumentException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.NegativeSkip")); //$NON-NLS-1$ |
| } |
| synchronized(lock) { |
| ensureOpen(); |
| long r = n; |
| while (r > 0) { |
| if (nextChar >= nChars) { |
| fill(); |
| } |
| if (nextChar >= nChars) { |
| break; |
| } |
| long d = nChars - nextChar; |
| if (r <= d) { |
| nextChar += r; |
| r = 0; |
| break; |
| } |
| r -= d; |
| nextChar = nChars; |
| } |
| return n - r; |
| } |
| } |
| |
| /** |
| * Reads characters into a portion of an array, reading from the underlying stream if necessary. |
| * |
| * @param cbuf |
| * The character buffer into which we are to read. |
| * @param off |
| * Starting offset. |
| * @param len |
| * Number of chars to read. |
| * @return The number of read characters. |
| * @throws IOException |
| * Thrown if the stream is closed or an I/O operation fails. |
| * @see #read(char[], int, int) |
| */ |
| private int internalRead(char[] cbuf, int off, int len) throws IOException { |
| if (nextChar >= nChars) { |
| if (len >= characterBuffer.length) { |
| return input.read(cbuf, off, len); |
| } |
| fill(); |
| } |
| int readChars = -1; |
| if (nextChar < nChars) { |
| readChars = Math.min(len, nChars - nextChar); |
| System.arraycopy(characterBuffer, nextChar, cbuf, off, readChars); |
| nextChar += readChars; |
| } |
| return readChars; |
| } |
| |
| /** |
| * Make sure that the underlying stream hasn't been closed. |
| * |
| * @throws IOException |
| * Thrown if the stream has been closed. |
| */ |
| private void ensureOpen() throws IOException { |
| if (input == null) { |
| throw new IOException(AcceleoEngineMessages |
| .getString("AcceleoEvaluationContext.ClosedStream")); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Fills the input buffer, taking the mark into account if it is valid. |
| * |
| * @throws IOException |
| * Thrown if the stream is closed or an I/O operation fails. |
| */ |
| private void fill() throws IOException { |
| int n; |
| do { |
| n = input.read(characterBuffer, 0, characterBuffer.length); |
| } while (n == 0); |
| |
| if (n > 0) { |
| nChars = n; |
| nextChar = 0; |
| } |
| } |
| } |
| } |