NEW - bug 271356: Capture logged error messages
https://bugs.eclipse.org/bugs/show_bug.cgi?id=271356
diff --git a/plugins/org.eclipse.epp.usagedata.gathering/plugin.xml b/plugins/org.eclipse.epp.usagedata.gathering/plugin.xml
index e81ba3f..ecde7f3 100644
--- a/plugins/org.eclipse.epp.usagedata.gathering/plugin.xml
+++ b/plugins/org.eclipse.epp.usagedata.gathering/plugin.xml
@@ -22,6 +22,9 @@
       <monitor
             class="org.eclipse.epp.usagedata.internal.gathering.monitors.SystemInfoMonitor">
       </monitor>
+      <monitor
+            class="org.eclipse.epp.usagedata.internal.gathering.monitors.LogMonitor">
+      </monitor>
    </extension>
    <extension
          point="org.eclipse.core.runtime.preferences">
diff --git a/plugins/org.eclipse.epp.usagedata.gathering/src/org/eclipse/epp/usagedata/internal/gathering/monitors/LogMonitor.java b/plugins/org.eclipse.epp.usagedata.gathering/src/org/eclipse/epp/usagedata/internal/gathering/monitors/LogMonitor.java
new file mode 100644
index 0000000..e9718df
--- /dev/null
+++ b/plugins/org.eclipse.epp.usagedata.gathering/src/org/eclipse/epp/usagedata/internal/gathering/monitors/LogMonitor.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2009 The Eclipse Foundation.
+ * 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:
+ *    The Eclipse Foundation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.epp.usagedata.internal.gathering.monitors;
+
+import org.eclipse.core.runtime.ILogListener;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.epp.usagedata.internal.gathering.services.UsageDataService;
+
+/**
+ * The {@link LogMonitor} class records messages that are
+ * written to the log. Only messages with a severity of {@link IStatus#ERROR}
+ * are recorded.
+ *
+ * @see IStatus#ERROR
+ * @see Platform#addLogListener(ILogListener)
+ */
+public class LogMonitor implements UsageMonitor {
+	
+	private static final String WHAT_ERROR = "error";
+	private static final String KIND_LOG = "log";
+	
+	private UsageDataService usageDataService;
+	ILogListener listener = new ILogListener() {
+		public void logging(IStatus status, String plugin) {
+			if (status.getSeverity() != IStatus.ERROR) return;
+			usageDataService.recordEvent(WHAT_ERROR, KIND_LOG, status.getMessage(), null);
+		}
+	};
+	
+	public void startMonitoring(UsageDataService usageDataService) {
+		this.usageDataService = usageDataService;
+		Platform.addLogListener(listener);
+	}
+
+	public void stopMonitoring() {
+		Platform.removeLogListener(listener);
+	}
+
+}
diff --git a/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/UsageDataRecorderUtils.java b/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/UsageDataRecorderUtils.java
index ff85090..88eb0fd 100644
--- a/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/UsageDataRecorderUtils.java
+++ b/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/UsageDataRecorderUtils.java
@@ -1,17 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2009 The Eclipse Foundation.
+ * 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:
+ *    The Eclipse Foundation - initial API and implementation
+ *******************************************************************************/
 package org.eclipse.epp.usagedata.internal.recording;
 
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Writer;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEvent;
 
 public class UsageDataRecorderUtils {
-
-	private UsageDataRecorderUtils() {
-	}
-
-	public static void writeHeader(FileWriter writer) throws IOException {
+	
+	/**
+	 * Write a header onto the {@link Writer}.
+	 * 
+	 * @param writer
+	 *            a {@link Writer}. Must not be <code>null</code>.
+	 * @throws IOException
+	 *             if writing to the {@link Writer} fails.
+	 */
+	public static void writeHeader(Writer writer) throws IOException {
 		writer.write("what");
 		writer.write(",");
 		writer.write("kind");
@@ -27,12 +44,17 @@
 	}
 	
 	/**
-	 * Dump the event on the writer. This method assumes
-	 * exclusive access to the writer.
+	 * Dump the event on the writer. This method assumes exclusive access to the
+	 * writer.
 	 * 
-	 * @param writer target for the event information.
-	 * @param event event to write.
+	 * @param writer
+	 *            target for the event information. Must not be
+	 *            <code>null</code>.
+	 * @param event
+	 *            event to write. Must not be <code>null</code>.
+	 * 
 	 * @throws IOException
+	 *             if writing to the {@link Writer} fails.
 	 */
 	public static void writeEvent(Writer writer, UsageDataEvent event) throws IOException {
 		writer.write(event.what);
@@ -43,9 +65,99 @@
 		writer.write(",");
 		writer.write(event.bundleVersion != null ? event.bundleVersion : "");
 		writer.write(",");
-		writer.write(event.description != null ? event.description : "");
+		writer.write(event.description != null ? encode(event.description) : "");
 		writer.write(",");
 		writer.write(String.valueOf(event.when));
 		writer.write("\n");
 	}
+
+	/**
+	 * This method encodes the description so that it can be successfully parsed
+	 * as a single entry by a CSV parser. This takes care of most of the
+	 * escape characters to ensure that the output gets dumped onto a single
+	 * line. We probably take care of more escape characters than we really
+	 * need to, but that's better than the opposite...
+	 * <p>
+	 * The escaped characters should only be an issue in the rare case when
+	 * a status message contains them. Most of our monitors do not generate
+	 * text that contains them.
+	 * 
+	 * @param description
+	 *            a {@link String}. Must not be <code>null</code>.
+	 * 
+	 * @return a {@link String}.
+	 */
+	public static String encode(String description) {
+		StringBuilder builder = new StringBuilder();
+		builder.append('"');
+		for(int index=0;index<description.length();index++) {
+			char next = description.charAt(index);
+			switch (next) {
+			case '"' : 
+				builder.append('"');
+				builder.append(next);
+				break;
+			case '\\' :
+				builder.append("\\\\");
+				break;
+			case '\n' :
+				builder.append("\\n");
+				break;
+			case '\r' :
+				builder.append("\\r");
+				break;
+			case '\b' :
+				builder.append("\\b");
+				break;
+			case '\t' :
+				builder.append("\\t");
+				break;
+			case '\f' :
+				builder.append("\\f");
+				break;				
+			default :
+				builder.append(next);
+			}
+		}
+		builder.append('"');
+		return builder.toString();
+	}
+
+	/**
+	 * Split the String parameter into substrings. The parameter is assumed to
+	 * be in CSV format. Comma separators are assumed. An entry that starts and
+	 * ends with double-quotes may contain commas or escaped double-quotes.
+	 * Double-quotes are escaped by putting two one-after-the-other. This method
+	 * assumes that there are no extraneous white spaces (i.e. leading or
+	 * trailing) in the input.
+	 * <p>
+	 * Note that we don't worry about trying to re-translate escaped characters
+	 * back into their unescaped form. We assume that this method is used exclusively
+	 * for displaying events in a preview pane, or for applying filters; we don't
+	 * need to translate the escaped characters in either of these cases.
+	 * <p>
+	 * The value: "first,\"\"\"second\"\", third\",fourth" will be parsed into
+	 * three strings: "first", "\"second\", third", and "fourth".
+	 * 
+	 * @param line
+	 *            a {@link String}. Must not be <code>null</code>.
+	 * 
+	 * @return an array of {@link String}s.
+	 */
+	public static String[] splitLine(String line) {
+		List<String> strings = new java.util.ArrayList<String>(); 
+		Matcher matcher = Pattern.compile("(\"([^\"]|\"\")*\"|[^,]*)(,|$)").matcher(line);
+		while (matcher.find()) {
+			String string = matcher.group();
+			// Remove leading commas.
+			string = string.replaceAll(",$", "");
+			// Remove optional leading and trailing double-quotes.
+			string = string.replaceAll("^?\"(.*)\"$", "$1");
+			// Replace double double-quotes with a single double-quote
+			string = string.replaceAll("\"\"", "\"");
+			
+			strings.add( string ); 
+		}
+		return (String[]) strings.toArray(new String[strings.size()]);
+	}
 }
diff --git a/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/uploading/UsageDataFileReader.java b/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/uploading/UsageDataFileReader.java
index 8f8c349..7b15998 100644
--- a/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/uploading/UsageDataFileReader.java
+++ b/plugins/org.eclipse.epp.usagedata.recording/src/org/eclipse/epp/usagedata/internal/recording/uploading/UsageDataFileReader.java
@@ -22,6 +22,7 @@
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEvent;
+import org.eclipse.epp.usagedata.internal.recording.UsageDataRecorderUtils;
 
 public class UsageDataFileReader {
 	public interface Iterator {
@@ -60,7 +61,7 @@
 	}
 
 	private UsageDataEvent createUsageDataEvent(String line) {
-		String[] tokens = line.split("\\,");
+		String[] tokens = UsageDataRecorderUtils.splitLine(line);
 		UsageDataEvent usageDataEvent = new UsageDataEvent(tokens[0], tokens[1], tokens[4], tokens[2], tokens[3], Long.valueOf(tokens[5]));
 		return usageDataEvent;
 	}