/** * Copyright (C) 2014-2020 Philip Helger (www.helger.com) * philip[at]helger[dot]com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.helger.css.utils; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.nio.charset.Charset; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.WillNotClose; import javax.annotation.concurrent.NotThreadSafe; import com.helger.commons.ValueEnforcer; import com.helger.commons.annotation.ReturnsMutableCopy; import com.helger.commons.base64.Base64; import com.helger.commons.charset.CharsetHelper; import com.helger.commons.collection.ArrayHelper; import com.helger.commons.equals.EqualsHelper; import com.helger.commons.hashcode.HashCodeGenerator; import com.helger.commons.mime.CMimeType; import com.helger.commons.mime.IMimeType; import com.helger.commons.mime.MimeType; import com.helger.commons.mime.MimeTypeHelper; import com.helger.commons.string.ToStringGenerator; /** * This class represents a single CSS data URL (RFC 2397).<br> * Note: manual serialization is required, because {@link Charset} is not * Serializable. * * @author Philip Helger */ @NotThreadSafe public class CSSDataURL implements Serializable { private IMimeType m_aMimeType; private boolean m_bBase64Encoded; private byte [] m_aContent; private Charset m_aCharset; private String m_sContent; /** * Determine the charset from the passed MIME type. If no charset was found, * return the default charset. * * @param aMimeType * The MIME type to investigate. * @return Never <code>null</code>. */ @Nonnull public static Charset getCharsetFromMimeTypeOrDefault (@Nullable final IMimeType aMimeType) { final Charset ret = MimeTypeHelper.getCharsetFromMimeType (aMimeType); return ret != null ? ret : CSSDataURLHelper.DEFAULT_CHARSET; } /** * Default constructor. Default MIME type, no Base64 encoding and no content. */ public CSSDataURL () { this (CSSDataURLHelper.DEFAULT_MIME_TYPE.getClone (), false, new byte [0], CSSDataURLHelper.DEFAULT_CHARSET, ""); } /** * Constructor * * @param aMimeType * The MIME type to be used. If it contains a charset, this charset * will be used otherwise the default charset will be used. * @param bBase64Encoded * <code>true</code> if the content of this data should be Base64 * encoded. It is recommended to set this to <code>true</code> if you * have binary data like images. * @param aContent * The content of the data URL as a byte array. May not be * <code>null</code>. */ public CSSDataURL (@Nonnull final IMimeType aMimeType, final boolean bBase64Encoded, @Nonnull final byte [] aContent) { this (aMimeType, bBase64Encoded, aContent, getCharsetFromMimeTypeOrDefault (aMimeType), null); } /** * Full constructor * * @param aMimeType * The MIME type to be used. May not be <code>null</code>. If you don't * know provide the default MIME type from * {@link CSSDataURLHelper#DEFAULT_MIME_TYPE}. * @param bBase64Encoded * <code>true</code> if the data URL String representation should be * Base64 encoded, <code>false</code> if not. It is recommended to set * this to <code>true</code> if you have binary data like images. * @param aContent * The content of the data URL as a byte array. May not be * <code>null</code> but may be empty. This content may not be Base64 * encoded! * @param aCharset * The charset to be used to encode the String. May not be * <code>null</code>. The default is * {@link CSSDataURLHelper#DEFAULT_CHARSET}. * @param sContent * The String representation of the content. It must match the byte * array in the specified charset. If this parameter is * <code>null</code> than the String content representation is lazily * created in {@link #getContentAsString()}. */ public CSSDataURL (@Nonnull final IMimeType aMimeType, final boolean bBase64Encoded, @Nonnull final byte [] aContent, @Nonnull final Charset aCharset, @Nullable final String sContent) { ValueEnforcer.notNull (aMimeType, "MimeType"); ValueEnforcer.notNull (aContent, "Content"); ValueEnforcer.notNull (aCharset, "Charset"); // Check if a charset is contained in the MIME type and if it matches the // provided charset final Charset aMimeTypeCharset = MimeTypeHelper.getCharsetFromMimeType (aMimeType); if (aMimeTypeCharset == null) { // No charset found in MIME type if (!aCharset.equals (CSSDataURLHelper.DEFAULT_CHARSET)) { // append charset only if it is not the default charset m_aMimeType = ((MimeType) aMimeType.getClone ()).addParameter (CMimeType.PARAMETER_NAME_CHARSET, aCharset.name ()); } else { // Default charset provided m_aMimeType = aMimeType; } } else { // MIME type has a charset - check if it matches the passed one if (!aMimeTypeCharset.equals (aCharset)) throw new IllegalArgumentException ("The provided charset '" + aCharset.name () + "' differs from the charset in the MIME type: '" + aMimeTypeCharset.name () + "'"); m_aMimeType = aMimeType; } m_bBase64Encoded = bBase64Encoded; m_aContent = ArrayHelper.getCopy (aContent); m_aCharset = aCharset; m_sContent = sContent; } private void writeObject (@Nonnull final ObjectOutputStream out) throws IOException { out.writeObject (m_aMimeType); out.writeBoolean (m_bBase64Encoded); out.writeObject (m_aContent); out.writeUTF (m_aCharset.name ()); out.writeObject (m_sContent); } private void readObject (@Nonnull final ObjectInputStream in) throws IOException, ClassNotFoundException { m_aMimeType = (IMimeType) in.readObject (); m_bBase64Encoded = in.readBoolean (); m_aContent = (byte []) in.readObject (); final String sCharsetName = in.readUTF (); m_aCharset = CharsetHelper.getCharsetFromName (sCharsetName); m_sContent = (String) in.readObject (); } /** * @return The MIME type of the data URL. If none was specified, than the * default MIME Type {@link CSSDataURLHelper#DEFAULT_MIME_TYPE} must * be used. */ @Nonnull public IMimeType getMimeType () { return m_aMimeType; } /** * @return <code>true</code> if the parsed data URL was Base64 encoded or if * this data URL should be Base64 encoded. */ public boolean isBase64Encoded () { return m_bBase64Encoded; } /** * @return The length of the content in bytes. Always ≥ 0. */ @Nonnegative public int getContentLength () { return m_aContent.length; } /** * Get a copy of all content bytes. No Base64 encoding is performed in this * method. * * @return A copy of the binary data of the data URL. Neither * <code>null</code> but maybe empty. */ @Nonnull @ReturnsMutableCopy public byte [] getContentBytes () { return ArrayHelper.getCopy (m_aContent); } /** * Write all the binary content to the passed output stream. No Base64 * encoding is performed in this method. * * @param aOS * The output stream to write to. May not be <code>null</code>. * @throws IOException * from OutputStream */ public void writeContentBytes (@Nonnull @WillNotClose final OutputStream aOS) throws IOException { aOS.write (m_aContent, 0, m_aContent.length); } /** * @return The charset to be used for String encoding. May not be * <code>null</code>. The default is * {@link CSSDataURLHelper#DEFAULT_CHARSET}. */ @Nonnull public Charset getCharset () { return m_aCharset; } /** * Get the data content of this Data URL as String. If no String * representation was provided in the constructor, than it is lazily created * inside this method in which case instances of this class are not * thread-safe. If a non-<code>null</code> String was provided in the * constructor, this object is immutable. No Base64 encoding is performed in * this method. * * @return The content in a String representation using the charset of this * object. Never <code>null</code>. */ @Nonnull public String getContentAsString () { if (m_sContent == null) m_sContent = new String (m_aContent, m_aCharset); return m_sContent; } /** * Get the content as a Base64 encoded {@link String} in the {@link Charset} * specified by {@link #getCharset()}. The encoding is applied independent of * the {@link #isBase64Encoded()} state. * * @return Never <code>null</code>. */ @Nonnull public String getContentAsBase64EncodedString () { // Add Base64 encoded String final byte [] aEncoded = Base64.encodeBytesToBytes (m_aContent); // Print the string in the specified charset return new String (aEncoded, m_aCharset); } /** * Get the data content of this Data URL as String in the specified charset. * No Base64 encoding is performed in this method. * * @param aCharset * The charset to be used. May not be <code>null</code>. * @return The content in a String representation using the provided charset. * Never <code>null</code>. */ @Nonnull public String getContentAsString (@Nonnull final Charset aCharset) { if (m_aCharset.equals (aCharset)) { // Potentially return cached version return getContentAsString (); } return new String (m_aContent, aCharset); } /** * @return The complete representation of the data URL, starting with "data:". * All data is emitted, even if it is the default value. Base64 * encoding is performed in this method. */ @Nonnull public String getAsString () { // Return the non-optimized version return getAsString (false); } /** * @param bOptimizedVersion * <code>true</code> to create optimized version * @return The complete representation of the data URL, starting with "data:". * All data is emitted, even if it is the default value. */ @Nonnull public String getAsString (final boolean bOptimizedVersion) { final StringBuilder aSB = new StringBuilder (CSSDataURLHelper.PREFIX_DATA_URL); if (bOptimizedVersion) { // Do not emit the default, if it is the optimized version if (!m_aMimeType.equals (CSSDataURLHelper.DEFAULT_MIME_TYPE)) if (m_aMimeType.getAsStringWithoutParameters () .equals (CSSDataURLHelper.DEFAULT_MIME_TYPE.getAsStringWithoutParameters ())) { // Emit only the parameters aSB.append (m_aMimeType.getParametersAsString (CSSDataURLHelper.MIME_QUOTING)); } else { // Non-default MIME type aSB.append (m_aMimeType.getAsString (CSSDataURLHelper.MIME_QUOTING)); } } else { // Use URL escaping to quote MIME type parameter values! aSB.append (m_aMimeType.getAsString (CSSDataURLHelper.MIME_QUOTING)); } // Base64 marker if (m_bBase64Encoded) { // Avoid the ";base64" if the content is empty if (m_aContent.length > 0 || !bOptimizedVersion) aSB.append (CSSDataURLHelper.BASE64_MARKER); } // Start content aSB.append (CSSDataURLHelper.SEPARATOR_CONTENT); if (m_aContent.length > 0) { if (m_bBase64Encoded) { // Add Base64 encoded String aSB.append (getContentAsBase64EncodedString ()); } else { // Append String as is aSB.append (getContentAsString ()); } } return aSB.toString (); } @Override public boolean equals (final Object o) { if (o == this) return true; if (o == null || !getClass ().equals (o.getClass ())) return false; final CSSDataURL rhs = (CSSDataURL) o; return m_aMimeType.equals (rhs.m_aMimeType) && m_bBase64Encoded == rhs.m_bBase64Encoded && EqualsHelper.equals (m_aContent, rhs.m_aContent) && m_aCharset.equals (rhs.m_aCharset); } @Override public int hashCode () { return new HashCodeGenerator (this).append (m_aMimeType) .append (m_bBase64Encoded) .append (m_aContent) .append (m_aCharset) .getHashCode (); } @Override public String toString () { return new ToStringGenerator (this).append ("mimeType", m_aMimeType) .append ("base64Encoded", m_bBase64Encoded) .append ("content.length", m_aContent.length) .append ("charset", m_aCharset) .append ("hasStringContent", m_sContent != null) .getToString (); } }