diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/PartialDate.java b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/PartialDate.java index 4163952c1..ea2c2c902 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/PartialDate.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/PartialDate.java @@ -1,5 +1,7 @@ package org.navalplanner.business.common.partialtime; +import java.io.Serializable; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -389,4 +391,16 @@ public class PartialDate implements ReadablePartial { + this.granularity); return get(granularity.getMostSpecific()); } + + public Serializable[] getDataForPersistence() { + return new Serializable[] { + new Timestamp(normalizedInstant.getMillis()), + granularity.name() }; + } + + public static PartialDate createFromDataForPersistence(Object... values) { + Timestamp instant = (Timestamp) values[0]; + Granularity granularity = Granularity.valueOf((String) values[1]); + return PartialDate.createFrom(instant).with(granularity); + } } diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/IntervalOfPartialDatesType.java b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/IntervalOfPartialDatesType.java new file mode 100644 index 000000000..d91b660e6 --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/IntervalOfPartialDatesType.java @@ -0,0 +1,135 @@ +package org.navalplanner.business.common.partialtime.hibernate; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import org.hibernate.HibernateException; +import org.hibernate.engine.SessionImplementor; +import org.hibernate.type.CustomType; +import org.hibernate.type.Type; +import org.hibernate.usertype.CompositeUserType; +import org.navalplanner.business.common.partialtime.IntervalOfPartialDates; +import org.navalplanner.business.common.partialtime.PartialDate; + +public class IntervalOfPartialDatesType implements CompositeUserType { + + private static final String[] PROPERTY_NAMES = { "start", "end" }; + private final PartialDateType PARTIAL_DATE_TYPE = new PartialDateType(); + + @Override + public IntervalOfPartialDates assemble(Serializable cached, + SessionImplementor session, + Object owner) throws HibernateException { + Object[] components = (Object[]) cached; + PartialDate start = PARTIAL_DATE_TYPE.assemble( + (Serializable) components[0], owner); + PartialDate end = PARTIAL_DATE_TYPE.assemble( + (Serializable) components[1], owner); + return new IntervalOfPartialDates(start, end); + } + + @Override + public Serializable disassemble(Object value, SessionImplementor session) + throws HibernateException { + IntervalOfPartialDates interval = (IntervalOfPartialDates) value; + return new Object[] { + PARTIAL_DATE_TYPE.disassemble(interval.getStart()), + PARTIAL_DATE_TYPE.disassemble(interval.getEnd()) }; + } + + @Override + public Object deepCopy(Object value) throws HibernateException { + return value; + } + + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + if (x == y) + return true; + if (x == null || y == null) + return false; + return x.equals(y); + } + + @Override + public String[] getPropertyNames() { + return PROPERTY_NAMES; + } + + @Override + public Type[] getPropertyTypes() { + Properties emptyProperties = new Properties(); + CustomType partialDateTypeAsType = new CustomType( + PartialDateType.class, + emptyProperties); + return new Type[] { partialDateTypeAsType, partialDateTypeAsType }; + } + + @Override + public Object getPropertyValue(Object component, int property) + throws HibernateException { + IntervalOfPartialDates interval = (IntervalOfPartialDates) component; + return property == 0 ? interval.getStart() : interval.getEnd(); + } + + @Override + public int hashCode(Object x) throws HibernateException { + return x.hashCode(); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Object nullSafeGet(ResultSet rs, String[] names, + SessionImplementor session, Object owner) + throws HibernateException, SQLException { + PartialDate start = PARTIAL_DATE_TYPE.nullSafeGet(rs, subArray(names, + 0, 2), owner); + PartialDate end = PARTIAL_DATE_TYPE.nullSafeGet(rs, subArray(names, 2, + 2), owner); + if (start == null || end == null) + return null; + return new IntervalOfPartialDates(start, end); + } + + private static String[] subArray(String[] array, int initialPosition, + int size) { + String[] result = new String[size]; + System.arraycopy(array, initialPosition, result, 0, size); + return result; + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, + SessionImplementor session) throws HibernateException, SQLException { + IntervalOfPartialDates interval = (IntervalOfPartialDates) value; + PARTIAL_DATE_TYPE.nullSafeSet(st, interval.getStart(), index); + PARTIAL_DATE_TYPE.nullSafeSet(st, interval.getEnd(), index + 2); + } + + @Override + public Object replace(Object original, Object target, + SessionImplementor session, Object owner) throws HibernateException { + return original; + } + + @Override + public Class returnedClass() { + return IntervalOfPartialDates.class; + } + + @Override + public void setPropertyValue(Object component, int property, Object value) + throws HibernateException { + throw new UnsupportedOperationException( + "IntervalOfPartialDates is immutable"); + } + +} diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/PartialDateType.java b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/PartialDateType.java new file mode 100644 index 000000000..ae01d818d --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/PartialDateType.java @@ -0,0 +1,115 @@ +package org.navalplanner.business.common.partialtime.hibernate; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; + +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.usertype.UserType; +import org.navalplanner.business.common.partialtime.PartialDate; + +/** + * Persists a {@link PartialDate} through hibernate.
+ * @author Óscar González Fernández + */ +public class PartialDateType implements UserType { + // TODO consider the possibility of using an integer for storing the enum + private static final int[] SQL_TYPES = { Types.TIMESTAMP, Types.VARCHAR }; + + @Override + public int[] sqlTypes() { + return SQL_TYPES; + } + + @Override + public Class returnedClass() { + return PartialDate.class; + } + + public static class CachedRepresentation implements Serializable { + private final Serializable[] fields; + + private CachedRepresentation(Serializable[] fields) { + this.fields = fields; + } + + public PartialDate toOriginal() { + return PartialDate.createFromDataForPersistence(fields[0], + fields[1]); + } + } + + @Override + public PartialDate assemble(Serializable cached, Object owner) + throws HibernateException { + CachedRepresentation representation = (CachedRepresentation) cached; + return representation.toOriginal(); + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + PartialDate partialDate = (PartialDate) value; + return new CachedRepresentation(partialDate.getDataForPersistence()); + } + + public Object deepCopy(Object value) throws HibernateException { + return value; + } + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + if (x == y) + return true; + if (x == null || y == null) + return false; + return x.equals(y); + } + + @Override + public int hashCode(Object x) throws HibernateException { + return x.hashCode(); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public PartialDate nullSafeGet(ResultSet rs, String[] names, Object owner) + throws HibernateException, SQLException { + Timestamp timestamp = (Timestamp) Hibernate.TIMESTAMP.nullSafeGet(rs, + names[0]); + String granularity = (String) Hibernate.STRING + .nullSafeGet(rs, names[1]); + if (timestamp == null || granularity == null) + return null; + return PartialDate.createFromDataForPersistence(timestamp, granularity); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index) + throws HibernateException, SQLException { + Timestamp timeToSet = null; + String granularityToSet = null; + PartialDate partialDate = (PartialDate) value; + if (partialDate != null) { + Serializable[] dataForPersistence = partialDate + .getDataForPersistence(); + timeToSet = (Timestamp) dataForPersistence[0]; + granularityToSet = (String) dataForPersistence[1]; + } + Hibernate.TIMESTAMP.nullSafeSet(st, timeToSet, index); + Hibernate.STRING.nullSafeSet(st, granularityToSet, index + 1); + } + + @Override + public Object replace(Object original, Object target, Object owner) + throws HibernateException { + return original; + } +} diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/TimeQuantityType.java b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/TimeQuantityType.java new file mode 100644 index 000000000..0fa7903a8 --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/common/partialtime/hibernate/TimeQuantityType.java @@ -0,0 +1,111 @@ +package org.navalplanner.business.common.partialtime.hibernate; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Iterator; + +import net.sf.json.JSONObject; + +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.usertype.UserType; +import org.navalplanner.business.common.partialtime.TimeQuantity; +import org.navalplanner.business.common.partialtime.PartialDate.Granularity; + +public class TimeQuantityType implements UserType { + + private static final int[] SQL_TYPES = { Types.VARCHAR }; + + @Override + public int[] sqlTypes() { + return SQL_TYPES; + } + + private static String asString(TimeQuantity timeQuantity) { + JSONObject jsonObject = new JSONObject(); + for (Granularity granularity : Granularity.values()) { + Integer value = timeQuantity.valueFor(granularity); + if (value != 0) + jsonObject.put(granularity.name(), value); + } + return jsonObject.toString(); + } + + private static TimeQuantity fromString(String timeQuantityAsString) { + JSONObject jsonObject = JSONObject.fromObject(timeQuantityAsString); + Iterator keys = jsonObject.keys(); + TimeQuantity result = TimeQuantity.empty(); + while(keys.hasNext()){ + Object key = keys.next(); + Object object = jsonObject.get(key); + result = result.plus((Integer) object, Granularity + .valueOf((String) key)); + } + return result; + } + + @Override + public Object assemble(Serializable cached, Object owner) + throws HibernateException { + return fromString((String) cached); + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + return asString((TimeQuantity) value); + } + + @Override + public Object deepCopy(Object value) throws HibernateException { + return value; + } + + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + if (x == y) + return true; + if (x == null || y == null) + return false; + return x.equals(y); + } + + @Override + public int hashCode(Object x) throws HibernateException { + return x.hashCode(); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Object nullSafeGet(ResultSet rs, String[] names, Object owner) + throws HibernateException, SQLException { + String timeQuantityAsString = (String) Hibernate.STRING.nullSafeGet(rs, + names); + return fromString(timeQuantityAsString); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index) + throws HibernateException, SQLException { + Hibernate.STRING.nullSafeSet(st, asString((TimeQuantity) value), index); + } + + @Override + public Object replace(Object original, Object target, Object owner) + throws HibernateException { + return original; + } + + @Override + public Class returnedClass() { + return TimeQuantity.class; + } + +} diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingIntervalOfPartialDates.java b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingIntervalOfPartialDates.java new file mode 100644 index 000000000..41d9f7609 --- /dev/null +++ b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingIntervalOfPartialDates.java @@ -0,0 +1,37 @@ +package org.navalplanner.business.common.test.partialtime; + +import org.navalplanner.business.common.partialtime.IntervalOfPartialDates; + +public class EntityContainingIntervalOfPartialDates { + + private Long id; + + private Long version; + + private IntervalOfPartialDates interval; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public IntervalOfPartialDates getInterval() { + return interval; + } + + public void setInterval(IntervalOfPartialDates interval) { + this.interval = interval; + } + +} diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingPartialDate.java b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingPartialDate.java new file mode 100644 index 000000000..22b6b57f6 --- /dev/null +++ b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingPartialDate.java @@ -0,0 +1,38 @@ +package org.navalplanner.business.common.test.partialtime; + +import org.navalplanner.business.common.partialtime.PartialDate; + +public class EntityContainingPartialDate { + + private Long id; + + private Long version; + + private PartialDate partialDate; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public PartialDate getPartialDate() { + return partialDate; + } + + public void setPartialDate(PartialDate partialDate) { + this.partialDate = partialDate; + } + + +} diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingTimeQuantity.java b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingTimeQuantity.java new file mode 100644 index 000000000..9c0a190b6 --- /dev/null +++ b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/EntityContainingTimeQuantity.java @@ -0,0 +1,37 @@ +package org.navalplanner.business.common.test.partialtime; + +import org.navalplanner.business.common.partialtime.TimeQuantity; + +public class EntityContainingTimeQuantity { + + private Long id; + + private Long version; + + private TimeQuantity timeQuantity; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public TimeQuantity getTimeQuantity() { + return timeQuantity; + } + + public void setTimeQuantity(TimeQuantity timeQuantity) { + this.timeQuantity = timeQuantity; + } + +} diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/PartialDateRelatedClassesHibernateMappingTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/PartialDateRelatedClassesHibernateMappingTest.java new file mode 100644 index 000000000..0f05a35ca --- /dev/null +++ b/navalplanner-business/src/test/java/org/navalplanner/business/common/test/partialtime/PartialDateRelatedClassesHibernateMappingTest.java @@ -0,0 +1,79 @@ +package org.navalplanner.business.common.test.partialtime; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.navalplanner.business.BusinessGlobalNames.BUSINESS_SPRING_CONFIG_FILE; +import static org.navalplanner.business.test.BusinessGlobalNames.BUSINESS_SPRING_CONFIG_TEST_FILE; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.joda.time.LocalDate; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.navalplanner.business.common.partialtime.IntervalOfPartialDates; +import org.navalplanner.business.common.partialtime.PartialDate; +import org.navalplanner.business.common.partialtime.TimeQuantity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { BUSINESS_SPRING_CONFIG_FILE, + BUSINESS_SPRING_CONFIG_TEST_FILE }) +@Transactional +public class PartialDateRelatedClassesHibernateMappingTest { + + @Autowired + private SessionFactory sessionFactory; + + public Session getSession() { + return sessionFactory.getCurrentSession(); + } + + @Test + public void partialDatesCanBeSavedAndRetrieved() { + EntityContainingPartialDate entity = new EntityContainingPartialDate(); + PartialDate partialDate = PartialDate.createFrom(new LocalDate(2000, 4, + 20)); + entity.setPartialDate(partialDate); + getSession().save(entity); + getSession().flush(); + getSession().evict(entity); + EntityContainingPartialDate reloaded = (EntityContainingPartialDate) getSession() + .get(EntityContainingPartialDate.class, entity.getId()); + assertThat(reloaded.getPartialDate(), equalTo(partialDate)); + } + + @Test + public void intervalsCanBeSavedAndRetrieved() { + EntityContainingIntervalOfPartialDates entity = new EntityContainingIntervalOfPartialDates(); + PartialDate start = PartialDate.createFrom(new LocalDate(2000, 4, 20)); + PartialDate end = PartialDate.createFrom(new LocalDate(2001, 4, 23)); + IntervalOfPartialDates original = new IntervalOfPartialDates(start, end); + entity.setInterval(original); + getSession().save(entity); + getSession().flush(); + getSession().evict(entity); + EntityContainingIntervalOfPartialDates reloaded = (EntityContainingIntervalOfPartialDates) getSession() + .get( + EntityContainingIntervalOfPartialDates.class, entity.getId()); + assertThat(reloaded.getInterval(), equalTo(original)); + } + + @Test + public void timeQuantitysCanBeSavedAndRetrieved() { + EntityContainingTimeQuantity entity = new EntityContainingTimeQuantity(); + TimeQuantity duration = new IntervalOfPartialDates(PartialDate.createFrom(new LocalDate(2000, 4, 20)), PartialDate.createFrom(new LocalDate(2001, 4, 23))) + .getDuration(); + entity.setTimeQuantity(duration); + getSession().save(entity); + getSession().flush(); + getSession().evict(entity); + EntityContainingTimeQuantity reloaded = (EntityContainingTimeQuantity) getSession() + .get( + EntityContainingTimeQuantity.class, entity.getId()); + TimeQuantity timeQuantity = reloaded.getTimeQuantity(); + assertThat(timeQuantity, equalTo(duration)); + } +} diff --git a/navalplanner-business/src/test/resources/TestEntities.hbm.xml b/navalplanner-business/src/test/resources/TestEntities.hbm.xml new file mode 100644 index 000000000..d33117824 --- /dev/null +++ b/navalplanner-business/src/test/resources/TestEntities.hbm.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/navalplanner-business/src/test/resources/navalplanner-business-spring-config-test.xml b/navalplanner-business/src/test/resources/navalplanner-business-spring-config-test.xml index 78946c8fd..be89b5f59 100644 --- a/navalplanner-business/src/test/resources/navalplanner-business-spring-config-test.xml +++ b/navalplanner-business/src/test/resources/navalplanner-business-spring-config-test.xml @@ -32,6 +32,9 @@ org/navalplanner/business/orders/entities/Orders.hbm.xml + + TestEntities.hbm.xml +