001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.util;
018
019import java.nio.charset.Charset;
020import java.util.Base64;
021import javax.xml.bind.DatatypeConverter;
022
023import org.springframework.lang.UsesJava8;
024
025/**
026 * A simple utility class for Base64 encoding and decoding.
027 *
028 * <p>Adapts to either Java 8's {@link java.util.Base64} class or Apache Commons Codec's
029 * {@link org.apache.commons.codec.binary.Base64} class. With neither Java 8 nor Commons
030 * Codec present, {@link #encode}/{@link #decode} calls will throw an IllegalStateException.
031 * However, as of Spring 4.2, {@link #encodeToString} and {@link #decodeFromString} will
032 * nevertheless work since they can delegate to the JAXB DatatypeConverter as a fallback.
033 * However, this does not apply when using the "UrlSafe" methods for RFC 4648 "URL and
034 * Filename Safe Alphabet"; a delegate is required.
035 *
036 * <p><em>Note:</em> Apache Commons Codec does not add padding ({@code =}) when encoding
037 * with the URL and Filename Safe Alphabet.
038 *
039 * @author Juergen Hoeller
040 * @author Gary Russell
041 * @since 4.1
042 * @see java.util.Base64
043 * @see org.apache.commons.codec.binary.Base64
044 * @see javax.xml.bind.DatatypeConverter#printBase64Binary
045 * @see javax.xml.bind.DatatypeConverter#parseBase64Binary
046 */
047public abstract class Base64Utils {
048
049        private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
050
051
052        private static final Base64Delegate delegate;
053
054        static {
055                Base64Delegate delegateToUse = null;
056                // JDK 8's java.util.Base64 class present?
057                if (ClassUtils.isPresent("java.util.Base64", Base64Utils.class.getClassLoader())) {
058                        delegateToUse = new JdkBase64Delegate();
059                }
060                // Apache Commons Codec present on the classpath?
061                else if (ClassUtils.isPresent("org.apache.commons.codec.binary.Base64", Base64Utils.class.getClassLoader())) {
062                        delegateToUse = new CommonsCodecBase64Delegate();
063                }
064                delegate = delegateToUse;
065        }
066
067        /**
068         * Assert that Byte64 encoding between byte arrays is actually supported.
069         * @throws IllegalStateException if neither Java 8 nor Apache Commons Codec is present
070         */
071        private static void assertDelegateAvailable() {
072                Assert.state(delegate != null,
073                                "Neither Java 8 nor Apache Commons Codec found - Base64 encoding between byte arrays not supported");
074        }
075
076
077        /**
078         * Base64-encode the given byte array.
079         * @param src the original byte array (may be {@code null})
080         * @return the encoded byte array (or {@code null} if the input was {@code null})
081         * @throws IllegalStateException if Base64 encoding between byte arrays is not
082         * supported, i.e. neither Java 8 nor Apache Commons Codec is present at runtime
083         */
084        public static byte[] encode(byte[] src) {
085                assertDelegateAvailable();
086                return delegate.encode(src);
087        }
088
089        /**
090         * Base64-decode the given byte array.
091         * @param src the encoded byte array (may be {@code null})
092         * @return the original byte array (or {@code null} if the input was {@code null})
093         * @throws IllegalStateException if Base64 encoding between byte arrays is not
094         * supported, i.e. neither Java 8 nor Apache Commons Codec is present at runtime
095         */
096        public static byte[] decode(byte[] src) {
097                assertDelegateAvailable();
098                return delegate.decode(src);
099        }
100
101        /**
102         * Base64-encode the given byte array using the RFC 4648
103         * "URL and Filename Safe Alphabet".
104         * @param src the original byte array (may be {@code null})
105         * @return the encoded byte array (or {@code null} if the input was {@code null})
106         * @throws IllegalStateException if Base64 encoding between byte arrays is not
107         * supported, i.e. neither Java 8 nor Apache Commons Codec is present at runtime
108         * @since 4.2.4
109         */
110        public static byte[] encodeUrlSafe(byte[] src) {
111                assertDelegateAvailable();
112                return delegate.encodeUrlSafe(src);
113        }
114
115        /**
116         * Base64-decode the given byte array using the RFC 4648
117         * "URL and Filename Safe Alphabet".
118         * @param src the encoded byte array (may be {@code null})
119         * @return the original byte array (or {@code null} if the input was {@code null})
120         * @throws IllegalStateException if Base64 encoding between byte arrays is not
121         * supported, i.e. neither Java 8 nor Apache Commons Codec is present at runtime
122         * @since 4.2.4
123         */
124        public static byte[] decodeUrlSafe(byte[] src) {
125                assertDelegateAvailable();
126                return delegate.decodeUrlSafe(src);
127        }
128
129        /**
130         * Base64-encode the given byte array to a String.
131         * @param src the original byte array (may be {@code null})
132         * @return the encoded byte array as a UTF-8 String
133         * (or {@code null} if the input was {@code null})
134         */
135        public static String encodeToString(byte[] src) {
136                if (src == null) {
137                        return null;
138                }
139                if (src.length == 0) {
140                        return "";
141                }
142
143                if (delegate != null) {
144                        // Full encoder available
145                        return new String(delegate.encode(src), DEFAULT_CHARSET);
146                }
147                else {
148                        // JAXB fallback for String case
149                        return DatatypeConverter.printBase64Binary(src);
150                }
151        }
152
153        /**
154         * Base64-decode the given byte array from an UTF-8 String.
155         * @param src the encoded UTF-8 String (may be {@code null})
156         * @return the original byte array (or {@code null} if the input was {@code null})
157         */
158        public static byte[] decodeFromString(String src) {
159                if (src == null) {
160                        return null;
161                }
162                if (src.isEmpty()) {
163                        return new byte[0];
164                }
165
166                if (delegate != null) {
167                        // Full encoder available
168                        return delegate.decode(src.getBytes(DEFAULT_CHARSET));
169                }
170                else {
171                        // JAXB fallback for String case
172                        return DatatypeConverter.parseBase64Binary(src);
173                }
174        }
175
176        /**
177         * Base64-encode the given byte array to a String using the RFC 4648
178         * "URL and Filename Safe Alphabet".
179         * @param src the original byte array (may be {@code null})
180         * @return the encoded byte array as a UTF-8 String
181         * (or {@code null} if the input was {@code null})
182         * @throws IllegalStateException if Base64 encoding between byte arrays is not
183         * supported, i.e. neither Java 8 nor Apache Commons Codec is present at runtime
184         */
185        public static String encodeToUrlSafeString(byte[] src) {
186                assertDelegateAvailable();
187                return new String(delegate.encodeUrlSafe(src), DEFAULT_CHARSET);
188        }
189
190        /**
191         * Base64-decode the given byte array from an UTF-8 String using the RFC 4648
192         * "URL and Filename Safe Alphabet".
193         * @param src the encoded UTF-8 String (may be {@code null})
194         * @return the original byte array (or {@code null} if the input was {@code null})
195         * @throws IllegalStateException if Base64 encoding between byte arrays is not
196         * supported, i.e. neither Java 8 nor Apache Commons Codec is present at runtime
197         */
198        public static byte[] decodeFromUrlSafeString(String src) {
199                assertDelegateAvailable();
200                return delegate.decodeUrlSafe(src.getBytes(DEFAULT_CHARSET));
201        }
202
203
204        interface Base64Delegate {
205
206                byte[] encode(byte[] src);
207
208                byte[] decode(byte[] src);
209
210                byte[] encodeUrlSafe(byte[] src);
211
212                byte[] decodeUrlSafe(byte[] src);
213        }
214
215
216        @UsesJava8
217        static class JdkBase64Delegate implements Base64Delegate {
218
219                @Override
220                public byte[] encode(byte[] src) {
221                        if (src == null || src.length == 0) {
222                                return src;
223                        }
224                        return Base64.getEncoder().encode(src);
225                }
226
227                @Override
228                public byte[] decode(byte[] src) {
229                        if (src == null || src.length == 0) {
230                                return src;
231                        }
232                        return Base64.getDecoder().decode(src);
233                }
234
235                @Override
236                public byte[] encodeUrlSafe(byte[] src) {
237                        if (src == null || src.length == 0) {
238                                return src;
239                        }
240                        return Base64.getUrlEncoder().encode(src);
241                }
242
243                @Override
244                public byte[] decodeUrlSafe(byte[] src) {
245                        if (src == null || src.length == 0) {
246                                return src;
247                        }
248                        return Base64.getUrlDecoder().decode(src);
249                }
250
251        }
252
253
254        static class CommonsCodecBase64Delegate implements Base64Delegate {
255
256                private final org.apache.commons.codec.binary.Base64 base64 =
257                                new org.apache.commons.codec.binary.Base64();
258
259                private final org.apache.commons.codec.binary.Base64 base64UrlSafe =
260                                new org.apache.commons.codec.binary.Base64(0, null, true);
261
262                @Override
263                public byte[] encode(byte[] src) {
264                        return this.base64.encode(src);
265                }
266
267                @Override
268                public byte[] decode(byte[] src) {
269                        return this.base64.decode(src);
270                }
271
272                @Override
273                public byte[] encodeUrlSafe(byte[] src) {
274                        return this.base64UrlSafe.encode(src);
275                }
276
277                @Override
278                public byte[] decodeUrlSafe(byte[] src) {
279                        return this.base64UrlSafe.decode(src);
280                }
281
282        }
283
284}