001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.lang.codec;
020
021import org.apache.shiro.lang.util.ByteSource;
022
023import java.io.ByteArrayOutputStream;
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.FileNotFoundException;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.UnsupportedEncodingException;
030
031/**
032 * Base abstract class that provides useful encoding and decoding operations, especially for character data.
033 *
034 * @since 0.9
035 */
036@SuppressWarnings("checkstyle:BooleanExpressionComplexity")
037public abstract class CodecSupport {
038
039    /**
040     * Shiro's default preferred character encoding, equal to <b><code>UTF-8</code></b>.
041     */
042    public static final String PREFERRED_ENCODING = "UTF-8";
043
044    /**
045     * Converts the specified character array to a byte array using the Shiro's preferred encoding (UTF-8).
046     * <p/>
047     * This is a convenience method equivalent to calling the {@link #toBytes(String, String)} method with a
048     * a wrapping String and {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}, i.e.
049     * <p/>
050     * <code>toBytes( new String(chars), {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING} );</code>
051     *
052     * @param chars the character array to be converted to a byte array.
053     * @return the byte array of the UTF-8 encoded character array.
054     */
055    public static byte[] toBytes(char[] chars) {
056        return toBytes(new String(chars), PREFERRED_ENCODING);
057    }
058
059    /**
060     * Converts the specified character array into a byte array using the specified character encoding.
061     * <p/>
062     * This is a convenience method equivalent to calling the {@link #toBytes(String, String)} method with a
063     * a wrapping String and the specified encoding, i.e.
064     * <p/>
065     * <code>toBytes( new String(chars), encoding );</code>
066     *
067     * @param chars    the character array to be converted to a byte array
068     * @param encoding the character encoding to use to when converting to bytes.
069     * @return the bytes of the specified character array under the specified encoding.
070     * @throws CodecException if the JVM does not support the specified encoding.
071     */
072    public static byte[] toBytes(char[] chars, String encoding) throws CodecException {
073        return toBytes(new String(chars), encoding);
074    }
075
076    /**
077     * Converts the specified source argument to a byte array with Shiro's
078     * {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
079     *
080     * @param source the string to convert to a byte array.
081     * @return the bytes representing the specified string under the {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
082     * @see #toBytes(String, String)
083     */
084    public static byte[] toBytes(String source) {
085        return toBytes(source, PREFERRED_ENCODING);
086    }
087
088    /**
089     * Converts the specified source to a byte array via the specified encoding, throwing a
090     * {@link CodecException CodecException} if the encoding fails.
091     *
092     * @param source   the source string to convert to a byte array.
093     * @param encoding the encoding to use to use.
094     * @return the byte array of the specified source with the given encoding.
095     * @throws CodecException if the JVM does not support the specified encoding.
096     */
097    public static byte[] toBytes(String source, String encoding) throws CodecException {
098        try {
099            return source.getBytes(encoding);
100        } catch (UnsupportedEncodingException e) {
101            String msg = "Unable to convert source [" + source + "] to byte array using "
102                    + "encoding '" + encoding + "'";
103            throw new CodecException(msg, e);
104        }
105    }
106
107    /**
108     * Converts the specified byte array to a String using the {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
109     *
110     * @param bytes the byte array to turn into a String.
111     * @return the specified byte array as an encoded String ({@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}).
112     * @see #toString(byte[], String)
113     */
114    public static String toString(byte[] bytes) {
115        return toString(bytes, PREFERRED_ENCODING);
116    }
117
118    /**
119     * Converts the specified byte array to a String using the specified character encoding.  This implementation
120     * does the same thing as <code>new {@link String#String(byte[], String) String(byte[], encoding)}</code>, but will
121     * wrap any {@link UnsupportedEncodingException} with a nicer runtime {@link CodecException}, allowing you to
122     * decide whether or not you want to catch the exception or let it propagate.
123     *
124     * @param bytes    the byte array to convert to a String
125     * @param encoding the character encoding used to encode the String.
126     * @return the specified byte array as an encoded String
127     * @throws CodecException if the JVM does not support the specified encoding.
128     */
129    public static String toString(byte[] bytes, String encoding) throws CodecException {
130        try {
131            return new String(bytes, encoding);
132        } catch (UnsupportedEncodingException e) {
133            String msg = "Unable to convert byte array to String with encoding '" + encoding + "'.";
134            throw new CodecException(msg, e);
135        }
136    }
137
138    /**
139     * Returns the specified byte array as a character array using the
140     * {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
141     *
142     * @param bytes the byte array to convert to a char array
143     * @return the specified byte array encoded as a character array ({@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}).
144     * @see #toChars(byte[], String)
145     */
146    public static char[] toChars(byte[] bytes) {
147        return toChars(bytes, PREFERRED_ENCODING);
148    }
149
150    /**
151     * Converts the specified byte array to a character array using the specified character encoding.
152     * <p/>
153     * Effectively calls <code>{@link #toString(byte[], String) toString(bytes,encoding)}
154     * .{@link String#toCharArray() toCharArray()};</code>
155     *
156     * @param bytes    the byte array to convert to a String
157     * @param encoding the character encoding used to encode the bytes.
158     * @return the specified byte array as an encoded char array
159     * @throws CodecException if the JVM does not support the specified encoding.
160     */
161    public static char[] toChars(byte[] bytes, String encoding) throws CodecException {
162        return toString(bytes, encoding).toCharArray();
163    }
164
165    /**
166     * Returns {@code true} if the specified object can be easily converted to bytes by instances of this class,
167     * {@code false} otherwise.
168     * <p/>
169     * The default implementation returns {@code true} IFF the specified object is an instance of one of the following
170     * types:
171     * <ul>
172     * <li>{@code byte[]}</li>
173     * <li>{@code char[]}</li>
174     * <li>{@link ByteSource}</li>
175     * <li>{@link String}</li>
176     * <li>{@link File}</li>
177     * </li>{@link InputStream}</li>
178     * </ul>
179     *
180     * @param o the object to test to see if it can be easily converted to a byte array
181     * @return {@code true} if the specified object can be easily converted to bytes by instances of this class,
182     * {@code false} otherwise.
183     * @since 1.0
184     */
185    protected boolean isByteSource(Object o) {
186        return o instanceof byte[] || o instanceof char[] || o instanceof String
187                || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
188    }
189
190    /**
191     * Converts the specified Object into a byte array.
192     * <p/>
193     * If the argument is a {@code byte[]}, {@code char[]}, {@link ByteSource}, {@link String}, {@link File}, or
194     * {@link InputStream}, it will be converted automatically and returned.}
195     * <p/>
196     * If the argument is anything other than these types, it is passed to the
197     * {@link #objectToBytes(Object) objectToBytes} method which must be overridden by subclasses.
198     *
199     * @param object the Object to convert into a byte array
200     * @return a byte array representation of the Object argument.
201     */
202    protected byte[] toBytes(Object object) {
203        if (object == null) {
204            String msg = "Argument for byte conversion cannot be null.";
205            throw new IllegalArgumentException(msg);
206        }
207        if (object instanceof byte[]) {
208            return (byte[]) object;
209        } else if (object instanceof ByteSource) {
210            return ((ByteSource) object).getBytes();
211        } else if (object instanceof char[]) {
212            return toBytes((char[]) object);
213        } else if (object instanceof String) {
214            return toBytes((String) object);
215        } else if (object instanceof File) {
216            return toBytes((File) object);
217        } else if (object instanceof InputStream) {
218            return toBytes((InputStream) object);
219        } else {
220            return objectToBytes(object);
221        }
222    }
223
224    /**
225     * Converts the specified Object into a String.
226     * <p/>
227     * If the argument is a {@code byte[]} or {@code char[]} it will be converted to a String using the
228     * {@link #PREFERRED_ENCODING}.  If a String, it will be returned as is.
229     * <p/>
230     * If the argument is anything other than these three types, it is passed to the
231     * {@link #objectToString(Object) objectToString} method.
232     *
233     * @param o the Object to convert into a byte array
234     * @return a byte array representation of the Object argument.
235     */
236    protected String toString(Object o) {
237        if (o == null) {
238            String msg = "Argument for String conversion cannot be null.";
239            throw new IllegalArgumentException(msg);
240        }
241        if (o instanceof byte[]) {
242            return toString((byte[]) o);
243        } else if (o instanceof char[]) {
244            return new String((char[]) o);
245        } else if (o instanceof String) {
246            return (String) o;
247        } else {
248            return objectToString(o);
249        }
250    }
251
252    protected byte[] toBytes(File file) {
253        if (file == null) {
254            throw new IllegalArgumentException("File argument cannot be null.");
255        }
256        try {
257            return toBytes(new FileInputStream(file));
258        } catch (FileNotFoundException e) {
259            String msg = "Unable to acquire InputStream for file [" + file + "]";
260            throw new CodecException(msg, e);
261        }
262    }
263
264    /**
265     * Converts the specified {@link InputStream InputStream} into a byte array.
266     *
267     * @param in the InputStream to convert to a byte array
268     * @return the bytes of the input stream
269     * @throws IllegalArgumentException if the {@code InputStream} argument is {@code null}.
270     * @throws CodecException           if there is any problem reading from the {@link InputStream}.
271     * @since 1.0
272     */
273    protected byte[] toBytes(InputStream in) {
274        if (in == null) {
275            throw new IllegalArgumentException("InputStream argument cannot be null.");
276        }
277        final int bufferSize = 512;
278        ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
279        byte[] buffer = new byte[bufferSize];
280        int bytesRead;
281        try {
282            while ((bytesRead = in.read(buffer)) != -1) {
283                out.write(buffer, 0, bytesRead);
284            }
285            return out.toByteArray();
286        } catch (IOException ioe) {
287            throw new CodecException(ioe);
288        } finally {
289            try {
290                in.close();
291            } catch (IOException ignored) {
292            }
293            try {
294                out.close();
295            } catch (IOException ignored) {
296            }
297        }
298    }
299
300    /**
301     * Default implementation throws a CodecException immediately since it can't infer how to convert the Object
302     * to a byte array.  This method must be overridden by subclasses if anything other than the three default
303     * types (listed in the {@link #toBytes(Object) toBytes(Object)} JavaDoc) are to be converted to a byte array.
304     *
305     * @param o the Object to convert to a byte array.
306     * @return a byte array representation of the Object argument.
307     */
308    protected byte[] objectToBytes(Object o) {
309        String msg = "The " + getClass().getName() + " implementation only supports conversion to "
310                + "byte[] if the source is of type byte[], char[], String, " + ByteSource.class.getName()
311                + " File or InputStream.  The instance provided as a method "
312                + "argument is of type [" + o.getClass().getName() + "].  If you would like to convert "
313                + "this argument type to a byte[], you can 1) convert the argument to one of the supported types "
314                + "yourself and then use that as the method argument or 2) subclass " + getClass().getName()
315                + "and override the objectToBytes(Object o) method.";
316        throw new CodecException(msg);
317    }
318
319    /**
320     * Default implementation merely returns <code>objectArgument.toString()</code>.  Subclasses can override this
321     * method for different mechanisms of converting an object to a String.
322     *
323     * @param o the Object to convert to a byte array.
324     * @return a String representation of the Object argument.
325     */
326    protected String objectToString(Object o) {
327        return o.toString();
328    }
329}