001// Copyright (C) 1999-2002 by Jason Hunter <jhunter_AT_acm_DOT_org>.
002// All rights reserved.  Use of this class is limited.
003// Please see the LICENSE for more information.
004
005package com.oreilly.servlet;
006
007import java.io.*;
008
009/** 
010 * A class to encode Base64 streams and strings.  
011 * See RFC 1521 section 5.2 for details of the Base64 algorithm.
012 * <p>
013 * This class can be used for encoding strings:
014 * <blockquote><pre>
015 * String unencoded = "webmaster:try2gueSS";
016 * String encoded = Base64Encoder.encode(unencoded);
017 * </pre></blockquote>
018 * or for encoding streams:
019 * <blockquote><pre>
020 * OutputStream out = new Base64Encoder(System.out);
021 * </pre></blockquote>
022 *
023 * @author <b>Jason Hunter</b>, Copyright &#169; 2000
024 * @version 1.2, 2002/11/01, added encode(byte[]) method to better handle
025 *                           binary data (thanks to Sean Graham)
026 * @version 1.1, 2000/11/17, fixed bug with sign bit for char values
027 * @version 1.0, 2000/06/11
028 */
029public class Base64Encoder extends FilterOutputStream {
030
031  private static final char[] chars = {
032    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
033    'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
034    'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
035    'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
036    'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
037    'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
038    '8', '9', '+', '/'
039  };
040
041  private int charCount;
042  private int carryOver;
043
044  /**
045   * Constructs a new Base64 encoder that writes output to the given
046   * OutputStream.
047   *
048   * @param out the output stream
049   */
050  public Base64Encoder(OutputStream out) {
051    super(out);
052  }
053
054  /**
055   * Writes the given byte to the output stream in an encoded form.
056   *
057   * @exception IOException if an I/O error occurs
058   */
059  public void write(int b) throws IOException {
060    // Take 24-bits from three octets, translate into four encoded chars
061    // Break lines at 76 chars
062    // If necessary, pad with 0 bits on the right at the end
063    // Use = signs as padding at the end to ensure encodedLength % 4 == 0
064
065    // Remove the sign bit,
066    // thanks to Christian Schweingruber <chrigu@lorraine.ch>
067    if (b < 0) {
068      b += 256;
069    }
070
071    // First byte use first six bits, save last two bits
072    if (charCount % 3 == 0) {
073      int lookup = b >> 2;
074      carryOver = b & 3;        // last two bits
075      out.write(chars[lookup]);
076    }
077    // Second byte use previous two bits and first four new bits,
078    // save last four bits
079    else if (charCount % 3 == 1) {
080      int lookup = ((carryOver << 4) + (b >> 4)) & 63;
081      carryOver = b & 15;       // last four bits
082      out.write(chars[lookup]);
083    }
084    // Third byte use previous four bits and first two new bits,
085    // then use last six new bits
086    else if (charCount % 3 == 2) {
087      int lookup = ((carryOver << 2) + (b >> 6)) & 63;
088      out.write(chars[lookup]);
089      lookup = b & 63;          // last six bits
090      out.write(chars[lookup]);
091      carryOver = 0;
092    }
093    charCount++;
094
095    // Add newline every 76 output chars (that's 57 input chars)
096    if (charCount % 57 == 0) {
097      out.write('\n');
098    }
099  }
100
101  /**
102   * Writes the given byte array to the output stream in an 
103   * encoded form.
104   *
105   * @param b the data to be written
106   * @param off the start offset of the data
107   * @param len the length of the data
108   * @exception IOException if an I/O error occurs
109   */
110  public void write(byte[] buf, int off, int len) throws IOException {
111    // This could of course be optimized
112    for (int i = 0; i < len; i++) {
113      write(buf[off + i]);
114    }
115  }
116
117  /**
118   * Closes the stream, this MUST be called to ensure proper padding is
119   * written to the end of the output stream.
120   *
121   * @exception IOException if an I/O error occurs
122   */
123  public void close() throws IOException {
124    // Handle leftover bytes
125    if (charCount % 3 == 1) {  // one leftover
126      int lookup = (carryOver << 4) & 63;
127      out.write(chars[lookup]);
128      out.write('=');
129      out.write('=');
130    }
131    else if (charCount % 3 == 2) {  // two leftovers
132      int lookup = (carryOver << 2) & 63;
133      out.write(chars[lookup]);
134      out.write('=');
135    }
136    super.close();
137  }
138
139  /**
140   * Returns the encoded form of the given unencoded string.  The encoder
141   * uses the ISO-8859-1 (Latin-1) encoding to convert the string to bytes.
142   * For greater control over the encoding, encode the string to bytes
143   * yourself and use encode(byte[]).
144   *
145   * @param unencoded the string to encode
146   * @return the encoded form of the unencoded string
147   */
148  public static String encode(String unencoded) {
149    byte[] bytes = null;
150    try {
151      bytes = unencoded.getBytes("8859_1");
152    }
153    catch (UnsupportedEncodingException ignored) { }
154    return encode(bytes);
155  }
156
157  /**
158   * Returns the encoded form of the given unencoded string.
159   *
160   * @param unencoded the string to encode
161   * @return the encoded form of the unencoded string
162   */
163  public static String encode(byte[] bytes) {
164    ByteArrayOutputStream out = 
165      new ByteArrayOutputStream((int) (bytes.length * 1.37));
166    Base64Encoder encodedOut = new Base64Encoder(out);
167    
168    try {
169      encodedOut.write(bytes);
170      encodedOut.close();
171
172      return out.toString("8859_1");
173    }
174    catch (IOException ignored) { return null; }
175  }
176
177  public static void main(String[] args) throws Exception {
178    if (args.length != 1) {
179      System.err.println(
180        "Usage: java com.oreilly.servlet.Base64Encoder fileToEncode");
181      return;
182    }
183
184    Base64Encoder encoder = null;
185    BufferedInputStream in = null;
186    try {
187      encoder = new Base64Encoder(System.out);
188      in = new BufferedInputStream(new FileInputStream(args[0]));
189
190      byte[] buf = new byte[4 * 1024];  // 4K buffer
191      int bytesRead;
192      while ((bytesRead = in.read(buf)) != -1) {
193        encoder.write(buf, 0, bytesRead);
194      }
195    }
196    finally {
197      if (in != null) in.close();
198      if (encoder != null) encoder.close();
199    }
200  }
201}