org.eclipse.acceleo.engine.internal.evaluation.AcceleoEvaluationContext.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.acceleo.engine.internal.evaluation.AcceleoEvaluationContext.java

Source

/*******************************************************************************
 * Copyright (c) 2008, 2012 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>
     * &quot;file&quot; 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>
     * &quot;file&quot; 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 (int i = 0; i < listeners.size(); i++) {
            listeners.get(i).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;

        /**
         * 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 bufferGap = 0;
                    if (nextChar >= nChars) {
                        bufferGap = nextChar - nChars;
                        fill();
                    }
                    if (nextChar >= nChars) {
                        // Reached the end of the stream
                        if (lineBuffer != null && lineBuffer.length() > 0) {
                            line = lineBuffer.toString();
                        }
                        break;
                    }

                    nextChar = nextChar + bufferGap;

                    boolean eol = false;
                    char c = 0;
                    int i;

                    for (i = nextChar; 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;
                            } else if (nextChar != max) {
                                lastEOL = "\r"; //$NON-NLS-1$
                                nextChar++;
                            } else if (nextChar == max && DOS_LINE_SEPARATOR.equals(lastEOL)) {
                                nextChar += 2;
                            } else if (nextChar == max) {
                                nextChar++;
                            }
                        }
                    }

                    if (lineBuffer == null) {
                        lineBuffer = new StringBuilder();
                    }
                    lineBuffer.append(characterBuffer, startChar, i - startChar);
                }
            }
            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;
            }
        }
    }
}