Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added setting to force UTC timestamps in JpaEventStore

For proper sorting, it is highly recommended to store timestamps in UTC format.
This ensures that timestamps stores in ISO8601 string are sortable.

Issue #AXON-230 Fixed
  • Loading branch information...
commit 0ccd5e47d8ea60c0ade3c5ea408254593de5e83d 1 parent 9d5416e
@abuijze abuijze authored
View
14 .../main/java/org/axonframework/contextsupport/spring/JpaEventStoreBeanDefinitionParser.java
@@ -17,6 +17,7 @@
package org.axonframework.contextsupport.spring;
import org.axonframework.common.jpa.ContainerManagedEntityManagerProvider;
+import org.axonframework.eventstore.jpa.DefaultEventEntryStore;
import org.axonframework.eventstore.jpa.JpaEventStore;
import org.axonframework.serializer.Serializer;
import org.axonframework.serializer.xml.XStreamSerializer;
@@ -42,6 +43,7 @@
*/
public class JpaEventStoreBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
+ private static final String FORCE_UTC_TIMESTAMP_ATTRIBUTE = "force-utc-timestamp";
private UpcasterChainBeanDefinitionParser upcasterChainParser = new UpcasterChainBeanDefinitionParser();
/**
* the event serializer attribute.
@@ -73,7 +75,8 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit
} else {
builder.addConstructorArgValue(
BeanDefinitionBuilder.genericBeanDefinition(ContainerManagedEntityManagerProvider.class)
- .getBeanDefinition());
+ .getBeanDefinition()
+ );
}
Object serializer;
if (element.hasAttribute(EVENT_SERIALIZER_ATTRIBUTE)) {
@@ -83,9 +86,16 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit
}
builder.addConstructorArgValue(serializer);
if (element.hasAttribute(EVENT_ENTRY_STORE_ATTRIBUTE)) {
- Object eventEntryStore = new RuntimeBeanReference(element.getAttribute(EVENT_ENTRY_STORE_ATTRIBUTE));
+ Object eventEntryStore = new RuntimeBeanReference(element.getAttribute(EVENT_ENTRY_STORE_ATTRIBUTE));
builder.addConstructorArgValue(eventEntryStore);
+ } else if (element.hasAttribute(FORCE_UTC_TIMESTAMP_ATTRIBUTE)) {
+ builder.addConstructorArgValue(
+ BeanDefinitionBuilder.genericBeanDefinition(DefaultEventEntryStore.class)
+ .addConstructorArgValue(element.getAttribute(FORCE_UTC_TIMESTAMP_ATTRIBUTE))
+ .getBeanDefinition()
+ );
}
+
if (element.hasAttribute(DATA_SOURCE_ATTRIBUTE)) {
builder.addPropertyReference("dataSource", element.getAttribute(DATA_SOURCE_ATTRIBUTE));
}
View
17 core/src/main/java/org/axonframework/eventstore/jpa/AbstractEventEntry.java
@@ -73,6 +73,20 @@
*/
protected AbstractEventEntry(String type, DomainEventMessage event,
SerializedObject<byte[]> payload, SerializedObject<byte[]> metaData) {
+ this(type, event, event.getTimestamp(), payload, metaData);
+ }
+
+ /**
+ * Initialize an Event entry for the given <code>event</code>.
+ *
+ * @param type The type identifier of the aggregate root the event belongs to
+ * @param event The event to store in the EventStore
+ * @param timestamp The timestamp to store
+ * @param payload The serialized payload of the Event
+ * @param metaData The serialized metaData of the Event
+ */
+ protected AbstractEventEntry(String type, DomainEventMessage event, DateTime timestamp,
+ SerializedObject<byte[]> payload, SerializedObject<byte[]> metaData) {
this.eventIdentifier = event.getIdentifier();
this.type = type;
this.payloadType = payload.getType().getName();
@@ -81,7 +95,7 @@ protected AbstractEventEntry(String type, DomainEventMessage event,
this.aggregateIdentifier = event.getAggregateIdentifier().toString();
this.sequenceNumber = event.getSequenceNumber();
this.metaData = Arrays.copyOf(metaData.getData(), metaData.getData().length);
- this.timeStamp = event.getTimestamp().toString();
+ this.timeStamp = timestamp.toString();
}
/**
@@ -95,6 +109,7 @@ protected AbstractEventEntry() {
*
* @return the Aggregate Identifier of the associated event.
*/
+ @Override
public Object getAggregateIdentifier() {
return aggregateIdentifier;
}
View
30 core/src/main/java/org/axonframework/eventstore/jpa/DefaultEventEntryStore.java
@@ -20,6 +20,7 @@
import org.axonframework.serializer.SerializedDomainEventData;
import org.axonframework.serializer.SerializedObject;
import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,6 +48,31 @@
private static final Logger logger = LoggerFactory.getLogger(DefaultEventEntryStore.class);
+ private final boolean forceUtc;
+
+ /**
+ * Initialize the Event Entry Store, storing timestamps in the system timezone.
+ *
+ * @see #DefaultEventEntryStore(boolean)
+ */
+ public DefaultEventEntryStore() {
+ this(false);
+ }
+
+ /**
+ * Initializes the EventEntryStore, with the possibility to force timestamps to be stored in UTC timezone. Although
+ * it is strongly recommended to set this value to <code>true</code>, it defaults to <code>false</code>, for
+ * backwards compatibility reasons.
+ * <p/>
+ * Providing <code>false</code> will store the timestamps in the system timezone.
+ *
+ * @param forceUtcTimestamp whether to store dates in UTC format.
+ */
+ public DefaultEventEntryStore(boolean forceUtcTimestamp) {
+ this.forceUtc = forceUtcTimestamp;
+ }
+
+
@Override
@SuppressWarnings({"unchecked"})
public void persistEvent(String aggregateType, DomainEventMessage event, SerializedObject serializedPayload,
@@ -112,6 +138,10 @@ public void persistSnapshot(String aggregateType, DomainEventMessage snapshotEve
protected DomainEventEntry createDomainEventEntry(String aggregateType, DomainEventMessage event,
SerializedObject<byte[]> serializedPayload,
SerializedObject<byte[]> serializedMetaData) {
+ if (forceUtc) {
+ return new DomainEventEntry(aggregateType, event, event.getTimestamp().toDateTime(DateTimeZone.UTC),
+ serializedPayload, serializedMetaData);
+ }
return new DomainEventEntry(aggregateType, event, serializedPayload, serializedMetaData);
}
View
18 core/src/main/java/org/axonframework/eventstore/jpa/DomainEventEntry.java
@@ -18,6 +18,7 @@
import org.axonframework.domain.DomainEventMessage;
import org.axonframework.serializer.SerializedObject;
+import org.joda.time.DateTime;
import javax.persistence.Entity;
@@ -51,4 +52,21 @@ public DomainEventEntry(String type, DomainEventMessage event,
SerializedObject<byte[]> metaData) {
super(type, event, payload, metaData);
}
+
+ /**
+ * Initialize an Event entry for the given <code>event</code>.
+ *
+ * @param type The type identifier of the aggregate root the event belongs to
+ * @param event The event to store in the eventstore
+ * @param dateTime The timestamp to store in the Event Store
+ * @param payload The serialized version of the Event
+ * @param metaData The serialized metaData of the Event
+ */
+ public DomainEventEntry(String type, DomainEventMessage event, DateTime dateTime,
+ SerializedObject<byte[]> payload,
+ SerializedObject<byte[]> metaData) {
+ super(type, event, dateTime, payload, metaData);
+ }
+
+
}
View
30 core/src/main/resources/org/axonframework/contextsupport/spring/axon-core-2.2.xsd
@@ -790,6 +790,16 @@
</xsd:annotation>
</xsd:element>
</xsd:all>
+ <xsd:attribute name="force-utc-timestamp" use="optional" type="xsd:boolean">
+ <xsd:annotation>
+ <xsd:documentation>Indicates whether or not the timestamps of events should be forced to
+ UTC. When false, timestamps are stored with the system timezone information. Although
+ it is strongly recommended to set this value to true, it defaults to false, for
+ backwards compatibility reasons. This setting is ignored when an entity-manager-provider
+ attribute is also specified.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attribute name="data-source" use="optional"
type="xsd:string">
<xsd:annotation>
@@ -922,7 +932,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="persistence-exception-resolver" use="optional" type="xsd:string">
- <xsd:annotation>
+ <xsd:annotation>
<xsd:documentation>
Sets the persistence exception resolver to use to detect concurrency related errors in
database generated exceptions.
@@ -936,7 +946,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="connection-provider" use="optional" type="xsd:string">
- <xsd:annotation>
+ <xsd:annotation>
<xsd:documentation>
Reference to the bean providing connections to the backing database. This allows for
more fine-grained control over how connections are managed.
@@ -950,7 +960,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="max-snapshots-archived" use="optional" type="xsd:string">
- <xsd:annotation>
+ <xsd:annotation>
<xsd:documentation>
Sets the maximum number of snapshots to archive for an aggregate. The EventStore will
keep at most this number of snapshots per aggregate. Defaults to 1. A value less than 1
@@ -959,7 +969,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="batch-size" use="optional" type="xsd:string">
- <xsd:annotation>
+ <xsd:annotation>
<xsd:documentation>
Sets the number of events that should be read at each database access. When more than
this number of events must be read to rebuild an aggregate's state, the events are read
@@ -968,16 +978,16 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="sql-schema" use="optional" type="xsd:string">
- <xsd:annotation>
+ <xsd:annotation>
<xsd:documentation>
Reference to the bean that defines the SQL statements to use for different Event Store
operations
</xsd:documentation>
- <xsd:appinfo>
- <tool:annotation kind="ref">
- <tool:expected-type type="org.axonframework.eventstore.jdbc.EventSqlSchema"/>
- </tool:annotation>
- </xsd:appinfo>
+ <xsd:appinfo>
+ <tool:annotation kind="ref">
+ <tool:expected-type type="org.axonframework.eventstore.jdbc.EventSqlSchema"/>
+ </tool:annotation>
+ </xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="event-entry-store-ref" type="xsd:string">
View
25 ...t/java/org/axonframework/contextsupport/spring/JpaEventStoreBeanDefinitionParserTest.java
@@ -16,19 +16,13 @@
package org.axonframework.contextsupport.spring;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import java.util.List;
-
import org.axonframework.eventstore.jpa.EventEntryStore;
import org.axonframework.eventstore.jpa.JpaEventStore;
import org.axonframework.serializer.Serializer;
import org.axonframework.upcasting.LazyUpcasterChain;
import org.axonframework.upcasting.SimpleUpcasterChain;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.*;
+import org.junit.runner.*;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -38,6 +32,10 @@
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:contexts/axon-namespace-support-context.xml"})
public class JpaEventStoreBeanDefinitionParserTest {
@@ -75,11 +73,14 @@ public void jpaEventStore_withGivenUpcasterChainStrategy() {
.getGenericArgumentValue(RuntimeBeanReference.class);
ValueHolder upcasterList = upcasterChainDefinition.getConstructorArgumentValues()
.getIndexedArgumentValue(1, List.class);
+ BeanDefinition eventEntryStoreDefinition = (BeanDefinition) definition.getConstructorArgumentValues()
+ .getArgumentValue(2, EventEntryStore.class)
+ .getValue();
assertNotNull(upcasterList);
assertEquals(1, ((List) upcasterList.getValue()).size());
assertNotNull(converterFactory);
assertEquals("converterFactory", ((RuntimeBeanReference) converterFactory.getValue()).getBeanName());
-
+ assertEquals("true", eventEntryStoreDefinition.getConstructorArgumentValues().getArgumentValue(0, Boolean.class).getValue());
assertNotNull(beanFactory.getBean("eventStore2"));
}
@@ -97,16 +98,16 @@ public void jpaEventStore_withDefaultUpcasterChainStrategy() {
assertNotNull(beanFactory.getBean("eventStore3"));
}
-
+
@Test
public void jpaEventStore_withCustomEventEntryStore() {
BeanDefinition definition = beanFactory.getBeanDefinition("eventStoreWithCustomEventEntryStore");
-
+
ValueHolder reference = definition.getConstructorArgumentValues().getArgumentValue(2, EventEntryStore.class);
assertNotNull("Event entry store reference is wrong", reference);
RuntimeBeanReference beanReference = (RuntimeBeanReference) reference.getValue();
assertEquals("Event entry store reference is wrong", "customEventEntryStore", beanReference.getBeanName());
-
+
assertNotNull(beanFactory.getBean("eventStoreWithCustomEventEntryStore"));
}
}
View
3  core/src/test/resources/contexts/axon-namespace-support-context.xml
@@ -172,7 +172,8 @@
event-serializer="eventSerializer" max-snapshots-archived="2" batch-size="1000"/>
<axon:jpa-event-store id="eventStore2" data-source="dataSource" entity-manager-provider="myEntityManagerProvider"
- event-serializer="eventSerializer" max-snapshots-archived="2" batch-size="1000">
+ event-serializer="eventSerializer" force-utc-timestamp="true" max-snapshots-archived="2"
+ batch-size="1000">
<axon:upcasters converter-factory="converterFactory" strategy="eager">
<bean class="org.axonframework.testutils.MockitoMockFactoryBean">
<property name="mockType" value="org.axonframework.upcasting.Upcaster"/>
Please sign in to comment.
Something went wrong with that request. Please try again.