/* * Copyright (c) 2012-2017 Jakub Białek * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.google.code.ssm.transcoders; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.code.ssm.providers.CacheTranscoder; import com.google.code.ssm.providers.CachedObject; import com.google.code.ssm.providers.CachedObjectImpl; /** * * Transcoder responsible to decode and encode objects using default java serialization/deserialization. Before storing * data if size of data is bigger than defined {@link JavaTranscoder#setCompressionThreshold(int)} those data are * compressed using GZIP. This transcoder is similar to SerializingTranscoder in xmemcached or spymemcached. * * @author Jakub Białek * @since 3.0.0 * */ @ToString @EqualsAndHashCode public class JavaTranscoder implements CacheTranscoder { // NO_UCD /** * Default compression threshold value. */ public static final int DEFAULT_COMPRESSION_THRESHOLD = 16384; public static final String DEFAULT_CHARSET = "UTF-8"; private static final Logger LOGGER = LoggerFactory.getLogger(JavaTranscoder.class); // General flags private static final int SERIALIZED = 1; private static final int COMPRESSED = 2; @Getter @Setter private int compressionThreshold = DEFAULT_COMPRESSION_THRESHOLD; @Override public Object decode(final CachedObject d) { byte[] data = d.getData(); if ((d.getFlags() & COMPRESSED) != 0) { data = decompress(d.getData()); } if ((d.getFlags() & SERIALIZED) != 0 && data != null) { return deserialize(data); } else { LOGGER.warn("Cannot decode cached data {} using java transcoder", data); throw new RuntimeException("Cannot decode cached data using java transcoder"); } } @Override public CachedObject encode(final Object o) { byte[] data = serialize(o); int flags = SERIALIZED; if (data.length > getCompressionThreshold()) { byte[] compressed = compress(data); if (compressed.length < data.length) { LOGGER.debug("Compressed {} from {} to {}", new Object[] { o.getClass().getName(), data.length, compressed.length }); data = compressed; flags |= COMPRESSED; } else { LOGGER.info("Compression increased the size of {} from {} to {}", new Object[] { o.getClass().getName(), data.length, compressed.length }); } } return new CachedObjectImpl(flags, data); } /** * Serialize object using java serialization. * * @param o * object to serialize * @return serialized data */ protected byte[] serialize(final Object o) { if (o == null) { throw new NullPointerException("Can't serialize null"); } byte[] data = null; ByteArrayOutputStream bos = null; ObjectOutputStream os = null; try { bos = new ByteArrayOutputStream(); os = new ObjectOutputStream(bos); os.writeObject(o); os.close(); bos.close(); data = bos.toByteArray(); } catch (IOException e) { throw new IllegalArgumentException("Non-serializable object", e); } finally { close(os); close(bos); } return data; } /** * Deserialize given stream using java deserialization. * * @param in * data to deserialize * @return deserialized object */ protected Object deserialize(final byte[] in) { Object o = null; ByteArrayInputStream bis = null; ObjectInputStream is = null; try { if (in != null) { bis = new ByteArrayInputStream(in); is = new ObjectInputStream(bis); o = is.readObject(); is.close(); bis.close(); } } catch (IOException e) { LOGGER.warn(String.format("Caught IOException decoding %d bytes of data", in.length), e); } catch (ClassNotFoundException e) { LOGGER.warn(String.format("Caught CNFE decoding %d bytes of data", in.length), e); } finally { close(is); close(bis); } return o; } /** * Compress the given array of bytes. * * @param in * data to compress */ protected byte[] compress(final byte[] in) { if (in == null) { throw new NullPointerException("Can't compress null"); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); GZIPOutputStream gz = null; try { gz = new GZIPOutputStream(bos); gz.write(in); } catch (IOException e) { throw new RuntimeException("IO exception compressing data", e); } finally { close(gz); close(bos); } return bos.toByteArray(); } /** * Decompress the given array of bytes. * * @param in * data to decompress * @return null if the bytes cannot be decompressed */ protected byte[] decompress(final byte[] in) { if (in == null) { return null; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayInputStream bis = new ByteArrayInputStream(in); GZIPInputStream gis = null; try { gis = new GZIPInputStream(bis); byte[] buf = new byte[8192]; int r = -1; while ((r = gis.read(buf)) > 0) { bos.write(buf, 0, r); } return bos.toByteArray(); } catch (IOException e) { throw new RuntimeException("IO exception decompressing data", e); } finally { close(gis); close(bis); close(bos); } } protected void close(final Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { LOGGER.info(String.format("Unable to close %s", closeable), e); } } } }