Java tutorial
/** * Copyright (c) 2009 - 2018 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package org.candlepin.hibernate; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.StandardBasicTypes; import org.hibernate.usertype.DynamicParameterizedType; import org.hibernate.usertype.UserType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.Objects; import java.util.Properties; /** * ResultDataUserType handles writing objects that are job results to the resultData column in cp_job. * Initially Candlepin stored serialized Java objects into this column, but later revisions stored JSON * instead. This class takes care of reading in the data irrespective of the storage format. * * It also takes care of casting the data to the correct Java type. Hibernate provides us with the type of * the annotated field via elements of the DynamicParameterizedType class which we can then use to cast the * object we've read back. The JobStatus class that uses this UserType ultimately stores the result data * as an Object but at least readers of the data can downcast correctly later if they know what type the * job is storing. */ public class ResultDataUserType implements UserType, DynamicParameterizedType { private static final Logger log = LoggerFactory.getLogger(ResultDataUserType.class); public static final String JSON_CLASS = "jsonClass"; private ObjectMapper mapper; private Class<?> jsonClass; public ResultDataUserType() { mapper = new ObjectMapper(); Hibernate5Module hbm = new Hibernate5Module(); hbm.enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING); mapper.registerModule(hbm); AnnotationIntrospector primary = new JacksonAnnotationIntrospector(); AnnotationIntrospector secondary = new JaxbAnnotationIntrospector(mapper.getTypeFactory()); AnnotationIntrospector pair = new AnnotationIntrospectorPair(primary, secondary); mapper.setAnnotationIntrospector(pair); SimpleFilterProvider filterProvider = new SimpleFilterProvider(); // We're not going to want any of the JSON filters like DynamicPropertyFilter that we apply elsewhere filterProvider.setFailOnUnknownId(false); mapper.setFilterProvider(filterProvider); } @Override public int[] sqlTypes() { return new int[] { Types.VARBINARY }; } @Override public Class returnedClass() { return jsonClass; } @Override public boolean equals(Object x, Object y) throws HibernateException { return Objects.equals(x, y); } @Override public int hashCode(Object x) throws HibernateException { return Objects.hashCode(x); } @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { byte[] data = StandardBasicTypes.BINARY.nullSafeGet(rs, names[0], session); return deserialize(data); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { StandardBasicTypes.BINARY.nullSafeSet(st, serializeJson(value), index, session); } private Object deserialize(byte[] data) { if (data == null) { return null; } Object result; try { result = deserializeJson(data); } catch (JsonParseException e) { /* If we can't deserialize the result data, try to deserialize it as a Java object since that * is the legacy format. */ log.debug("Could not read result data as JSON. Trying as Java object."); result = deserializeJava(data); } catch (Exception e) { // This catch will also catch IOException which readValue also throws but for low level IO errors // that we likely wouldn't want to send on to deserializeJava log.warn("Could not read result data"); throw new RuntimeException(e); } return result; } @SuppressWarnings("unchecked") private <T> T deserializeJson(byte[] data) throws IOException { try (JsonParser parser = mapper.getFactory().createParser(data)) { try { return (T) mapper.readValue(parser, jsonClass); } catch (JsonMappingException e) { log.warn("Could not deserialize into " + jsonClass.getName() + ". Trying Object.", e); return (T) mapper.readValue(parser, Object.class); } } } @SuppressWarnings("unchecked") private <T> T deserializeJava(byte[] data) { try (ByteArrayInputStream bais = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(bais)) { return (T) ois.readObject(); } catch (Exception e) { throw new RuntimeException(e); } } private byte[] serializeJson(Object value) { byte[] data; if (value == null) { data = null; } else { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonGenerator generator = mapper.getFactory().createGenerator(baos, JsonEncoding.UTF8)) { mapper.writeValue(generator, value); data = baos.toByteArray(); } catch (IOException ioe) { throw new RuntimeException(ioe); } } return data; } private byte[] serializeJava(Object value) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(value); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException("Could not serialize Java object", e); } } @Override public Object deepCopy(Object value) throws HibernateException { // We serialize and then deserialize the object. This is slow but we know nothing about the object // type. Hibernate actually does something similar for mutable objects in its internal types. return deserializeJava(serializeJava(value)); } @Override public boolean isMutable() { // We don't strictly know if the type is mutable or not, but it's prudent to assume it is return true; } @Override public Serializable disassemble(Object value) throws HibernateException { return (byte[]) deepCopy(value); } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return deepCopy(cached); } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return deepCopy(original); } @Override @SuppressWarnings("unchecked") public void setParameterValues(Properties parameters) { ParameterType reader = (ParameterType) parameters.get(PARAMETER_TYPE); if (reader != null) { jsonClass = reader.getReturnedClass(); } else { // The else is encountered if the entity is configured via XML String jsonClassName = (String) parameters.get(JSON_CLASS); try { jsonClass = classForName(jsonClassName, this.getClass()); } catch (ClassNotFoundException exception) { throw new HibernateException("Class not found: " + jsonClass, exception); } } } /** * Load a Class based on a fully-qualified name. Borrowed from an internal Hibernate utility class. * * @param name the name of the class to find * @param caller the class of the caller * @return a Class object corresponding to the name given * @throws ClassNotFoundException if the class is not found */ private Class<?> classForName(String name, Class caller) throws ClassNotFoundException { try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader != null) { return classLoader.loadClass(name); } } catch (Throwable ignore) { } return Class.forName(name, true, caller.getClassLoader()); } }