| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.ode.daohib.bpel.hobj; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.InputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.Serializable; |
| import java.sql.SQLException; |
| import java.sql.PreparedStatement; |
| import java.sql.ResultSet; |
| import java.sql.Types; |
| import java.util.zip.GZIPInputStream; |
| import java.util.zip.GZIPOutputStream; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.hibernate.usertype.UserType; |
| |
| /** |
| * Custom Hibernate datatype that compresses (GZip) byte arrays |
| * to increase performance and save disk space. |
| */ |
| public class GZipDataType implements UserType { |
| private static final Logger log = LoggerFactory.getLogger(GZipDataType.class); |
| |
| public static final int[] SQL_TYPES = new int[] { Types.BLOB }; |
| |
| public static final Class RETURNED_CLASS = new byte[0].getClass(); |
| |
| /** For backward compatibility with non-zipped data, prefix the gzip stream with a magic sequence */ |
| public static final byte[] GZIP_PREFIX = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x03, 0x02, 0x01, 0x00 }; |
| |
| // Compression statistics |
| private static long _totalBytesBefore = 0; |
| private static long _totalBytesAfter = 0; |
| private static volatile long _lastLogTime = 0; |
| private static final Object STATS_LOCK = new Object(); |
| |
| private static volatile boolean _compressionEnabled = System.getProperty("org.apache.ode.daohib.bpel.hobj.GZipDataType.enabled", "true").equalsIgnoreCase("true"); |
| |
| /** Reconstruct an object from the cacheable representation */ |
| public Object assemble(Serializable cached, Object owner) { |
| // serializable representation is same |
| return cached; |
| } |
| |
| /** Transform the object into its cacheable representation */ |
| public Serializable disassemble(Object value) { |
| // as-is |
| return (Serializable) value; |
| } |
| |
| /** Return a deep copy of the persistent state */ |
| public Object deepCopy(Object value) { |
| if (value == null) return null; |
| return ((byte[]) value).clone(); |
| } |
| |
| /** Compare two instances of the class mapped by this type for persistence "equality". */ |
| public boolean equals(Object x, Object y) { |
| byte[] buf1 = (byte[]) x; |
| byte[] buf2 = (byte[]) y; |
| if (buf1 == buf2) return true; |
| if (buf1 == null && buf2 != null) return false; |
| if (buf1 != null && buf2 == null) return false; |
| if (buf1.length != buf2.length) return false; |
| for (int i=0; i<buf1.length; i++) { |
| if (buf1[i] != buf2[i]) return false; |
| } |
| return true; |
| } |
| |
| /** Get a hashcode for the instance, consistent with persistence "equality" */ |
| public int hashCode(Object x) { |
| if (x == null) return 0; |
| byte[] buf = (byte[]) x; |
| int hash = 0; |
| for (int i=0; i<buf.length; i++) { |
| hash += buf[i]; |
| } |
| return hash; |
| } |
| |
| /** Are objects of this type mutable? */ |
| public boolean isMutable() { |
| return false; |
| } |
| |
| /** Retrieve an instance of the mapped class from a JDBC resultset. */ |
| public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException { |
| if (names.length != 1) throw new IllegalStateException("Expected a single column name instead of "+names.length); |
| byte[] buf = rs.getBytes(names[0]); |
| if (buf == null) { |
| return null; |
| } |
| if (buf.length >= GZIP_PREFIX.length) { |
| boolean gzip = true; |
| for (int i=0; i<GZIP_PREFIX.length; i++) { |
| if (buf[i] != GZIP_PREFIX[i]) { |
| gzip = false; |
| break; |
| } |
| } |
| if (gzip) { |
| buf = gunzip(new ByteArrayInputStream(buf, GZIP_PREFIX.length, buf.length-GZIP_PREFIX.length)); |
| } |
| } |
| return buf; |
| } |
| |
| /** Write an instance of the mapped class to a prepared statement. */ |
| public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException { |
| byte[] buf = (byte[]) value; |
| if (buf != null) { |
| synchronized (STATS_LOCK) { |
| if (_totalBytesBefore > Integer.MAX_VALUE) { |
| // prevent overflow - renormalize to percent value |
| _totalBytesAfter = _totalBytesAfter*100/_totalBytesBefore; |
| _totalBytesBefore = 100; |
| } |
| _totalBytesBefore += buf.length; |
| } |
| // only try to zip if we have more than 100 bytes |
| if (buf != null && buf.length > 100 && _compressionEnabled) { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(buf.length); |
| for (int i=0; i<GZIP_PREFIX.length; i++) { |
| baos.write(GZIP_PREFIX[i]); |
| } |
| gzip((byte[]) value, baos); |
| byte[] zipped = baos.toByteArray(); |
| // only use zipped representation if we gain 2% or more |
| if (zipped.length*100/buf.length < 99) { |
| buf = zipped; |
| } |
| } |
| synchronized (STATS_LOCK) { |
| _totalBytesAfter += buf.length; |
| } |
| if (log.isDebugEnabled()) { |
| long now = System.currentTimeMillis(); |
| if (_lastLogTime+5000 < now) { |
| log.debug("Average compression ratio: "+ (_totalBytesAfter*100/_totalBytesBefore)+"%"); |
| _lastLogTime = now; |
| } |
| } |
| } |
| st.setBytes(index, buf); |
| } |
| |
| /** During merge, replace the existing (target) value in the entity we are |
| * merging to with a new (original) value from the detached entity we are merging. |
| */ |
| public Object replace(Object original, Object target, Object owner) { |
| return original; |
| } |
| |
| /** The class returned by nullSafeGet(). */ |
| public Class returnedClass() { |
| return RETURNED_CLASS; |
| } |
| |
| /** Return the SQL type codes for the columns mapped by this type. */ |
| public int[] sqlTypes() { |
| return SQL_TYPES; |
| } |
| |
| /** |
| * Compress (using gzip algorithm) a byte array into an output stream. |
| */ |
| public static void gzip(byte[] content, OutputStream out) { |
| try { |
| GZIPOutputStream zip = new GZIPOutputStream(out); |
| zip.write(content, 0, content.length); |
| zip.finish(); |
| zip.close(); |
| } catch (IOException ex) { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| /** |
| * Decompress (using gzip algorithm) a byte array. |
| */ |
| public static byte[] gunzip(InputStream input) { |
| try { |
| GZIPInputStream unzip = new GZIPInputStream(input); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024); |
| byte[] buf = new byte[4096]; |
| int len; |
| while ((len = unzip.read(buf)) > 0) { |
| baos.write(buf, 0, len); |
| } |
| unzip.close(); |
| return baos.toByteArray(); |
| } catch (IOException ex) { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| } |