--- old/src/java.base/share/classes/java/lang/String.java 2014-09-25 15:32:01.589805013 +0100 +++ new/src/java.base/share/classes/java/lang/String.java 2014-09-25 15:32:01.481805018 +0100 @@ -28,6 +28,7 @@ import java.io.ObjectStreamField; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -448,7 +449,38 @@ if (charset == null) throw new NullPointerException("charset"); checkBounds(bytes, offset, length); - this.value = StringCoding.decode(charset, bytes, offset, length); + this.value = StringCoding.decode(charset, bytes, offset, length); + } + + /** + * Constructs a new {@code String} by decoding the specified + * {@linkplain java.nio.ByteBuffer byte buffer} using the specified + * {@linkplain java.nio.charset.Charset charset}. + * The length of the new {@code String} is a function of the charset, and + * hence may not be equal to the length of the subarray. + * + *

This method always replaces malformed-input and unmappable-character + * sequences with this charset's default replacement string. The {@link + * java.nio.charset.CharsetDecoder} class should be used when more control + * over the decoding process is required. + * + * @param bytes + * The bytes to be decoded into characters + * + * @param charset + * The {@linkplain java.nio.charset.Charset charset} to be used to + * decode the {@code bytes} + * + * @throws IndexOutOfBoundsException + * If the {@code offset} and {@code length} arguments index + * characters outside the bounds of the {@code bytes} array + * + * @since 1.9 + */ + public String(ByteBuffer bytes, Charset charset) { + if (charset == null) + throw new NullPointerException("charset"); + this.value = StringCoding.decode(charset, bytes); } /** @@ -925,9 +957,56 @@ * @since 1.6 */ public byte[] getBytes(Charset charset) { - if (charset == null) throw new NullPointerException(); + Objects.requireNonNull(charset); return StringCoding.encode(charset, value, 0, value.length); } + + /** + *

Encodes this {@code String} into a sequence of bytes using the given + * {@linkplain java.nio.charset.Charset charset}, storing the result into a + * byte array that has been passed as an argument. + * + * @param destBuffer + * The destination array + * + * @param destOffset + * The start offset in the destination array + * + * @param charset + * The {@linkplain java.nio.charset.Charset} to be used to encode + * the {@code String} + * + * @return the number of bytes copied + * + * @since 1.9 + */ + public int getBytes(final byte[] destBuffer, final int destOffset, final Charset charset) { + Objects.requireNonNull(destBuffer); + Objects.requireNonNull(charset); + return StringCoding.encode(charset, value, 0, value.length, destBuffer, destOffset); + } + + /** + *

Encodes this {@code String} into a sequence of bytes using the given + * {@linkplain java.nio.charset.Charset charset}, storing the result into a + * {@linkplain java.nio.ByteBuffer byte buffer} that has been passed as an argument. + * + * @param destBuffer + * The destination {@linkplain java.nio.ByteBuffer} + * + * @param charset + * The {@linkplain java.nio.charset.Charset} to be used to encode + * the {@code String} + * + * @return the number of bytes copied + * + * @since 1.9 + */ + public int getBytes(final ByteBuffer destBuffer, final Charset charset) { + Objects.requireNonNull(destBuffer); + Objects.requireNonNull(charset); + return StringCoding.encode(charset, value, 0, value.length, destBuffer); + } /** * Encodes this {@code String} into a sequence of bytes using the --- old/src/java.base/share/classes/java/lang/StringCoding.java 2014-09-25 15:32:01.993804994 +0100 +++ new/src/java.base/share/classes/java/lang/StringCoding.java 2014-09-25 15:32:01.885804999 +0100 @@ -215,36 +215,79 @@ char[] ca = new char[en]; if (len == 0) return ca; - boolean isTrusted = false; - if (System.getSecurityManager() != null) { - if (!(isTrusted = (cs.getClass().getClassLoader0() == null))) { - ba = Arrays.copyOfRange(ba, off, off + len); - off = 0; - } + + boolean isTrusted = isTrusted(cs); + if (!isTrusted) { + ba = Arrays.copyOfRange(ba, off, off + len); + off = 0; } - cd.onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE) - .reset(); + + setupDecoder(cd); + if (cd instanceof ArrayDecoder) { int clen = ((ArrayDecoder)cd).decode(ba, off, len, ca); return safeTrim(ca, clen, cs, isTrusted); } else { ByteBuffer bb = ByteBuffer.wrap(ba, off, len); - CharBuffer cb = CharBuffer.wrap(ca); - try { - CoderResult cr = cd.decode(bb, cb, true); - if (!cr.isUnderflow()) - cr.throwException(); - cr = cd.flush(cb); - if (!cr.isUnderflow()) - cr.throwException(); - } catch (CharacterCodingException x) { - // Substitution is always enabled, - // so this shouldn't happen - throw new Error(x); - } - return safeTrim(ca, cb.position(), cs, isTrusted); + return performDecode(cs, isTrusted, ca, cd, bb); + } + } + + static char[] decode(Charset cs, ByteBuffer bb) { + // See comment at top of decode(Charset,byte[],int,int) + CharsetDecoder cd = cs.newDecoder(); + int len = bb.limit() - bb.position(); + int en = scale(len, cd.maxCharsPerByte()); + char[] ca = new char[en]; + if (len == 0) + return ca; + + boolean isTrusted = isTrusted(cs); + if (!isTrusted) { + // setup the bytebuffer for copying + + // copy the bytebuffer + ByteBuffer originalByteBuffer = bb; + bb = ByteBuffer.allocateDirect(len) + .put(originalByteBuffer); + bb.position(0); } + + setupDecoder(cd); + + return performDecode(cs, isTrusted, ca, cd, bb); + } + + private static boolean isTrusted(Charset cs) { + return System.getSecurityManager() != null + // null classloader means the class is on the boot classpath + && cs.getClass().getClassLoader0() == null; + } + + private static void setupDecoder(CharsetDecoder cd) { + cd.onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .reset(); + } + + static char[] performDecode( + Charset cs, boolean isTrusted, char[] ca, + CharsetDecoder cd, ByteBuffer bb) { + + CharBuffer cb = CharBuffer.wrap(ca); + try { + CoderResult cr = cd.decode(bb, cb, true); + if (!cr.isUnderflow()) + cr.throwException(); + cr = cd.flush(cb); + if (!cr.isUnderflow()) + cr.throwException(); + } catch (CharacterCodingException x) { + // Substitution is always enabled, + // so this shouldn't happen + throw new Error(x); + } + return safeTrim(ca, cb.position(), cs, isTrusted); } static char[] decode(byte[] ba, int off, int len) { @@ -350,16 +393,12 @@ byte[] ba = new byte[en]; if (len == 0) return ba; - boolean isTrusted = false; - if (System.getSecurityManager() != null) { - if (!(isTrusted = (cs.getClass().getClassLoader0() == null))) { - ca = Arrays.copyOfRange(ca, off, off + len); - off = 0; - } + boolean isTrusted = isTrusted(cs); + if (!isTrusted) { + ca = Arrays.copyOfRange(ca, off, off + len); + off = 0; } - ce.onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE) - .reset(); + setupEncoder(ce); if (ce instanceof ArrayEncoder) { int blen = ((ArrayEncoder)ce).encode(ca, off, len, ba); return safeTrim(ba, blen, cs, isTrusted); @@ -379,6 +418,34 @@ return safeTrim(ba, bb.position(), cs, isTrusted); } } + + static int encode(Charset cs, char[] ca, int off, int len, byte[] destBuffer, int destOffset) { + ByteBuffer bb = ByteBuffer.wrap(destBuffer, destOffset, destBuffer.length - destOffset); + return encode(cs, ca, off, len, bb); + } + + static int encode(Charset cs, char[] ca, int off, int len, ByteBuffer destBuffer) { + CharsetEncoder ce = cs.newEncoder(); + final int originalPosition = destBuffer.position(); + CharBuffer cb = CharBuffer.wrap(ca, off, len); + try { + CoderResult cr = ce.encode(cb, destBuffer, true); + if (!cr.isUnderflow()) + cr.throwException(); + cr = ce.flush(destBuffer); + if (!cr.isUnderflow()) + cr.throwException(); + } catch (CharacterCodingException x) { + throw new Error(x); + } + return destBuffer.position() - originalPosition; + } + + private static void setupEncoder(CharsetEncoder encoder) { + encoder.onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .reset(); + } static byte[] encode(char[] ca, int off, int len) { String csn = Charset.defaultCharset().name(); --- /dev/null 2014-09-05 01:14:58.823419986 +0100 +++ new/test/java/lang/String/StringByteBufferConversions.java 2014-09-25 15:32:02.193804985 +0100 @@ -0,0 +1,144 @@ + +import static org.testng.Assert.assertEquals; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.testng.annotations.Test; + +/* @test + * @summary Test creating a String from a bytebuffer + * @run testng StringByteBufferConversions + */ +public class StringByteBufferConversions { + + @Test(expectedExceptions = NullPointerException.class) + public void nullByteBufferShouldThrowException() { + // given + ByteBuffer buffer = null; + + // when + new String(buffer, UTF_8); + } + + @Test(expectedExceptions = NullPointerException.class) + public void nullCharsetShouldThrowException() { + // given + ByteBuffer buffer = ByteBuffer.allocateDirect(20); + + // when + new String(buffer, null); + } + + @Test + public void heapByteBuffersCanBeConvertedToStrings() { + // given + String expected = "Hello World"; + ByteBuffer buffer = ByteBuffer.wrap(expected.getBytes(UTF_8)); + + // when + String str = new String(buffer, UTF_8); + + // then + assertEquals(str, expected); + } + + @Test + public void directByteBuffersCanBeConvertedToStrings() { + // given + String expected = "Hello World"; + byte[] bytes = expected.getBytes(UTF_8); + ByteBuffer buffer = ByteBuffer.allocateDirect(bytes.length) + .put(bytes, 0, bytes.length); + + buffer.position(0); + + // when + String str = new String(buffer, UTF_8); + + // then + assertEquals(str, expected); + } + + @Test + public void chunksOfByteBuffersCanBeConvertedToStrings() { + // given + String expected = "Hello World"; + byte[] bytes = expected.getBytes(UTF_8); + int offset = 5; + int length = offset + bytes.length + 5; + ByteBuffer buffer = ByteBuffer.allocateDirect(length); + buffer.position(offset); + buffer.put(bytes, 0, bytes.length); + + buffer.position(offset); + buffer.limit(offset + bytes.length); + + // when + String str = new String(buffer, UTF_8); + + // then + assertEquals(str, expected); + } + + @Test(expectedExceptions = NullPointerException.class) + public void getBytesThrowsExceptionWithNullBuffer() { + "".getBytes((ByteBuffer)null, UTF_8); + } + + @Test(expectedExceptions = NullPointerException.class) + public void getBytesThrowsExceptionWithNullByteArray() { + "".getBytes((byte[])null, 0, UTF_8); + } + + @Test(expectedExceptions = NullPointerException.class) + public void getBytesThrowsExceptionWithNullCharset() { + byte[] bytes = new byte[16]; + "".getBytes(bytes, 0, null); + } + + @Test + public void shouldEncodeStringsOntoByteArrays() { + byte[] bytes = new byte[11]; + int written = "Hello World".getBytes(bytes, 0, UTF_8); + assertEquals(written, 11); + + assertEquals(new String(bytes, UTF_8), "Hello World"); + } + + @Test + public void shouldEncodeStringsOntoByteBuffer() { + ByteBuffer bytes = ByteBuffer.allocateDirect(11); + int written = "Hello World".getBytes(bytes, UTF_8); + assertEquals(written, 11); + + bytes.position(0); + assertEquals(new String(bytes, UTF_8), "Hello World"); + } + + @Test + public void shouldEncodeStringsOntoChunkOfByteArray() { + byte[] bytes = new byte[20]; + final int offset = 5; + int written = "Hello World".getBytes(bytes, offset, UTF_8); + assertEquals(written, 11); + + assertEquals(new String(bytes, 5, 11, UTF_8), "Hello World"); + } + + @Test + public void shouldEncodeStringsOntoChunkOfByteBuffer() { + ByteBuffer bytes = ByteBuffer.allocateDirect(20); + final int offset = 5; + bytes.position(offset); + int written = "Hello World".getBytes(bytes, UTF_8); + assertEquals(written, 11); + + bytes.position(offset); + bytes.limit(offset + 11); + assertEquals(new String(bytes, UTF_8), "Hello World"); + } + +} +