org.apache.james.mime4j.codec.QuotedPrintableInputStream.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.mime4j.codec.QuotedPrintableInputStream.java

Source

/****************************************************************
 * 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.james.mime4j.codec;

import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Performs Quoted-Printable decoding on an underlying stream.
 */
public class QuotedPrintableInputStream extends InputStream {
    private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class);

    private InputStream stream;
    ByteQueue byteq = new ByteQueue();
    ByteQueue pushbackq = new ByteQueue();
    private byte state = 0;
    private boolean closed = false;

    public QuotedPrintableInputStream(InputStream stream) {
        this.stream = stream;
    }

    /**
     * Terminates Quoted-Printable coded content. This method does NOT close 
     * the underlying input stream.
     * 
     * @throws IOException on I/O errors.
     */
    @Override
    public void close() throws IOException {
        this.closed = true;
    }

    @Override
    public int read() throws IOException {
        if (closed) {
            throw new IOException("QuotedPrintableInputStream has been closed");
        }
        fillBuffer();
        if (byteq.count() == 0)
            return -1;
        else {
            byte val = byteq.dequeue();
            if (val >= 0)
                return val;
            else
                return val & 0xFF;
        }
    }

    /**
     * Pulls bytes out of the underlying stream and places them in the
     * pushback queue.  This is necessary (vs. reading from the
     * underlying stream directly) to detect and filter out "transport
     * padding" whitespace, i.e., all whitespace that appears immediately
     * before a CRLF.
     *
     * @throws IOException Underlying stream threw IOException.
     */
    private void populatePushbackQueue() throws IOException {
        //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!");

        if (pushbackq.count() != 0)
            return;

        while (true) {
            int i = stream.read();
            switch (i) {
            case -1:
                // stream is done
                pushbackq.clear(); // discard any whitespace preceding EOF
                return;
            case ' ':
            case '\t':
                pushbackq.enqueue((byte) i);
                break;
            case '\r':
            case '\n':
                pushbackq.clear(); // discard any whitespace preceding EOL
                pushbackq.enqueue((byte) i);
                return;
            default:
                pushbackq.enqueue((byte) i);
                return;
            }
        }
    }

    /**
     * Causes the pushback queue to get populated if it is empty, then
     * consumes and decodes bytes out of it until one or more bytes are
     * in the byte queue.  This decoding step performs the actual QP
     * decoding.
     *
     * @throws IOException Underlying stream threw IOException.
     */
    private void fillBuffer() throws IOException {
        byte msdChar = 0; // first digit of escaped num
        while (byteq.count() == 0) {
            if (pushbackq.count() == 0) {
                populatePushbackQueue();
                if (pushbackq.count() == 0)
                    return;
            }

            byte b = pushbackq.dequeue();

            switch (state) {
            case 0: // start state, no bytes pending
                if (b != '=') {
                    byteq.enqueue(b);
                    break; // state remains 0
                } else {
                    state = 1;
                    break;
                }
            case 1: // encountered "=" so far
                if (b == '\r') {
                    state = 2;
                    break;
                } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
                    state = 3;
                    msdChar = b; // save until next digit encountered
                    break;
                } else if (b == '=') {
                    /*
                     * Special case when == is encountered.
                     * Emit one = and stay in this state.
                     */
                    if (log.isWarnEnabled()) {
                        log.warn("Malformed MIME; got ==");
                    }
                    byteq.enqueue((byte) '=');
                    break;
                } else {
                    if (log.isWarnEnabled()) {
                        log.warn("Malformed MIME; expected \\r or " + "[0-9A-Z], got " + b);
                    }
                    state = 0;
                    byteq.enqueue((byte) '=');
                    byteq.enqueue(b);
                    break;
                }
            case 2: // encountered "=\r" so far
                if (b == '\n') {
                    state = 0;
                    break;
                } else {
                    if (log.isWarnEnabled()) {
                        log.warn("Malformed MIME; expected " + (int) '\n' + ", got " + b);
                    }
                    state = 0;
                    byteq.enqueue((byte) '=');
                    byteq.enqueue((byte) '\r');
                    byteq.enqueue(b);
                    break;
                }
            case 3: // encountered =<digit> so far; expecting another <digit> to complete the octet
                if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
                    byte msd = asciiCharToNumericValue(msdChar);
                    byte low = asciiCharToNumericValue(b);
                    state = 0;
                    byteq.enqueue((byte) ((msd << 4) | low));
                    break;
                } else {
                    if (log.isWarnEnabled()) {
                        log.warn("Malformed MIME; expected " + "[0-9A-Z], got " + b);
                    }
                    state = 0;
                    byteq.enqueue((byte) '=');
                    byteq.enqueue(msdChar);
                    byteq.enqueue(b);
                    break;
                }
            default: // should never happen
                log.error("Illegal state: " + state);
                state = 0;
                byteq.enqueue(b);
                break;
            }
        }
    }

    /**
     * Converts '0' => 0, 'A' => 10, etc.
     * @param c ASCII character value.
     * @return Numeric value of hexadecimal character.
     */
    private byte asciiCharToNumericValue(byte c) {
        if (c >= '0' && c <= '9') {
            return (byte) (c - '0');
        } else if (c >= 'A' && c <= 'Z') {
            return (byte) (0xA + (c - 'A'));
        } else if (c >= 'a' && c <= 'z') {
            return (byte) (0xA + (c - 'a'));
        } else {
            /*
             * This should never happen since all calls to this method
             * are preceded by a check that c is in [0-9A-Za-z]
             */
            throw new IllegalArgumentException((char) c + " is not a hexadecimal digit");
        }
    }

}