Bug 507664 - IOConsoleOutputStream does not handle multi-byte characters
at buffer boundaries correctly

Change-Id: Iee66ccfd74f93760b1c0e26c7114d4b48cee6324
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java
index 3939c4d..b683fe2 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java
@@ -55,6 +55,31 @@
 			TestHelper.waitForJobs();
 			TestCase.assertEquals("whole test string should be written", testString, document.get()); //$NON-NLS-1$
 		}
+		TestHelper.waitForJobs();
+		// after closing the stream, the document content should still be the
+		// same
+		TestCase.assertEquals("closing the stream should not alter the document", testString, document.get()); //$NON-NLS-1$
+	}
+
+	public void testConsoleOutputStreamLastR() throws IOException, InterruptedException {
+		String testString = "a\r"; //$NON-NLS-1$
+		byte[] testStringBuffer = testString.getBytes(StandardCharsets.UTF_8);
+		TestCase.assertEquals("Test string \"" + testString + "\" should consist of 2 UTF-8 bytes", 2, testStringBuffer.length); //$NON-NLS-1$ //$NON-NLS-2$
+		MessageConsole console = new MessageConsole("Test Console", //$NON-NLS-1$
+				IConsoleConstants.MESSAGE_CONSOLE_TYPE, null, StandardCharsets.UTF_8.name(), true);
+		IDocument document = console.getDocument();
+		TestHelper.waitForJobs();
+		TestCase.assertEquals("Document should be empty", "", document.get()); //$NON-NLS-1$ //$NON-NLS-2$
+		try (IOConsoleOutputStream outStream = console.newOutputStream()) {
+			outStream.write(testStringBuffer);
+			// everything but pending \r should be written
+			TestHelper.waitForJobs();
+			TestCase.assertEquals("First char should be written", testString.substring(0, 1), document.get()); //$NON-NLS-1$
+		}
+		TestHelper.waitForJobs();
+		// after closing the stream, the document content should still be the
+		// same
+		TestCase.assertEquals("closing the stream should write the pending \\r", testString, document.get()); //$NON-NLS-1$
 	}
 
 }
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java
index 7e78ddf..8ee4707 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java
@@ -168,10 +168,15 @@
 			// Closeable#close() has no effect if already closed
 			return;
         }
+		StringBuilder builder = new StringBuilder();
         if (prependCR) { // force writing of last /r
             prependCR = false;
-            notifyParitioner("\r"); //$NON-NLS-1$
+			builder.append('\r');
         }
+		this.decoder.finish(builder);
+		if (builder.length() > 0) {
+			notifyParitioner(builder.toString());
+		}
         console.streamClosed(this);
         closed = true;
         partitioner = null;
@@ -193,7 +198,7 @@
      * @see java.io.OutputStream#write(byte[], int, int)
      */
     @Override
-	public void write(byte[] b, int off, int len) throws IOException {
+	public synchronized void write(byte[] b, int off, int len) throws IOException {
 		StringBuilder builder = new StringBuilder();
 		this.decoder.decode(builder, b, off, len);
 		encodedWrite(builder.toString());
@@ -317,10 +322,12 @@
 	 * @throws IOException if the stream is closed
 	 * @since 3.7
 	 */
-	public void setCharset(Charset charset) throws IOException {
+	public synchronized void setCharset(Charset charset) throws IOException {
 		StringBuilder builder = new StringBuilder();
 		this.decoder.finish(builder);
-		this.encodedWrite(builder.toString());
+		if (builder.length() > 0) {
+			this.encodedWrite(builder.toString());
+		}
 		this.decoder = new StreamDecoder(charset);
     }
 
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java
index dde14fd..aeb0179 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java
@@ -29,6 +29,7 @@
 	private final CharsetDecoder decoder;
 	private final ByteBuffer inputBuffer;
 	private final CharBuffer outputBuffer;
+	private boolean finished;
 
 	public StreamDecoder(Charset charset) {
 		this.decoder = charset.newDecoder();
@@ -37,6 +38,7 @@
 		this.inputBuffer = ByteBuffer.allocate(StreamDecoder.BUFFER_SIZE);
 		this.inputBuffer.flip();
 		this.outputBuffer = CharBuffer.allocate(StreamDecoder.BUFFER_SIZE);
+		this.finished = false;
 	}
 
 	private void consume(StringBuilder consumer) {
@@ -45,15 +47,15 @@
 		this.outputBuffer.clear();
 	}
 
-	private void internalDecode(StringBuilder consumer, byte[] buffer, int offset, int length, boolean last) {
+	private void internalDecode(StringBuilder consumer, byte[] buffer, int offset, int length) {
 		assert (offset >= 0);
 		assert (length >= 0);
 		int position = offset;
 		int end = offset + length;
 		assert (end <= buffer.length);
-		boolean finished = false;
+		boolean finishedReading = false;
 		do {
-			CoderResult result = this.decoder.decode(this.inputBuffer, this.outputBuffer, last);
+			CoderResult result = this.decoder.decode(this.inputBuffer, this.outputBuffer, false);
 			if (result.isOverflow()) {
 				this.consume(consumer);
 			} else if (result.isUnderflow()) {
@@ -65,23 +67,28 @@
 					this.inputBuffer.put(buffer, position, read);
 					position += read;
 				} else {
-					finished = true;
+					finishedReading = true;
 				}
 				this.inputBuffer.flip();
 			} else {
 				assert false;
 			}
-		} while (!finished);
+		} while (!finishedReading);
 	}
 
 	public void decode(StringBuilder consumer, byte[] buffer, int offset, int length) {
-		this.internalDecode(consumer, buffer, offset, length, false);
+		this.internalDecode(consumer, buffer, offset, length);
 		this.consume(consumer);
 	}
 
 	public void finish(StringBuilder consumer) {
-		this.internalDecode(consumer, new byte[0], 0, 0, true);
+		if (this.finished) {
+			return;
+		}
+		this.finished = true;
 		CoderResult result;
+		result = this.decoder.decode(this.inputBuffer, this.outputBuffer, true);
+		assert (result.isOverflow() || result.isUnderflow());
 		do {
 			result = this.decoder.flush(this.outputBuffer);
 			if (result.isOverflow()) {