/* ==================================================================== * Copyright (c) 2006 J.T. Beetstra * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ==================================================================== */ package com.beetstra.jutf7; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; /** * <p> * The CharsetEncoder used to encode both variants of the UTF-7 charset and the * modified-UTF-7 charset. * </p> * <p> * <strong>Please note this class does not behave strictly according to the * specification in Sun Java VMs before 1.6.</strong> This is done to get around * a bug in the implementation of * {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)}. Unfortunately, * that method cannot be overridden. * </p> * * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6221056">JDK * bug 6221056< /a> * @author Jaap Beetstra */ class UTF7StyleCharsetEncoder extends CharsetEncoder { private static final float AVG_BYTES_PER_CHAR = 1.5f; private static final float MAX_BYTES_PER_CHAR = 5.0f; private final UTF7StyleCharset cs; private final Base64Util base64; private final byte shift; private final byte unshift; private final boolean strict; private boolean base64mode; private int bitsToOutput; private int sextet; static boolean useUglyHackToForceCallToFlushInJava5; static { String version = System.getProperty("java.specification.version"); String vendor = System.getProperty("java.vm.vendor"); useUglyHackToForceCallToFlushInJava5 = "1.4".equals(version) || "1.5".equals(version); useUglyHackToForceCallToFlushInJava5 &= "Sun Microsystems Inc.".equals(vendor); } UTF7StyleCharsetEncoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) { super(cs, AVG_BYTES_PER_CHAR, MAX_BYTES_PER_CHAR); this.cs = cs; this.base64 = base64; this.strict = strict; this.shift = cs.shift(); this.unshift = cs.unshift(); } /* * (non-Javadoc) * @see java.nio.charset.CharsetEncoder#implReset() */ protected void implReset() { base64mode = false; sextet = 0; bitsToOutput = 0; } /** * {@inheritDoc} * <p> * Note that this method might return <code>CoderResult.OVERFLOW</code> (as * is required by the specification) if insufficient space is available in * the output buffer. However, calling it again on JDKs before Java 6 * triggers a bug in * {@link java.nio.charset.CharsetEncoder#flush(ByteBuffer)} causing it to * throw an IllegalStateException (the buggy method is <code>final</code>, * thus cannot be overridden). * </p> * * @see <a * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6227608"> * JDK bug 6227608< /a> * @param out The output byte buffer * @return A coder-result object describing the reason for termination */ protected CoderResult implFlush(ByteBuffer out) { if (base64mode) { if (out.remaining() < 2) return CoderResult.OVERFLOW; if (bitsToOutput != 0) out.put(base64.getChar(sextet)); out.put(unshift); } return CoderResult.UNDERFLOW; } /** * {@inheritDoc} * <p> * Note that this method might return <code>CoderResult.OVERFLOW</code>, * even though there is sufficient space available in the output buffer. * This is done to force the broken implementation of * {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)} to call flush * (the buggy method is <code>final</code>, thus cannot be overridden). * </p> * <p> * However, String.getBytes() fails if CoderResult.OVERFLOW is returned, * since this assumes it always allocates sufficient bytes (maxBytesPerChar * * nr_of_chars). Thus, as an extra check, the size of the input buffer is * compared against the size of the output buffer. A static variable is used * to indicate if a broken java version is used. * </p> * <p> * It is not possible to directly write the last few bytes, since more bytes * might be waiting to be encoded then those available in the input buffer. * </p> * * @see <a * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6221056"> * JDK bug 6221056< /a> * @param in The input character buffer * @param out The output byte buffer * @return A coder-result object describing the reason for termination */ protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { while (in.hasRemaining()) { if (out.remaining() < 4) return CoderResult.OVERFLOW; char ch = in.get(); if (cs.canEncodeDirectly(ch)) { unshift(out, ch); out.put((byte)ch); } else if (!base64mode && ch == shift) { out.put(shift); out.put(unshift); } else encodeBase64(ch, out); } /* * <HACK type="ugly"> These lines are required to trick JDK 1.5 and * earlier into flushing when using Charset.encode(String), * Charset.encode(CharBuffer) or CharsetEncoder.encode(CharBuffer) * Without them, the last few bytes may be missing. */ if (base64mode && useUglyHackToForceCallToFlushInJava5 && out.limit() != MAX_BYTES_PER_CHAR * in.limit()) return CoderResult.OVERFLOW; /* </HACK> */ return CoderResult.UNDERFLOW; } /** * <p> * Writes the bytes necessary to leave <i>base 64 mode</i>. This might * include an unshift character. * </p> * * @param out * @param ch */ private void unshift(ByteBuffer out, char ch) { if (!base64mode) return; if (bitsToOutput != 0) out.put(base64.getChar(sextet)); if (base64.contains(ch) || ch == unshift || strict) out.put(unshift); base64mode = false; sextet = 0; bitsToOutput = 0; } /** * <p> * Writes the bytes necessary to encode a character in <i>base 64 mode</i>. * All bytes which are fully determined will be written. The fields * <code>bitsToOutput</code> and <code>sextet</code> are used to remember * the bytes not yet fully determined. * </p> * * @param out * @param ch */ private void encodeBase64(char ch, ByteBuffer out) { if (!base64mode) out.put(shift); base64mode = true; bitsToOutput += 16; while (bitsToOutput >= 6) { bitsToOutput -= 6; sextet += (ch >> bitsToOutput); sextet &= 0x3F; out.put(base64.getChar(sextet)); sextet = 0; } sextet = (ch << (6 - bitsToOutput)) & 0x3F; } }