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