/* * Copyright (c) 2017 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.magma.datasource.hibernate.type; import java.io.StringReader; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.Map; import javax.annotation.Nullable; import org.dom4j.Node; import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.engine.spi.Mapping; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.metamodel.relational.Size; import org.hibernate.type.AbstractType; import org.obiba.magma.Value; import org.obiba.magma.ValueType; import com.google.common.base.Strings; /** * A Hibernate Type for persisting {@code Value} instances. The strategy uses 3 columns: * <ul> * <li>value_type: stores the name of the ValueType</li> * <li>is_sequence: stores true when the {@code Value} is a {@code ValueSequence},false otherwise.</li> * <li>value: stores the value returned by {@code value.toString()}</li> * </ul> */ public class ValueHibernateType extends AbstractType { private static final long serialVersionUID = 1L; @Override public int getColumnSpan(Mapping mapping) throws MappingException { return 3; } @Override public String getName() { return "Value"; } @Override public boolean isDirty(Object old, Object current, boolean[] checkable, SessionImplementor session) throws HibernateException { return !old.equals(current); } @Override public boolean isMutable() { // Value instances are immutable return false; } @Override public Object nullSafeGet(ResultSet rs, String name, SessionImplementor session, Object owner) throws HibernateException, SQLException { throw new UnsupportedOperationException(); } @Nullable @Override public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { String valueTypeName = rs.getString(names[0]); // Even when the column is NOT NULL, a SELECT statement can return NULL (using a left join for example). // When this column is null, we cannot construct a valid {@code Value} instance, so this method returns null if(valueTypeName == null) { return null; } ValueType valueType = ValueType.Factory.forName(valueTypeName); boolean isSequence = rs.getBoolean(names[1]); String stringValue = rs.getString(names[2]); return isSequence ? valueType.sequenceOf(stringValue) : valueType.valueOf(stringValue); } @Override public void nullSafeSet(PreparedStatement st, Object obj, int index, boolean[] settable, SessionImplementor session) throws HibernateException, SQLException { Value value = (Value) obj; int offset = 0; if(settable[0]) { st.setString(index + offset++, value.getValueType().getName()); } if(settable[1]) { st.setBoolean(index + offset++, value.isSequence()); } if(settable[2]) { st.setString(index + offset, value.toString()); } } @Override public void nullSafeSet(PreparedStatement st, Object obj, int index, SessionImplementor session) throws HibernateException, SQLException { Value value = (Value) obj; st.setString(index, value.getValueType().getName()); st.setBoolean(index + 1, value.isSequence()); String stringValue = Strings.nullToEmpty(value.isNull() ? null : value.toString()); st.setClob(index + 2, new StringReader(stringValue), stringValue.length()); } @Override public Class<?> getReturnedClass() { return Value.class; } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public Object replace(Object original, Object target, SessionImplementor session, Object owner, Map copyCache) throws HibernateException { // It is safe to return the original parameter since Value instances are immutable return original; } @Override public Object fromXMLNode(Node xml, Mapping factory) throws HibernateException { throw new UnsupportedOperationException(); } @Override public void setToXMLNode(Node node, Object value, SessionFactoryImplementor factory) throws HibernateException { throw new UnsupportedOperationException(); } @Override public int[] sqlTypes(Mapping mapping) throws MappingException { return new int[] { Types.VARCHAR, Types.BIT, Types.CLOB }; } @Override public boolean[] toColumnNullness(Object value, Mapping mapping) { return new boolean[] { false, false, false }; } @Override public String toLoggableString(Object value, SessionFactoryImplementor factory) throws HibernateException { return value.toString(); } @Override public Object deepCopy(Object value, SessionFactoryImplementor factory) throws HibernateException { if(value == null) return null; return ((Value) value).copy(); } @Override public Size[] dictatedSizes(Mapping mapping) throws MappingException { return defaultSizes(mapping); } @Override public Size[] defaultSizes(Mapping mapping) throws MappingException { return new Size[] { // new Size(Size.DEFAULT_PRECISION, Size.DEFAULT_SCALE, Size.DEFAULT_LENGTH, Size.LobMultiplier.NONE), // 255 new Size(Size.DEFAULT_PRECISION, Size.DEFAULT_SCALE, 1, Size.LobMultiplier.NONE), // 1 new Size(Size.DEFAULT_PRECISION, Size.DEFAULT_SCALE, 1, Size.LobMultiplier.G) // 1GB }; } }