db-derby-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From krist...@apache.org
Subject svn commit: r547422 - in /db/derby/code/trunk/java: engine/org/apache/derby/impl/jdbc/ testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/
Date Thu, 14 Jun 2007 22:03:39 GMT
Author: kristwaa
Date: Thu Jun 14 15:03:38 2007
New Revision: 547422

URL: http://svn.apache.org/viewvc?view=rev&rev=547422
Log:
DERBY-2806: Added a position-aware stream that can be repositioned on request. This was needed
to support multiplexed operations on Clob, specifically involving streams. The code is isolated
to Clobs on top of a stream from store (read-only), with the exception of UTF8Reader which
is also used for other Clobs (temporary, modifiable ones).

Added:
    db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/PositionedStoreStream.java
  (with props)
Modified:
    db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/StoreStreamClob.java
    db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/UTF8Reader.java
    db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/ClobUpdateableReaderTest.java

Added: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/PositionedStoreStream.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/PositionedStoreStream.java?view=auto&rev=547422
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/PositionedStoreStream.java
(added)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/PositionedStoreStream.java
Thu Jun 14 15:03:38 2007
@@ -0,0 +1,239 @@
+/*
+
+   Derby - org.apache.derby.impl.jdbc.PositionedStoreStream
+
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+
+ */
+package org.apache.derby.impl.jdbc;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.derby.iapi.error.StandardException;
+import org.apache.derby.iapi.types.Resetable;
+
+/**
+ * A wrapper-stream able to reposition the underlying store stream.
+ * <p>
+ * Where a user expects the underlying stream to be at a given position,
+ * {@link #reposition} must be called with the expected position first. A use
+ * case for this scenario is the LOB objects, where you can request a stream and
+ * at the same time (this does not mean concurrently) query the LOB about its
+ * length or ask to get a part of the LOB returned. Such multiplexed operations
+ * must result in consistent and valid data, and to achieve this the underlying
+ * store stream must be able to reposition itself.
+ *
+ * <em>Synchronization</em>: Access to instances of this class must be
+ * externally synchronized on the connection synchronization object. There are
+ * two reasons for this:
+ * <ul> <li>Access to store must be single threaded.
+ *      <li>This class is not thread safe, and calling the various methods from
+ *          different threads concurrently can result in inconsistent position
+ *          values. To avoid redundant internal synchronization, this class
+ *          assumes and <b>requires</b> external synchronization (also called
+ *          client-side locking).
+ * </ul>
+ * @see EmbedConnection#getConnectionSynchronization
+ */
+//@NotThreadSafe
+public class PositionedStoreStream
+    extends InputStream
+    implements Resetable {
+
+    /** Underlying store stream serving bytes. */
+    //@GuardedBy("EmbedConnection.getConnectionSynchronization()")
+    private final InputStream stream;
+    /** Convenience reference to the stream as a resettable stream. */
+    //@GuardedBy("EmbedConnection.getConnectionSynchronization()")
+    private final Resetable resettable;
+    /**
+     * Position of the underlying store stream.
+     * Note that the position is maintained by this class, not the underlying
+     * store stream itself.
+     * <em>Future improvement</em>: Add this functionality to the underlying
+     * store stream itself to avoid another level in the stream stack.
+     */
+    //@GuardedBy("EmbedConnection.getConnectionSynchronization()")
+    private long pos = 0L;
+
+    /**
+     * Creates a positioned store stream on top of the specified resettable
+     * stream.
+     *
+     * @param in a {@link Resetable}-stream
+     * @throws ClassCastException if the inputstream does not implement
+     *      {@link Resetable}
+     */
+    public PositionedStoreStream(InputStream in) {
+        this.stream = in;
+        this.resettable = (Resetable)in;
+    }
+
+    /**
+     * Reads a number of bytes from the underlying stream and stores them in the
+     * specified byte array.
+     *
+     * @return The actual number of bytes read, or -1 if the end of the stream
+     *      is reached.
+     * @throws IOException if an I/O error occurs
+     */
+    public int read(byte[] b)
+            throws IOException {
+        int ret = this.stream.read(b);
+        this.pos += ret;
+        return ret;
+    }
+
+    /**
+     * Reads a number of bytes from the underlying stream and stores them in the
+     * specified byte array at the specified offset.
+     *
+     * @return The actual number of bytes read, or -1 if the end of the stream
+     *      is reached.
+     * @throws IOException if an I/O error occurs
+     */
+    public int read(byte[] b, int off, int len)
+            throws IOException {
+        int ret = this.stream.read(b, off, len);
+        this.pos += ret;
+        return ret;
+    }
+
+    /**
+     * Reads a single byte from the underlying stream.
+     *
+     * @return The next byte of data, or -1 if the end of the stream is reached.
+     * @throws IOException if an I/O error occurs
+     */
+    public int read()
+            throws IOException {
+        int ret = this.stream.read();
+        if (ret > -1) {
+            this.pos++;
+        }
+        return ret;
+    }
+
+    /**
+     * Skips up to the specified number of bytes from the underlying stream.
+     *
+     * @return The actual number of bytes skipped.
+     * @throws IOException if an I/O error occurs
+     */
+    public long skip(long toSkip)
+            throws IOException {
+        long ret = this.stream.skip(toSkip);
+        this.pos += ret;
+        return ret;
+    }
+
+    /**
+     * Resets the resettable stream.
+     *
+     * @throws IOException
+     * @throws StandardException if resetting the stream in store fails
+     * @see Resetable#resetStream
+     */
+    public void resetStream()
+            throws IOException, StandardException {
+        this.resettable.resetStream();
+        this.pos = 0L;
+    }
+
+    /**
+     * Initialize the resettable stream for use.
+     *
+     * @throws StandardException if initializing the store in stream fails
+     * @see Resetable#initStream
+     */
+    public void initStream()
+            throws StandardException {
+        this.resettable.initStream();
+        this.pos = 0L;
+    }
+
+    /**
+     * Closes the resettable stream.
+     *
+     * @see Resetable#closeStream
+     */
+    public void closeStream() {
+        this.resettable.closeStream();
+    }
+
+    /**
+     * Repositions the underlying store stream to the requested position.
+     * <p>
+     * Repositioning is required because there can be several uses of the store
+     * stream, which changes the position of it. If a class is dependent on the
+     * underlying stream not changing its position, it must call reposition with
+     * the position it expects before using the stream again.
+     *
+     * @throws IOException if reading from the store stream fails
+     * @throws StandardException if resetting the store in stream fails, or
+     *      some other exception happens in store
+     * @see #getPosition
+     */
+    public void reposition(long requestedPos)
+            throws IOException, StandardException {
+        if (this.pos < requestedPos) {
+            // Reposition from current position.
+            skipFully(requestedPos - this.pos);
+            this.pos = requestedPos;
+        } else if (this.pos > requestedPos) {
+            // Reposition from start.
+            this.resettable.resetStream();
+            skipFully(requestedPos);
+            this.pos = requestedPos;
+        }
+    }
+
+    /**
+     * Returns the current position of the underlying store stream.
+     *
+     * @return Current byte position of the store stream.
+     */
+    public long getPosition() {
+        return this.pos;
+    }
+
+    /**
+     * Skip exactly the requested number of bytes.
+     *
+     * @throws EOFException if EOF is reached before all bytes are skipped
+     * @throws IOException if reading from the stream fails
+     */
+    private void skipFully(long toSkip)
+            throws IOException {
+        long remaining = toSkip;
+        while (remaining > 0) {
+            long skippedNow = this.stream.skip(remaining);
+            if (skippedNow == 0) {
+                if (this.stream.read() == -1) {
+                    throw new EOFException("Reached end-of-stream prematurely" +
+                        ", with " + remaining + " byte(s) to go");
+                } else {
+                    skippedNow = 1;
+                }
+            }
+            remaining -= skippedNow;
+        }
+    }
+} // End class PositionedStoreStream

Propchange: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/PositionedStoreStream.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/StoreStreamClob.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/StoreStreamClob.java?view=diff&rev=547422&r1=547421&r2=547422
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/StoreStreamClob.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/StoreStreamClob.java Thu Jun
14 15:03:38 2007
@@ -66,7 +66,7 @@
      * {@link Resetable}.
      */
     //@GuardedBy("synchronizationObject")
-    private final InputStream storeStream;
+    private final PositionedStoreStream positionedStoreStream;
     /** The connection (child) this Clob belongs to. */
     private final ConnectionChild conChild;
     /** Object used for synchronizing access to the store stream. */
@@ -96,10 +96,10 @@
      */
     public StoreStreamClob(InputStream stream, ConnectionChild conChild)
             throws StandardException {
-        this.storeStream = stream;
+        this.positionedStoreStream = new PositionedStoreStream(stream);
         this.conChild = conChild;
         this.synchronizationObject = conChild.getConnectionSynchronization();
-        ((Resetable)this.storeStream).initStream();
+        this.positionedStoreStream.initStream();
     }
 
     /**
@@ -107,7 +107,7 @@
      */
     public void release() {
         if (!released) {
-            ((Resetable)this.storeStream).closeStream();
+            this.positionedStoreStream.closeStream();
             this.released = true;
         }
     }
@@ -126,11 +126,15 @@
         long byteLength = 0;
         try {
             this.conChild.setupContextStack();
+            this.positionedStoreStream.reposition(0L);
             // See if length is encoded in the stream.
-            byteLength = resetStoreStream(true);
+            int us1 = this.positionedStoreStream.read();
+            int us2 = this.positionedStoreStream.read();
+            byteLength = (us1 << 8) + (us2 << 0);
             if (byteLength == 0) {
                 while (true) {
-                    long skipped = this.storeStream.skip(SKIP_BUFFER_SIZE);
+                    long skipped =
+                        this.positionedStoreStream.skip(SKIP_BUFFER_SIZE);
                     if (skipped <= 0) {
                         break;
                     }
@@ -140,6 +144,8 @@
                 byteLength -= 3;
             }
             return byteLength;
+        } catch (StandardException se) {
+            throw Util.generateCsSQLException(se);
         } finally {
             this.conChild.restoreContextStack();
         }
@@ -183,8 +189,13 @@
     public InputStream getRawByteStream()
             throws IOException, SQLException {
         checkIfValid();
-        resetStoreStream(true);
-        return this.storeStream;
+        try {
+            // Skip the encoded length.
+            this.positionedStoreStream.reposition(2L);
+        } catch (StandardException se) {
+            throw Util.generateCsSQLException(se);
+        }
+        return this.positionedStoreStream;
     }
 
     /**
@@ -200,9 +211,14 @@
     public Reader getReader(long pos)
             throws IOException, SQLException  {
         checkIfValid();
-        resetStoreStream(false);
-        Reader reader = new UTF8Reader(this.storeStream, TypeId.CLOB_MAXWIDTH,
-            this.conChild, this.synchronizationObject);
+        try {
+            this.positionedStoreStream.reposition(0L);
+        } catch (StandardException se) {
+            throw Util.generateCsSQLException(se);
+        }
+        Reader reader = new UTF8Reader(this.positionedStoreStream,
+                                TypeId.CLOB_MAXWIDTH, this.conChild,
+                                this.synchronizationObject);
         long leftToSkip = pos -1;
         long skipped;
         while (leftToSkip > 0) {
@@ -302,39 +318,5 @@
             throw new IllegalStateException(
                 "The Clob has been released and is not valid");
         }
-    }
-
-    /**
-     * Reset the store stream, skipping two bytes of length encoding if
-     * requested.
-     *
-     * @param skipEncodedLength <code>true</code> will cause length encoding
to
-     *      be skipped. Note that the length is not always recorded when data is
-     *      written to store, and therefore it is ignored.
-     * @return The length encoded in the stream, or <code>-1</code> if the
-     *      length information is not decoded. A return value of <code>0</code>
-     *      means the stream is ended with a Derby end-of-stream marker.
-     * @throws IOException if skipping the two bytes fails
-     * @throws SQLException if resetting the stream fails in store
-     */
-    private long resetStoreStream(boolean skipEncodedLength)
-            throws IOException, SQLException {
-        try {
-            ((Resetable)this.storeStream).resetStream();
-        } catch (StandardException se) {
-            throw noStateChangeLOB(se);
-        }
-        long encodedLength = -1L;
-        if (skipEncodedLength) {
-            int b1 = this.storeStream.read();
-            int b2 = this.storeStream.read();
-            if (b1 == -1 || b2 == -1) {
-                throw Util.setStreamFailure(
-                    new IOException("Reached end-of-stream prematurely"));
-            }
-            // Length is currently written as an unsigned short.
-            encodedLength = (b1 << 8) + (b2 << 0);
-        }
-        return encodedLength;
     }
 } // End class StoreStreamClob

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/UTF8Reader.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/UTF8Reader.java?view=diff&rev=547422&r1=547421&r2=547422
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/UTF8Reader.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/jdbc/UTF8Reader.java Thu Jun 14
15:03:38 2007
@@ -29,12 +29,20 @@
 import java.io.EOFException;
 import java.sql.SQLException;
 
+import org.apache.derby.iapi.error.StandardException;
+import org.apache.derby.iapi.services.sanity.SanityManager;
+import org.apache.derby.iapi.types.Resetable;
+
 /**
 */
 public final class UTF8Reader extends Reader
 {
 
 	private InputStream in;
+    /** Stream store that can reposition itself on request. */
+    private final PositionedStoreStream positionedIn;
+    /** Store last visited position in the store stream. */
+    private long rawStreamPos = 0L;
 	private final long         utfLen;	// bytes
 	private long        utfCount;		// bytes
 	private long		readerCharCount; // characters
@@ -55,17 +63,46 @@
 	long maxFieldSize,
     ConnectionChild      parent,
 	Object synchronization) 
-        throws IOException
+        throws IOException, SQLException
 	{
 		super(synchronization);
-
-		this.in     = new BufferedInputStream (in);
 		this.maxFieldSize = maxFieldSize;
 		this.parent = parent;
 
-		synchronized (lock) {
-			this.utfLen = readUnsignedShort();
-		}
+        parent.setupContextStack();
+        try {
+            synchronized (lock) { // Synchronize access to store.
+                if (in instanceof PositionedStoreStream) {
+                    this.positionedIn = (PositionedStoreStream)in;
+                    // This stream is already buffered, and buffering it again
+                    // this high up complicates the handling a lot. Must
+                    // implement a special buffered reader to buffer again.
+                    // Note that buffering this UTF8Reader again, does not
+                    // cause any trouble...
+                    this.in = in;
+                    try {
+                        this.positionedIn.resetStream();
+                    } catch (StandardException se) {
+                        IOException ioe = new IOException(se.getMessage());
+                        ioe.initCause(se);
+                        throw ioe;
+                    }
+                } else {
+                    this.positionedIn = null;
+                    // Buffer this for improved performance.
+                    this.in = new BufferedInputStream (in);
+                }
+                this.utfLen = readUnsignedShort();
+                // Even if we are reading the encoded length, the stream may
+                // not be a positioned stream. This is currently true when a
+                // stream is passed in after a ResetSet.getXXXStream method.
+                if (this.positionedIn != null) {
+                    this.rawStreamPos = this.positionedIn.getPosition();
+                }
+            } // End synchronized block
+        } finally {
+            parent.restoreContextStack();
+        }
 	}
 
     /**
@@ -88,11 +125,19 @@
                 Object synchronization)
                 throws IOException {
         super(synchronization);
-
-        this.in = new BufferedInputStream(in);
         this.maxFieldSize = maxFieldSize;
         this.parent = parent;
         this.utfLen = streamSize;
+        this.positionedIn = null;
+
+        if (SanityManager.DEBUG) {
+            // Do not allow the inputstream here to be a Resetable, as this
+            // means (currently, not by design...) that the length is encoded in
+            // the stream and we can't pass that out as data to the user.
+            SanityManager.ASSERT(!(in instanceof Resetable));
+        }
+        // Buffer this for improved performance.
+        this.in = new BufferedInputStream(in);
     }
 
 	/*
@@ -270,6 +315,7 @@
 	/**
 		Fill the buffer, return true if eof has been reached.
 	*/
+    //@GuardedBy("lock")
 	private boolean fillBuffer() throws IOException
 	{
 		if (in == null)
@@ -281,7 +327,15 @@
 		try {
 		
 			parent.setupContextStack();
-
+            // If we are operating on a positioned stream, reposition it to
+            // continue reading at the position we stopped last time.
+            if (this.positionedIn != null) {
+                try {
+                    this.positionedIn.reposition(this.rawStreamPos);
+                } catch (StandardException se) {
+                    throw Util.generateCsSQLException(se);
+                }
+            }
 readChars:
 		while (
 				(charactersInBuffer < buffer.length) &&
@@ -361,8 +415,14 @@
 		if (utfLen != 0 && utfCount > utfLen) 
 			throw utfFormatException("utfCount " + utfCount + " utfLen " + utfLen);		  
 
-		if (charactersInBuffer != 0)
+        if (charactersInBuffer != 0) {
+            if (this.positionedIn != null) {
+                // Save the last visisted position so we can start reading where
+                // we let go the next time we fill the buffer.
+                this.rawStreamPos = this.positionedIn.getPosition();
+            }
 			return false;
+        }
 
 		closeIn();
 		return true;
@@ -370,7 +430,10 @@
 			parent.restoreContextStack();
 		}
 		} catch (SQLException sqle) {
-			throw new IOException(sqle.getSQLState() + ":" + sqle.getMessage());
+            IOException ioe =
+                new IOException(sqle.getSQLState() + ": " + sqle.getMessage());
+            ioe.initCause(sqle);
+            throw ioe;
 		}
 	}
 

Modified: db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/ClobUpdateableReaderTest.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/ClobUpdateableReaderTest.java?view=diff&rev=547422&r1=547421&r2=547422
==============================================================================
--- db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/ClobUpdateableReaderTest.java
(original)
+++ db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/ClobUpdateableReaderTest.java
Thu Jun 14 15:03:38 2007
@@ -27,13 +27,19 @@
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.Statement;
 import junit.framework.Test;
 import junit.framework.TestSuite;
+import org.apache.derbyTesting.functionTests.util.streams.LoopingAlphabetReader;
 import org.apache.derbyTesting.junit.BaseJDBCTestCase;
 import org.apache.derbyTesting.junit.Decorator;
 import org.apache.derbyTesting.junit.TestConfiguration;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
 /**
  * Test class to test <code>UpdateableReader</code> for <code>Clob</code>
in
  * embedded driver.
@@ -181,6 +187,88 @@
         }
     }   
     
+    /**
+     * Tests that the Clob can handle multiple streams and the length call
+     * multiplexed.
+     * <p>
+     * This test was written after bug DERBY-2806 was reported, where getting
+     * the length of the Clob after fetching a stream from it would exhaust
+     * the stream and cause the next read to return -1.
+     * <p>
+     * The test is written to work on a Clob that operates on streams from
+     * the store, which currently means that it must be over a certain size
+     * and that no modifying methods can be called on it.
+     */
+    public void testMultiplexedOperationProblem()
+            throws IOException, SQLException {
+        int length = 266000;
+        PreparedStatement ps = prepareStatement(
+                "insert into updateClob (id, data) values (?,?)");
+        ps.setInt(1, length);
+        ps.setCharacterStream(2, new LoopingAlphabetReader(length), length);
+        assertEquals(1, ps.executeUpdate());
+        ps.close();
+        PreparedStatement psFetchClob = prepareStatement(
+                "select data from updateClob where id = ?");
+        psFetchClob.setInt(1, length);
+        ResultSet rs = psFetchClob.executeQuery();
+        assertTrue("No Clob of length " + length + " in database", rs.next());
+        Clob clob = rs.getClob(1);
+        assertEquals(length, clob.length());
+        Reader r = clob.getCharacterStream();
+        int lastReadChar = r.read();
+        lastReadChar = assertCorrectChar(lastReadChar, r.read());
+        lastReadChar = assertCorrectChar(lastReadChar, r.read());
+        assertEquals(length, clob.length());
+        // Must be bigger than internal buffers might be.
+        int nextChar;
+        for (int i = 2; i < 160000; i++) {
+            nextChar = r.read();
+            // Check manually to report position where it fails.
+            if (nextChar == -1) {
+                fail("Failed at position " + i + ", stream should not be" +
+                        " exhausted now");
+            }
+            lastReadChar = assertCorrectChar(lastReadChar, nextChar);
+        }
+        lastReadChar = assertCorrectChar(lastReadChar, r.read());
+        lastReadChar = assertCorrectChar(lastReadChar, r.read());
+        InputStream ra = clob.getAsciiStream();
+        assertEquals(length, clob.length());
+        int lastReadAscii = ra.read();
+        lastReadAscii = assertCorrectChar(lastReadAscii, ra.read());
+        lastReadAscii = assertCorrectChar(lastReadAscii, ra.read());
+        assertEquals(length, clob.length());
+        lastReadAscii = assertCorrectChar(lastReadAscii, ra.read());
+        lastReadChar = assertCorrectChar(lastReadChar, r.read());
+    }
+
+
+    /**
+     * Asserts that the two specified characters follow each other in the
+     * modern latin lowercase alphabet.
+     */
+    private int assertCorrectChar(int prevChar, int nextChar)
+            throws IOException {
+        assertTrue("Reached EOF unexpectedly", nextChar != -1);
+        if (nextChar < 97 && nextChar > 122) {
+            fail("Char out of range: " + nextChar);
+        }
+        if (prevChar < 97 && prevChar > 122) {
+            fail("Char out of range: " + prevChar);
+        }
+        if (prevChar > -1) {
+            // Work with modern latin lowercase: 97 - 122
+            if (prevChar == 122) {
+                assertTrue(prevChar + " -> " + nextChar,
+                        nextChar == 97);
+            } else {
+                assertTrue(prevChar + " -> " + nextChar,
+                        nextChar == prevChar +1);
+            }
+        }
+        return nextChar;
+    }
     /**
      * Generates a (static) string containing various Unicode characters.
      *



Mime
View raw message