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}