001package ezvcard.util;
002
003import java.io.UnsupportedEncodingException;
004import java.util.Arrays;
005
006import ezvcard.Messages;
007import ezvcard.util.org.apache.commons.codec.binary.Base64;
008
009/*
010 Copyright (c) 2012-2023, Michael Angstadt
011 All rights reserved.
012
013 Redistribution and use in source and binary forms, with or without
014 modification, are permitted provided that the following conditions are met: 
015
016 1. Redistributions of source code must retain the above copyright notice, this
017 list of conditions and the following disclaimer. 
018 2. Redistributions in binary form must reproduce the above copyright notice,
019 this list of conditions and the following disclaimer in the documentation
020 and/or other materials provided with the distribution. 
021
022 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
023 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
024 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
025 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
026 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
027 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
028 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
029 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
030 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
031 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
032
033 The views and conclusions contained in the software and documentation are those
034 of the authors and should not be interpreted as representing official policies, 
035 either expressed or implied, of the FreeBSD Project.
036 */
037
038/**
039 * <p>
040 * Represents a data URI.
041 * </p>
042 * <p>
043 * Example: {@code data:image/jpeg;base64,[base64 string]}
044 * </p>
045 * @author Michael Angstadt
046 */
047public final class DataUri {
048        private final byte[] data;
049        private final String text;
050        private final String contentType;
051
052        /**
053         * Creates a data URI.
054         * @param contentType the content type of the data (e.g. "image/png")
055         * @param data the data
056         */
057        public DataUri(String contentType, byte[] data) {
058                this(contentType, data, null);
059        }
060
061        /**
062         * Creates a data URI.
063         * @param contentType the content type of the text (e.g. "text/html")
064         * @param text the text
065         */
066        public DataUri(String contentType, String text) {
067                this(contentType, null, text);
068        }
069
070        /**
071         * Creates a data URI with a content type of "text/plain".
072         * @param text the text
073         */
074        public DataUri(String text) {
075                this("text/plain", text);
076        }
077
078        /**
079         * Copies a data URI.
080         * @param original the data URI to copy
081         */
082        public DataUri(DataUri original) {
083                this(original.contentType, (original.data == null) ? null : original.data.clone(), original.text);
084        }
085
086        private DataUri(String contentType, byte[] data, String text) {
087                this.contentType = (contentType == null) ? "" : contentType.toLowerCase();
088                this.data = data;
089                this.text = text;
090        }
091
092        /**
093         * Parses a data URI string.
094         * @param uri the URI string (e.g. "data:image/jpeg;base64,[base64 string]")
095         * @return the parsed data URI
096         * @throws IllegalArgumentException if the string is not a valid data URI or
097         * it cannot be parsed
098         */
099        public static DataUri parse(String uri) {
100                //Syntax: data:[<media type>][;charset=<character set>][;base64],<data>
101
102                String scheme = "data:";
103                if (uri.length() < scheme.length() || !uri.substring(0, scheme.length()).equalsIgnoreCase(scheme)) {
104                        //not a data URI
105                        throw Messages.INSTANCE.getIllegalArgumentException(18, scheme);
106                }
107
108                String contentType = null;
109                String charset = null;
110                boolean base64 = false;
111                String dataStr = null;
112                int tokenStart = scheme.length();
113                for (int i = scheme.length(); i < uri.length(); i++) {
114                        char c = uri.charAt(i);
115
116                        if (c == ';') {
117                                String token = uri.substring(tokenStart, i);
118                                if (contentType == null) {
119                                        contentType = token.toLowerCase();
120                                } else {
121                                        if (token.toLowerCase().startsWith("charset=")) {
122                                                int equals = token.indexOf('=');
123                                                charset = token.substring(equals + 1);
124                                        } else if ("base64".equalsIgnoreCase(token)) {
125                                                base64 = true;
126                                        }
127                                }
128                                tokenStart = i + 1;
129                                continue;
130                        }
131
132                        if (c == ',') {
133                                String token = uri.substring(tokenStart, i);
134                                if (contentType == null) {
135                                        contentType = token.toLowerCase();
136                                } else {
137                                        if (token.toLowerCase().startsWith("charset=")) {
138                                                int equals = token.indexOf('=');
139                                                charset = token.substring(equals + 1);
140                                        } else if ("base64".equalsIgnoreCase(token)) {
141                                                base64 = true;
142                                        }
143                                }
144
145                                dataStr = uri.substring(i + 1);
146                                break;
147                        }
148                }
149
150                if (dataStr == null) {
151                        throw Messages.INSTANCE.getIllegalArgumentException(20);
152                }
153
154                String text = null;
155                byte[] data = null;
156                if (base64) {
157                        dataStr = dataStr.replaceAll("\\s", "");
158                        data = Base64.decodeBase64(dataStr);
159                        if (charset != null) {
160                                try {
161                                        text = new String(data, charset);
162                                } catch (UnsupportedEncodingException e) {
163                                        throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(43, charset), e);
164                                }
165                                data = null;
166                        }
167                } else {
168                        text = dataStr;
169                }
170
171                return new DataUri(contentType, data, text);
172        }
173
174        /**
175         * Gets the binary data.
176         * @return the binary data or null if the value was text
177         */
178        public byte[] getData() {
179                return data;
180        }
181
182        /**
183         * Gets the content type.
184         * @return the content type (e.g. "image/png")
185         */
186        public String getContentType() {
187                return contentType;
188        }
189
190        /**
191         * Gets the text value.
192         * @return the text value or null if the value was binary
193         */
194        public String getText() {
195                return text;
196        }
197
198        /**
199         * Creates a data URI string.
200         * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]")
201         */
202        @Override
203        public String toString() {
204                return toString(null);
205        }
206
207        /**
208         * Creates a data URI string.
209         * @param charset only applicable if the data URI's value is text. Defines
210         * the character set to encode the text in, or null not to specify a
211         * character set
212         * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]")
213         * @throws IllegalArgumentException if the given character set is not
214         * supported by this JVM
215         */
216        public String toString(String charset) {
217                StringBuilder sb = new StringBuilder();
218                sb.append("data:");
219                sb.append(contentType);
220
221                if (data != null) {
222                        sb.append(";base64,");
223                        sb.append(Base64.encodeBase64String(data));
224                } else if (text != null) {
225                        if (charset == null) {
226                                sb.append(',').append(text);
227                        } else {
228                                byte[] data;
229                                try {
230                                        data = text.getBytes(charset);
231                                } catch (UnsupportedEncodingException e) {
232                                        throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(44, charset), e);
233                                }
234
235                                sb.append(";charset=").append(charset);
236                                sb.append(";base64,");
237                                sb.append(Base64.encodeBase64String(data));
238                        }
239                } else {
240                        sb.append(',');
241                }
242
243                return sb.toString();
244        }
245
246        @Override
247        public int hashCode() {
248                final int prime = 31;
249                int result = 1;
250                result = prime * result + contentType.hashCode();
251                result = prime * result + Arrays.hashCode(data);
252                result = prime * result + ((text == null) ? 0 : text.hashCode());
253                return result;
254        }
255
256        @Override
257        public boolean equals(Object obj) {
258                if (this == obj) return true;
259                if (obj == null) return false;
260                if (getClass() != obj.getClass()) return false;
261                DataUri other = (DataUri) obj;
262                if (!contentType.equals(other.contentType)) return false;
263                if (!Arrays.equals(data, other.data)) return false;
264                if (text == null) {
265                        if (other.text != null) return false;
266                } else if (!text.equals(other.text)) return false;
267                return true;
268        }
269}