/*******************************************************************************
 * Copyright (c) 2009, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.internal.debug;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.eclipse.osgi.framework.util.SecureAction;
import org.eclipse.osgi.service.debug.DebugTrace;

/**
 * The DebugTrace implementation for Eclipse.
 */
class EclipseDebugTrace implements DebugTrace {

	/** The system property used to specify size a trace file can grow before it is rotated */
	private static final String PROP_TRACE_SIZE_MAX = "eclipse.trace.size.max"; //$NON-NLS-1$
	/** The system property used to specify the maximum number of backup trace files to use */
	private static final String PROP_TRACE_FILE_MAX = "eclipse.trace.backup.max"; //$NON-NLS-1$
	/** The trace message for a thread stack dump */
	private final static String MESSAGE_THREAD_DUMP = "Thread Stack dump: "; //$NON-NLS-1$
	/** The trace message for a method completing with a return value */
	private final static String MESSAGE_EXIT_METHOD_WITH_RESULTS = "Exiting method {0}with result: "; //$NON-NLS-1$
	/** The trace message for a method completing with no return value */
	private final static String MESSAGE_EXIT_METHOD_NO_RESULTS = "Exiting method {0}with a void return"; //$NON-NLS-1$
	/** The trace message for a method starting with a set of arguments */
	private final static String MESSAGE_ENTER_METHOD_WITH_PARAMS = "Entering method {0}with parameters: ("; //$NON-NLS-1$
	/** The trace message for a method starting with no arguments */
	private final static String MESSAGE_ENTER_METHOD_NO_PARAMS = "Entering method {0}with no parameters"; //$NON-NLS-1$
	/** The version attribute written in the header of a new session */
	private final static String TRACE_FILE_VERSION_COMMENT = "version: "; //$NON-NLS-1$
	/** The verbose attribute written in the header of a new session */
	private final static String TRACE_FILE_VERBOSE_COMMENT = "verbose: "; //$NON-NLS-1$
	/** The version value written in the header of a new session */
	private final static String TRACE_FILE_VERSION = "1.1"; //$NON-NLS-1$
	/** The new session identifier to be written whenever a new session starts */
	private final static String TRACE_NEW_SESSION = "!SESSION "; //$NON-NLS-1$
	/** The date attribute written to the header of the trace file to show when this file was created */
	private final static String TRACE_FILE_DATE = "Time of creation: "; //$NON-NLS-1$
	/** Trace date formatter using the pattern: yyyy-MM-dd HH:mm:ss.SSS  */
	private final static SimpleDateFormat TRACE_FILE_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); //$NON-NLS-1$
	/** The comment character used by the trace file */
	private final static String TRACE_COMMENT = "#"; //$NON-NLS-1$
	/** The delimiter used to separate trace elements such as the time stamp, message, etc */
	private final static String TRACE_ELEMENT_DELIMITER = "|"; //$NON-NLS-1$
	/** The string written in place of the {@link EclipseDebugTrace#TRACE_TRACE_ELEMENT_DELIMITER} in entries */
	private final static String TRACE_ELEMENT_DELIMITER_ENCODED = "&#124;"; //$NON-NLS-1$
	/** OS-specific line separator */
	private static final String LINE_SEPARATOR;
	static {
		String s = System.getProperty("line.separator"); //$NON-NLS-1$
		LINE_SEPARATOR = s == null ? "\n" : s; //$NON-NLS-1$
	}
	/** The value written to the trace file if a null object is being traced */
	private final static String NULL_VALUE = "<null>"; //$NON-NLS-1$
	/**  */
	private final static SecureAction secureAction = AccessController.doPrivileged(SecureAction.createSecureAction());

	/******************* Tracing file attributes **************************/
	/** The default size a trace file can grow before it is rotated */
	private static final int DEFAULT_TRACE_FILE_SIZE = 1000; // The value is in KB.
	/** The default number of backup trace files */
	private static final int DEFAULT_TRACE_FILES = 10;
	/** The minimum size limit for trace file rotation */
	private static final int DEFAULT_TRACE_FILE_MIN_SIZE = 10;
	/** The extension used for log files */
	private static final String TRACE_FILE_EXTENSION = ".trace"; //$NON-NLS-1$
	/** The extension markup to use for backup log files*/
	private static final String BACKUP_MARK = ".bak_"; //$NON-NLS-1$
	/** The maximum size that a trace file should grow (0 = unlimited) */
	private int maxTraceFileSize = DEFAULT_TRACE_FILE_SIZE; // The value is in KB.
	/** The maximum number of trace files that should be saved */
	private int maxTraceFiles = DEFAULT_TRACE_FILES;
	/** The index of the currently backed-up trace file */
	private int backupTraceFileIndex = 0;

	/** An optional argument to specify the name of the class used by clients to trace messages.  If no trace class is specified
	 * then the class calling this API is assumed to be the class being traced.
	*/
	private String traceClass = null;
	/** The symbolic name of the bundle being traced */
	private String bundleSymbolicName = null;
	/** DebugOptions are used to determine if the specified bundle symbolic name + option-path has debugging enabled */
	private FrameworkDebugOptions debugOptions = null;

	private final boolean consoleLog;

	/**
	 * Construct a new EclipseDebugTrace for the specified bundle symbolic name and write messages to the specified
	 * trace file.
	 *
	 * @param bundleSymbolicName The symbolic name of the bundle being traced
	 * @param debugOptions Used to determine if the specified bundle symbolic name + option-path has tracing enabled
	 * @param traceClass The class that the client is using to perform trace API calls
	 */
	EclipseDebugTrace(final String bundleSymbolicName, final FrameworkDebugOptions debugOptions, final Class<?> traceClass) {
		this.consoleLog = "true".equals(debugOptions.getConfiguration().getConfiguration(EclipseStarter.PROP_CONSOLE_LOG)); //$NON-NLS-1$
		this.traceClass = traceClass != null ? traceClass.getName() : null;
		this.debugOptions = debugOptions;
		this.bundleSymbolicName = bundleSymbolicName;
		readLogProperties();
	}

	/**
	 * Is debugging enabled for the specified option-path
	 *
	 * @param optionPath The <i>option-path</i>
	 * @return Returns true if debugging is enabled for the specified option-path on this bundle; Otherwise false.
	 */
	private final boolean isDebuggingEnabled(final String optionPath) {
		if (optionPath == null)
			return true;
		boolean debugEnabled = false;
		if (debugOptions.isDebugEnabled()) {
			final String option = bundleSymbolicName + optionPath;
			debugEnabled = debugOptions.getBooleanOption(option, false);
		}
		return debugEnabled;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#trace(java.lang.String, java.lang.String)
	 */
	@Override
	public void trace(final String optionPath, final String message) {

		if (isDebuggingEnabled(optionPath)) {
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, message, traceClass);
			writeRecord(record);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#trace(java.lang.String, java.lang.String, java.lang.Throwable)
	 */
	@Override
	public void trace(final String optionPath, final String message, final Throwable error) {

		if (isDebuggingEnabled(optionPath)) {
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, message, error, traceClass);
			writeRecord(record);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceEntry(java.lang.String)
	 */
	@Override
	public void traceEntry(final String optionPath) {

		if (isDebuggingEnabled(optionPath)) {
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
			setMessage(record, EclipseDebugTrace.MESSAGE_ENTER_METHOD_NO_PARAMS);
			writeRecord(record);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceEntry(java.lang.String, java.lang.Object)
	 */
	@Override
	public void traceEntry(final String optionPath, final Object methodArgument) {

		if (isDebuggingEnabled(optionPath)) {
			traceEntry(optionPath, new Object[] {methodArgument});
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceEntry(java.lang.String, java.lang.Object[])
	 */
	@Override
	public void traceEntry(final String optionPath, final Object[] methodArguments) {

		if (isDebuggingEnabled(optionPath)) {
			final StringBuilder messageBuffer = new StringBuilder(EclipseDebugTrace.MESSAGE_ENTER_METHOD_WITH_PARAMS);
			if (methodArguments != null) {
				int i = 0;
				while (i < methodArguments.length) {
					if (methodArguments[i] != null) {
						messageBuffer.append(methodArguments[i].toString());
					} else {
						messageBuffer.append(EclipseDebugTrace.NULL_VALUE);
					}
					i++;
					if (i < methodArguments.length) {
						messageBuffer.append(" "); //$NON-NLS-1$
					}
				}
				messageBuffer.append(")"); //$NON-NLS-1$
			}
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
			setMessage(record, messageBuffer.toString());
			writeRecord(record);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceExit(java.lang.String)
	 */
	@Override
	public void traceExit(final String optionPath) {

		if (isDebuggingEnabled(optionPath)) {
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
			setMessage(record, EclipseDebugTrace.MESSAGE_EXIT_METHOD_NO_RESULTS);
			writeRecord(record);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceExit(java.lang.String, java.lang.Object)
	 */
	@Override
	public void traceExit(final String optionPath, final Object result) {

		if (isDebuggingEnabled(optionPath)) {
			final StringBuilder messageBuffer = new StringBuilder(EclipseDebugTrace.MESSAGE_EXIT_METHOD_WITH_RESULTS);
			if (result == null) {
				messageBuffer.append(EclipseDebugTrace.NULL_VALUE);
			} else {
				messageBuffer.append(result.toString());
			}
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, null, traceClass);
			setMessage(record, messageBuffer.toString());
			writeRecord(record);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.osgi.framework.debug.FrameworkDebugTrace#traceDumpStack(java.lang.String)
	 */
	@Override
	public void traceDumpStack(final String optionPath) {

		if (isDebuggingEnabled(optionPath)) {
			final StringBuilder messageBuffer = new StringBuilder(EclipseDebugTrace.MESSAGE_THREAD_DUMP);
			StackTraceElement[] elements = new Exception().getStackTrace();
			// the first element in this stack trace is going to be this class, so ignore it
			// the second element in this stack trace is going to either be the caller or the trace class.  Ignore it only if a traceClass is defined
			// the rest of the elements should be included in the file array
			int firstIndex = (traceClass == null) ? 1 : 2;
			int endIndex = elements.length - firstIndex;
			final StackTraceElement[] newElements = new StackTraceElement[endIndex];
			int i = 0;
			while (i < endIndex) {
				newElements[i] = elements[firstIndex];
				i++;
				firstIndex++;
			}
			messageBuffer.append(convertStackTraceElementsToString(newElements));
			final FrameworkDebugTraceEntry record = new FrameworkDebugTraceEntry(bundleSymbolicName, optionPath, messageBuffer.toString(), traceClass);
			writeRecord(record);
		}
	}

	/**
	 * Set the trace message for the specified record to include class and method information
	 * if verbose debugging is disabled.
	 *
	 * @param record The {@link FrameworkDebugTraceEntry} containing the information to persist to the trace file.
	 * @param originalMessage The original tracing message
	 */
	private final void setMessage(final FrameworkDebugTraceEntry record, final String originalMessage) {

		String argument = null;
		if (!debugOptions.isVerbose()) {
			final StringBuilder classMethodName = new StringBuilder(record.getClassName());
			classMethodName.append("#"); //$NON-NLS-1$
			classMethodName.append(record.getMethodName());
			classMethodName.append(" "); //$NON-NLS-1$
			argument = classMethodName.toString();
		} else {
			argument = ""; //$NON-NLS-1$
		}
		String newMessage = MessageFormat.format(originalMessage, new Object[] {argument});
		record.setMessage(newMessage);
	}

	/**
	 * Utility method to convert an array of StackTraceElement objects to form a String representation of a stack dump
	 *
	 * @param elements
	 *            The array of StackTraceElement objects
	 * @return A String of the stack dump produced by the list of elements
	 */
	private final String convertStackTraceElementsToString(final StackTraceElement[] elements) {

		final StringBuilder buffer = new StringBuilder();
		if (elements != null) {
			buffer.append("java.lang.Throwable: "); //$NON-NLS-1$
			buffer.append(EclipseDebugTrace.LINE_SEPARATOR);
			int i = 0;
			while (i < elements.length) {
				if (elements[i] != null) {
					buffer.append("\tat "); //$NON-NLS-1$
					buffer.append(elements[i].toString());
					buffer.append(EclipseDebugTrace.LINE_SEPARATOR);
				}
				i++;
			}
		}
		return buffer.toString();
	}

	/**
	 * Write the specified FrameworkTraceEntry to trace file
	 *
	 * @param entry The FrameworkTraceEntry to write to the log file.
	 */
	private void writeRecord(final FrameworkDebugTraceEntry entry) {

		if (entry != null) {
			synchronized (debugOptions.getWriteLock()) {
				final File tracingFile = debugOptions.getFile(); // the tracing file may be null if it has not been set
				Writer traceWriter = null;
				try {
					// check to see if the file should be rotated
					checkTraceFileSize(tracingFile, entry.getTimestamp());
					// open the trace file
					traceWriter = openWriter(tracingFile);
					if (debugOptions.newSession()) {
						writeSession(traceWriter, entry.getTimestamp());
					}
					writeMessage(traceWriter, entry);
					// flush the writer
					traceWriter.flush();
				} catch (Exception ex) {
					// any exceptions during tracing should be caught
					System.err.println("An exception occurred while writing to the platform trace file: ");//$NON-NLS-1$
					ex.printStackTrace(System.err);
				} finally {
					// close the trace writer
					closeWriter(traceWriter);
				}
			}
		}
	}

	/**
	 * Reads the PROP_TRACE_SIZE_MAX and PROP_TRACE_FILE_MAX properties.
	 */
	private void readLogProperties() {

		String newMaxTraceFileSize = debugOptions.getConfiguration().getConfiguration(PROP_TRACE_SIZE_MAX);
		if (newMaxTraceFileSize != null) {
			maxTraceFileSize = Integer.parseInt(newMaxTraceFileSize);
			if (maxTraceFileSize != 0 && maxTraceFileSize < DEFAULT_TRACE_FILE_MIN_SIZE) {
				// If the value is '0', then it means no size limitation.
				// Also, make sure no inappropriate(too small) assigned value.
				maxTraceFileSize = DEFAULT_TRACE_FILE_MIN_SIZE;
			}
		}

		String newMaxLogFiles = debugOptions.getConfiguration().getConfiguration(PROP_TRACE_FILE_MAX);
		if (newMaxLogFiles != null) {
			maxTraceFiles = Integer.parseInt(newMaxLogFiles);
			if (maxTraceFiles < 1) {
				// Make sure no invalid assigned value. (at least >= 1)
				maxTraceFiles = DEFAULT_TRACE_FILES;
			}
		}
	}

	/**
	 * Checks the trace file size.  If the file size reaches the limit then the trace file is rotated.
	 *
	 * @param traceFile The tracing file
	 * @param timestamp the timestamp for the session; this is the same timestamp as the first entry
	 * @return false if an error occurred trying to rotate the trace file
	 */
	private boolean checkTraceFileSize(final File traceFile, long timestamp) {

		// 0 file size means there is no size limit
		boolean isBackupOK = true;
		if (maxTraceFileSize > 0) {
			if ((traceFile != null) && traceFile.exists()) {
				if ((traceFile.length() >> 10) > maxTraceFileSize) { // Use KB as file size unit.
					final String traceFileName = traceFile.getAbsolutePath();

					// Delete old backup file that will be replaced.
					String backupFilename = ""; //$NON-NLS-1$
					if (traceFileName.toLowerCase().endsWith(TRACE_FILE_EXTENSION)) {
						backupFilename = traceFileName.substring(0, traceFileName.length() - TRACE_FILE_EXTENSION.length()) + BACKUP_MARK + backupTraceFileIndex + TRACE_FILE_EXTENSION;
					} else {
						backupFilename = traceFileName + BACKUP_MARK + backupTraceFileIndex;
					}
					final File backupFile = new File(backupFilename);
					if (backupFile.exists()) {
						if (!backupFile.delete()) {
							System.err.println("Error when trying to delete old trace file: " + backupFile.getName());//$NON-NLS-1$
							if (backupFile.renameTo(new File(backupFile.getAbsolutePath() + System.currentTimeMillis()))) {
								System.err.println("So we rename it to filename: " + backupFile.getName()); //$NON-NLS-1$
							} else {
								System.err.println("And we also cannot rename it!"); //$NON-NLS-1$
								isBackupOK = false;
							}
						}
					}

					// Rename current log file to backup one.
					boolean isRenameOK = traceFile.renameTo(backupFile);
					if (!isRenameOK) {
						System.err.println("Error when trying to rename trace file to backup one."); //$NON-NLS-1$
						isBackupOK = false;
					}
					/*
					 * Write a header to new log file stating that this new file is a continuation file.
					 * This method should already be called with the file lock set so we should be safe
					 * to update it here.
					*/
					Writer traceWriter = null;
					try {
						traceWriter = openWriter(traceFile);
						writeComment(traceWriter, "This is a continuation of trace file " + backupFile.getAbsolutePath()); //$NON-NLS-1$
						writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERSION_COMMENT + EclipseDebugTrace.TRACE_FILE_VERSION);
						writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERBOSE_COMMENT + debugOptions.isVerbose());
						writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_DATE + getFormattedDate(timestamp));
						traceWriter.flush();
					} catch (IOException ioEx) {
						ioEx.printStackTrace();
					} finally {
						closeWriter(traceWriter);
					}
					backupTraceFileIndex = (++backupTraceFileIndex) % maxTraceFiles;
				}
			}
		}
		return isBackupOK;
	}

	/**
	 * Writes a comment to the trace file
	 *
	 * @param traceWriter the trace writer
	 * @param comment the comment to be written to the trace file
	 * @throws IOException If an error occurs while writing the comment
	 */
	private void writeComment(final Writer traceWriter, final String comment) throws IOException {

		StringBuilder commentText = new StringBuilder(EclipseDebugTrace.TRACE_COMMENT);
		commentText.append(" "); //$NON-NLS-1$
		commentText.append(comment);
		commentText.append(EclipseDebugTrace.LINE_SEPARATOR);
		traceWriter.write(commentText.toString());
	}

	/**
	 * Accessor to retrieve the time stamp in a formatted manner.
	 *
	 * @return A formatted time stamp based on the {@link EclipseDebugTrace#TRACE_FILE_DATE_FORMATTER} formatter
	 */
	private final String getFormattedDate(long timestamp) {

		return EclipseDebugTrace.TRACE_FILE_DATE_FORMATTER.format(new Date(timestamp));
	}

	/**
	 * Accessor to retrieve the text of a {@link Throwable} in a formatted manner so that it can be written to the
	 * trace file.
	 *
	 * @param error The {@lnk Throwable} to format
	 * @return The complete text of a {@link Throwable} as a {@link String} or null if the input error is null.
	 */
	private final String getFormattedThrowable(Throwable error) {

		String result = null;
		if (error != null) {
			PrintStream throwableStream = null;
			try {
				ByteArrayOutputStream throwableByteOutputStream = new ByteArrayOutputStream();
				throwableStream = new PrintStream(throwableByteOutputStream, false);
				error.printStackTrace(throwableStream);
				result = encodeText(throwableByteOutputStream.toString());
			} finally {
				if (throwableStream != null) {
					throwableStream.close();
				}
			}
		}
		return result;
	}

	/**
	 * Writes header information to a new trace file
	 *
	 * @param traceWriter the trace writer
	 * @param timestamp the timestamp for the session; this is the same timestamp as the first entry
	 * @throws IOException If an error occurs while writing this session information
	 */
	private void writeSession(final Writer traceWriter, long timestamp) throws IOException {

		writeComment(traceWriter, EclipseDebugTrace.TRACE_NEW_SESSION + this.getFormattedDate(timestamp));
		writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERSION_COMMENT + EclipseDebugTrace.TRACE_FILE_VERSION);
		writeComment(traceWriter, EclipseDebugTrace.TRACE_FILE_VERBOSE_COMMENT + debugOptions.isVerbose());
		writeComment(traceWriter, "The following option strings are specified for this debug session:"); //$NON-NLS-1$
		final String[] allOptions = debugOptions.getAllOptions();
		for (String allOption : allOptions) {
			writeComment(traceWriter, "\t" + allOption); //$NON-NLS-1$
		}
	}

	/**
	 * Writes the specified trace entry object to the trace file using the
	 * {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER} as the delimiter between
	 * each element of the entry.
	 *
	 * @param traceWriter the trace writer
	 * @param entry The trace entry object to write to the trace file
	 * @throws IOException If an error occurs while writing this message
	 */
	private void writeMessage(final Writer traceWriter, final FrameworkDebugTraceEntry entry) throws IOException {

		final StringBuilder message = new StringBuilder(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
		message.append(" "); //$NON-NLS-1$
		message.append(encodeText(entry.getThreadName()));
		message.append(" "); //$NON-NLS-1$
		message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
		message.append(" "); //$NON-NLS-1$
		message.append(this.getFormattedDate(entry.getTimestamp()));
		message.append(" "); //$NON-NLS-1$
		message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
		message.append(" "); //$NON-NLS-1$
		if (!debugOptions.isVerbose()) {
			// format the trace entry for quiet tracing: only the thread name, timestamp, trace message, and exception (if necessary)
			message.append(encodeText(entry.getMessage()));
		} else {
			// format the trace entry for verbose tracing
			message.append(entry.getBundleSymbolicName());
			message.append(" "); //$NON-NLS-1$
			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
			message.append(" "); //$NON-NLS-1$
			message.append(encodeText(entry.getOptionPath()));
			message.append(" "); //$NON-NLS-1$
			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
			message.append(" "); //$NON-NLS-1$
			message.append(entry.getClassName());
			message.append(" "); //$NON-NLS-1$
			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
			message.append(" "); //$NON-NLS-1$
			message.append(entry.getMethodName());
			message.append(" "); //$NON-NLS-1$
			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
			message.append(" "); //$NON-NLS-1$
			message.append(entry.getLineNumber());
			message.append(" "); //$NON-NLS-1$
			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
			message.append(" "); //$NON-NLS-1$
			message.append(encodeText(entry.getMessage()));
		}
		if (entry.getThrowable() != null) {
			message.append(" "); //$NON-NLS-1$
			message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
			message.append(" "); //$NON-NLS-1$
			message.append(this.getFormattedThrowable(entry.getThrowable()));
		}
		message.append(" "); //$NON-NLS-1$
		message.append(EclipseDebugTrace.TRACE_ELEMENT_DELIMITER);
		message.append(EclipseDebugTrace.LINE_SEPARATOR);
		// write the message
		if ((traceWriter != null) && (message != null)) {
			traceWriter.write(message.toString());
		}
	}

	/**
	 * Encodes the specified string to replace any occurrence of the {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER}
	 * string with the {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER_ENCODED}
	 * string.  This can be used to ensure that the delimiter character does not break parsing when
	 * the entry text contains the delimiter character.
	 *
	 * @param inputString The original string to be written to the trace file.
	 * @return The original input string with all occurrences of
	 * {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER} replaced with
	 * {@link EclipseDebugTrace#TRACE_ELEMENT_DELIMITER_ENCODED}. A <code>null</code> value will be
	 * returned if the input string is <code>null</code>.
	 */
	private static String encodeText(final String inputString) {
		if (inputString == null || inputString.indexOf(TRACE_ELEMENT_DELIMITER) < 0)
			return inputString;
		final StringBuilder tempBuffer = new StringBuilder(inputString);
		int currentIndex = tempBuffer.indexOf(TRACE_ELEMENT_DELIMITER);
		while (currentIndex >= 0) {
			tempBuffer.replace(currentIndex, currentIndex + TRACE_ELEMENT_DELIMITER.length(), TRACE_ELEMENT_DELIMITER_ENCODED);
			currentIndex = tempBuffer.indexOf(TRACE_ELEMENT_DELIMITER);
		}
		return tempBuffer.toString();
	}

	/**
	 * Returns a Writer for the given OutputStream
	 * @param output an OutputStream to use for the Writer
	 * @return A Writer for the given OutputStream
	 */
	private Writer logForStream(OutputStream output) {
		return new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
	}

	/**
	 * Creates the trace writer.
	 * If the tracing file is null then the writer will use System.out to print any messages.
	 *
	 * @param traceFile The tracing file
	 * @return Returns a new Writer object
	 */
	private Writer openWriter(final File traceFile) {
		OutputStream out = null;
		if (traceFile != null) {
			try {
				out = secureAction.getFileOutputStream(traceFile, true);
			} catch (IOException ioEx) {
				// ignore and fall back to system.out; but print error message to indicate what happened
				System.err.println("Unable to open trace file: " + traceFile + ": " + ioEx.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		if (out == null) {
			out = new FilterOutputStream(System.out) {
				/**
				 * @throws IOException
				 */
				@Override
				public void close() throws IOException {
					// We don't want to close System.out
				}

				@Override
				public void write(byte[] var0, int var1, int var2) throws IOException {
					this.out.write(var0, var1, var2);
				}
			};
		} else if (consoleLog) {
			out = new FilterOutputStream(out) {
				@Override
				public void write(int b) throws IOException {
					System.out.write(b);
					out.write(b);
				}

				@Override
				public void write(byte[] b) throws IOException {
					System.out.write(b);
					out.write(b);
				}

				@Override
				public void write(byte[] b, int off, int len) throws IOException {
					System.out.write(b, off, len);
					out.write(b, off, len);
				}
			};
		}
		return logForStream(out);
	}

	/**
	 * Close the trace writer
	 *
	 * @param traceWriter The trace writer
	 */
	private void closeWriter(Writer traceWriter) {

		if (traceWriter != null) {
			try {
				traceWriter.close();
			} catch (IOException ioEx) {
				// we cannot log here; just print the stacktrace.
				ioEx.printStackTrace();
			}
			traceWriter = null;
		}
	}
}