// Commented for the Learning branch package com.limegroup.bittorrent.bencoding; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; /** * A BELong object reads bencoded data like "i87e" and parses it into a number. * You can also use a BELong to read and parse the length number at the start of a string, like "52:". */ class BELong extends Token { /** "-", the ASCII byte of a minus sign. */ private static final byte MINUS; // Java runs this code before control enters a method in this class static { // Conver the "-" String into a byte array using default ASCII encoding, and read the first byte byte minus = 0; try { minus = "-".getBytes(ASCII)[0]; } catch (UnsupportedEncodingException impossible) {} MINUS = minus; } /** A StringBuffer that holds the digits of the number, like "124" for the bencoded data "i124e". */ private StringBuffer sb = new StringBuffer(); /** A byte array that holds a single byte. */ private byte[] currentByte = new byte[1]; /** * A ByteBuffer that holds a single byte. * handleRead() uses buf to read bencoded data from our channel one character at a time. */ private ByteBuffer buf = ByteBuffer.wrap(currentByte); /** -1 for negative values, 0 for 0, 1 for positive values. */ private int multiplier = 1; /** When we've read the terminator like "e" or ":" from the channel, handleRead() sets done to true. */ private boolean done; /** * The charcter we're looking for that ends the number. * If the bencoded data is like "i87e", terminator will be "e". * If the bencoded data is like "5:hello", terminator will be ":". */ private final byte terminator; /** * Make a new BELong object that will read bencoded data like "i87e" from the given channel and turn it into a number. * * @param chan The ReadableByteChannel this new BELong object will read bencoded data from. * The program has already read the leading "i" from this channel. * The new BELong object will read the number and the trailing "e", but not take anything else out of the channel. */ BELong(ReadableByteChannel chan) { // Call the next constructor this( chan, // The channel we can read bencoded data from E, // "e", when we read a "e", that's the end of the number (byte)0); // No, we haven't read a digit like "0" through "9" from the channel yet } /** * Make a new BELong object that will read a bencoded number from the given channel like "i23e" or "53:". * In the "i23e" case, we've already read "i" from the channel. * In the "53:" case, we may have already read the first digit "5", if so, it's passed here as firstByte. * * @param chan A ReadableByteChannel the new BELong can read from to get more data. * @param terminator The character we'll look for to mark the end of the numerals, like "e" or ":". * @param firstByte If you already ready the first digit from the channel, pass it here. * If not, pass 0 for firstByte. */ BELong(ReadableByteChannel chan, byte terminator, byte firstByte) { // Save the channel this new object will read data from super(chan); // Save the character we'll watch for to end the number this.terminator = terminator; // If the caller already pulled the first byte of this number out of the channel if (firstByte != 0) { // Make sure the character the caller read was a digit, "0" through "9" if (firstByte < ZERO || firstByte > NINE) throw new IllegalArgumentException("invalid first byte"); // Add the number to the sb string of numerals we're building sb.append(firstByte - ZERO); // If firstByte is "5", subtract "0" to get the number 5, then append() turns that number back into text } } /** * The "NIODispatch" thread calls handleRead() when the channel this BELong object gets its bencoded data from has some more. * * Bencoded numbers look like "i87e" for 87, "i0e" for 0, and "i-32e" for negative 32. * BELong can also parse the number that tells the length of a bencoded string, like "52:". * * Loops, reading individual characters of bencoded data from the channel this BELong was given when it was made. * Looks for a leading "-", makes sure there are no leading 0s, and stops on the terminating character "e" or ":". * Puts the numerals into a StringBuffer named sb. * When it reaches the terminating "e" or ":", converts the numerals into a number and saves it as a Long object named result. * At this point, isDone() returns true, and getResult() returns the Long object. */ public void handleRead() throws IOException { // If we've read the complete number like "i87e", the program shouldn't call handleRead() on this BELong object again if (done) throw new IllegalStateException("this token is done. Don't read it!"); // Loop, reading individual characters from the channel while (true) { try { // Get the next character of bencoded data from the channel int read = chan.read(buf); if (read == -1) throw new IOException("channel closed before end of integer token"); else if (read == 0) return; // The channel doesn't have any more data for us right now, this method will continue the next time it's called } finally { // Mark the ByteBuffer empty for the next time, we can still get to the byte we read with currentByte[0] buf.clear(); } // The character isn't a numeral "0" through "9" if (currentByte[0] < ZERO || currentByte[0] > NINE) { // It's "-", a minus sign at the front of a bencoded number like "i-25e" if (currentByte[0] == MINUS && sb.length() == 0 && multiplier != -1) { // Set the multiplier to -1 so we'll make the number we parse negative multiplier = -1; // We read the terminating character like "e" or ":", and we have found some numerals before this } else if (currentByte[0] == terminator && sb.length() != 0) { try { // Compute the result number result = new Long(Long.parseLong(sb.toString()) * multiplier); // There was an error reading the text as a number } catch (NumberFormatException impossible) { throw new IOException(impossible.getMessage()); } // We don't need to the StringBuffer anymore sb = null; // Have isDone() return true after this so the caller can get the Long object with getResult() done = true; return; // It's not "0" through "9", or "-", or "e" or ":" } else { // The bencoded data has a mistake, throw an exception throw new IOException("invalid integer"); } // The character we read is "0" } else if (currentByte[0] == ZERO) { // We haven't read any numerals yet switch (sb.length()) { case 0: // Don't allow the number "i-0e" if (multiplier == -1) throw new IOException("negative 0"); // The number is "i0e", which is 0 multiplier = 0; break; // We've read some numerals before running into this 0 case 1: // Don't allow leading 0s, like "i050e" for 50 if (multiplier == 0) throw new IOException("leading 0s"); } // Add the "0" to sb, which hold the numerals we've read sb.append(0); // The character we read is "1" through "9" } else { // Make sure we haven't found a leading 0 before this if (multiplier == 0) throw new IOException("leading 0s - wrong"); // Add the numeral to the string of them we've read sb.append(currentByte[0] - ZERO); } } } /** * Find out if this BELong object has read the whole bencoded number like "i87e". * If it has, you can call getResult() to get the number in a Long object. * * @return True if this BELong has read and parsed the whole bencoded number. * False if you still need to call handleRead() to get it to read and parse more bencoded data from its channel. */ protected boolean isDone() { // Return the done flag that handleRead() will set to true when it's done return done; } /** * Find out what type of Token object this is, and what kind of Java object it will parse and make. * * @return Token.LONG, this is a BELong that will parse a Number object */ public int getType() { // Return the LONG code number for the number type return LONG; } }