Merge pull request #1 from crain/develop
Sync with latest Mifos I/O
diff --git a/api/src/main/java/io/mifos/rhythm/api/v1/client/RhythmManager.java b/api/src/main/java/io/mifos/rhythm/api/v1/client/RhythmManager.java
index 4b10d4f..a4f0981 100644
--- a/api/src/main/java/io/mifos/rhythm/api/v1/client/RhythmManager.java
+++ b/api/src/main/java/io/mifos/rhythm/api/v1/client/RhythmManager.java
@@ -17,6 +17,7 @@
import io.mifos.core.api.util.CustomFeignClientsConfiguration;
import io.mifos.rhythm.api.v1.domain.Beat;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
@@ -31,6 +32,21 @@
@SuppressWarnings("unused")
@FeignClient(value="rhythm-v1", path="/rhythm/v1", configuration = CustomFeignClientsConfiguration.class)
public interface RhythmManager {
+ @RequestMapping(
+ value = "/clockoffset",
+ method = RequestMethod.PUT,
+ produces = MediaType.APPLICATION_JSON_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ void setClockOffset(ClockOffset clockOffset);
+
+ @RequestMapping(
+ value = "/clockoffset",
+ method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_JSON_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ ClockOffset getClockOffset();
@RequestMapping(
value = "/applications/{applicationidentifier}",
diff --git a/api/src/main/java/io/mifos/rhythm/api/v1/domain/ClockOffset.java b/api/src/main/java/io/mifos/rhythm/api/v1/domain/ClockOffset.java
new file mode 100644
index 0000000..fbb8132
--- /dev/null
+++ b/api/src/main/java/io/mifos/rhythm/api/v1/domain/ClockOffset.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.api.v1.domain;
+
+import org.hibernate.validator.constraints.Range;
+
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class ClockOffset {
+ @Range(min = 0, max = 23)
+ private Integer hours;
+
+ @Range(min = 0, max = 59)
+ private Integer minutes;
+
+ @Range(min = 0, max = 59)
+ private Integer seconds;
+
+ public ClockOffset() {
+ this.hours = 0;
+ this.minutes = 0;
+ this.seconds = 0;
+ }
+
+ public ClockOffset(Integer hours, Integer minutes) {
+ this.hours = hours;
+ this.minutes = minutes;
+ this.seconds = 0;
+ }
+
+ public ClockOffset(Integer hours, Integer minutes, Integer seconds) {
+ this.hours = hours;
+ this.minutes = minutes;
+ this.seconds = seconds;
+ }
+
+ public Integer getHours() {
+ return hours;
+ }
+
+ public void setHours(Integer hours) {
+ this.hours = hours;
+ }
+
+ public Integer getMinutes() {
+ return minutes;
+ }
+
+ public void setMinutes(Integer minutes) {
+ this.minutes = minutes;
+ }
+
+ public Integer getSeconds() {
+ return seconds;
+ }
+
+ public void setSeconds(Integer seconds) {
+ this.seconds = seconds;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ClockOffset that = (ClockOffset) o;
+ return Objects.equals(hours, that.hours) &&
+ Objects.equals(minutes, that.minutes) &&
+ Objects.equals(seconds, that.seconds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hours, minutes, seconds);
+ }
+
+ @Override
+ public String toString() {
+ return "ClockOffset{" +
+ "hours=" + hours +
+ ", minutes=" + minutes +
+ ", seconds=" + seconds +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/rhythm/api/v1/events/EventConstants.java b/api/src/main/java/io/mifos/rhythm/api/v1/events/EventConstants.java
index 878999c..9f84439 100644
--- a/api/src/main/java/io/mifos/rhythm/api/v1/events/EventConstants.java
+++ b/api/src/main/java/io/mifos/rhythm/api/v1/events/EventConstants.java
@@ -27,8 +27,10 @@
String POST_BEAT = "post-beat";
String DELETE_APPLICATION = "delete-application";
String DELETE_BEAT = "delete-beat";
+ String PUT_CLOCKOFFSET = "put-clockoffset";
String SELECTOR_INITIALIZE = SELECTOR_NAME + " = '" + INITIALIZE + "'";
String SELECTOR_POST_BEAT = SELECTOR_NAME + " = '" + POST_BEAT + "'";
String SELECTOR_DELETE_APPLICATION = SELECTOR_NAME + " = '" + DELETE_APPLICATION + "'";
String SELECTOR_DELETE_BEAT = SELECTOR_NAME + " = '" + DELETE_BEAT + "'";
+ String SELECTOR_PUT_CLOCKOFFSET = SELECTOR_NAME + " = '" + PUT_CLOCKOFFSET + "'";
}
diff --git a/api/src/test/java/io/mifos/rhythm/api/v1/domain/ClockOffsetTest.java b/api/src/test/java/io/mifos/rhythm/api/v1/domain/ClockOffsetTest.java
new file mode 100644
index 0000000..62bdc52
--- /dev/null
+++ b/api/src/test/java/io/mifos/rhythm/api/v1/domain/ClockOffsetTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.api.v1.domain;
+
+import io.mifos.core.test.domain.ValidationTest;
+import io.mifos.core.test.domain.ValidationTestCase;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ClockOffsetTest extends ValidationTest<ClockOffset> {
+ public ClockOffsetTest(final ValidationTestCase<ClockOffset> testCase) {
+ super(testCase);
+ }
+
+ @Override
+ protected ClockOffset createValidTestSubject() {
+ return new ClockOffset();
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ValidationTestCase> ret = new ArrayList<>();
+ ret.add(new ValidationTestCase<ClockOffset>("basicCase")
+ .adjustment(x -> {})
+ .valid(true));
+ ret.add(new ValidationTestCase<ClockOffset>("everythingMidRange")
+ .adjustment(x -> {x.setHours(12); x.setMinutes(30); x.setSeconds(30);})
+ .valid(true));
+ ret.add(new ValidationTestCase<ClockOffset>("negativeHours")
+ .adjustment(x -> x.setHours(-1))
+ .valid(false));
+ ret.add(new ValidationTestCase<ClockOffset>("outOfDayHours")
+ .adjustment(x -> x.setHours(24))
+ .valid(false));
+ ret.add(new ValidationTestCase<ClockOffset>("negativeMinutes")
+ .adjustment(x -> x.setMinutes(-1))
+ .valid(false));
+ ret.add(new ValidationTestCase<ClockOffset>("outOfRangeMinutes")
+ .adjustment(x -> x.setMinutes(60))
+ .valid(false));
+ ret.add(new ValidationTestCase<ClockOffset>("negativeSeconds")
+ .adjustment(x -> x.setMinutes(-1))
+ .valid(false));
+ ret.add(new ValidationTestCase<ClockOffset>("outOfRangeSeconds")
+ .adjustment(x -> x.setMinutes(60))
+ .valid(false));
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java b/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
index 0af10b2..ff11d54 100644
--- a/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
+++ b/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
@@ -25,6 +25,7 @@
import io.mifos.core.test.listener.EventRecorder;
import io.mifos.rhythm.api.v1.client.RhythmManager;
import io.mifos.rhythm.api.v1.domain.Beat;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import io.mifos.rhythm.api.v1.events.BeatEvent;
import io.mifos.rhythm.api.v1.events.EventConstants;
import io.mifos.rhythm.service.config.RhythmConfiguration;
@@ -64,10 +65,11 @@
/**
* @author Myrle Krantz
*/
+@SuppressWarnings("SpringAutowiredFieldsWarningInspection")
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
classes = {AbstractRhythmTest.TestConfiguration.class},
- properties = {"rhythm.user=homer", "rhythm.beatCheckRate=500"}
+ properties = {"rhythm.user=homer", "rhythm.beatCheckRate=1000"}
)
public class AbstractRhythmTest {
@@ -117,7 +119,7 @@
EventRecorder eventRecorder;
@MockBean
- BeatPublisherService beatPublisherServiceSpy;
+ BeatPublisherService beatPublisherServiceMock;
@Autowired
@Qualifier(LOGGER_NAME)
@@ -148,9 +150,17 @@
final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
int alignmentHour = now.getHour();
final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, alignmentHour);
+
+ Mockito.doAnswer(new Returns(true)).when(beatPublisherServiceMock).publishBeat(
+ Matchers.eq(beatIdentifier),
+ Matchers.eq(tenantDataStoreContext.getTenantName()),
+ Matchers.eq(applicationIdentifier),
+ Matchers.eq(expectedBeatTimestamp));
+
final Beat ret = createBeat(applicationIdentifier, beatIdentifier, alignmentHour, expectedBeatTimestamp);
- Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(2_000).times(1)).publishBeat(beatIdentifier, tenantDataStoreContext.getTenantName(), applicationIdentifier, expectedBeatTimestamp);
+ Mockito.verify(beatPublisherServiceMock, Mockito.timeout(2_000).times(1))
+ .publishBeat(beatIdentifier, tenantDataStoreContext.getTenantName(), applicationIdentifier, expectedBeatTimestamp);
return ret;
}
@@ -182,25 +192,34 @@
beat.setIdentifier(beatIdentifier);
beat.setAlignmentHour(alignmentHour);
- Mockito.doAnswer(new AnswerWithDelay<>(2_000, new Returns(Optional.of(PermittableGroupIds.forApplication(applicationIdentifier))))).when(beatPublisherServiceSpy).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier));
- Mockito.doAnswer(new AnswerWithDelay<>(2_000, new Returns(true))).when(beatPublisherServiceSpy).publishBeat(Matchers.eq(beatIdentifier), Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier),
+ Mockito.doAnswer(new AnswerWithDelay<>(2_000, new Returns(Optional.of(PermittableGroupIds.forApplication(applicationIdentifier))))).when(beatPublisherServiceMock).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier));
+ Mockito.doAnswer(new AnswerWithDelay<>(2_000, new Returns(true))).when(beatPublisherServiceMock).publishBeat(Matchers.eq(beatIdentifier), Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier),
AdditionalMatchers.or(Matchers.eq(expectedBeatTimestamp), Matchers.eq(getNextTimeStamp(expectedBeatTimestamp))));
this.testSubject.createBeat(applicationIdentifier, beat);
- Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(applicationIdentifier, beat.getIdentifier())));
+ Assert.assertTrue(beat.getIdentifier(), this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(applicationIdentifier, beat.getIdentifier())));
- Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(2_500).times(1)).requestPermissionForBeats(tenantIdentifier, applicationIdentifier);
+ Mockito.verify(beatPublisherServiceMock, Mockito.timeout(2_500).times(1)).requestPermissionForBeats(tenantIdentifier, applicationIdentifier);
return beat;
}
LocalDateTime getExpectedBeatTimestamp(final LocalDateTime fromTime, final Integer alignmentHour) {
+ return getExpectedBeatTimestamp(fromTime, alignmentHour, new ClockOffset());
+ }
+
+ LocalDateTime getExpectedBeatTimestamp(
+ final LocalDateTime fromTime,
+ final Integer alignmentHour,
+ final ClockOffset clockOffset) {
final LocalDateTime midnight = fromTime.truncatedTo(ChronoUnit.DAYS);
- return midnight.plusHours(alignmentHour);
+ return midnight.plusHours(alignmentHour + clockOffset.getHours())
+ .plusMinutes(clockOffset.getMinutes())
+ .plusSeconds(clockOffset.getSeconds());
}
private LocalDateTime getNextTimeStamp(final LocalDateTime fromTime) {
return fromTime.plusDays(1);
}
-}
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/rhythm/TestBeats.java b/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
index 1f57da7..76c28b7 100644
--- a/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
+++ b/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
@@ -17,24 +17,35 @@
import io.mifos.core.api.util.NotFoundException;
import io.mifos.rhythm.api.v1.domain.Beat;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import io.mifos.rhythm.api.v1.events.BeatEvent;
import io.mifos.rhythm.api.v1.events.EventConstants;
+import io.mifos.rhythm.service.internal.repository.BeatEntity;
+import io.mifos.rhythm.service.internal.repository.BeatRepository;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
+import org.mockito.internal.stubbing.answers.Returns;
+import org.springframework.beans.factory.annotation.Autowired;
+import javax.transaction.Transactional;
+import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.concurrent.TimeUnit;
/**
* @author Myrle Krantz
*/
public class TestBeats extends AbstractRhythmTest {
+ @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
+ @Autowired
+ BeatRepository beatRepository;
@Test
public void shouldCreateBeat() throws InterruptedException {
@@ -93,14 +104,14 @@
final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, beat.getAlignmentHour());
- Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceSpy).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier));
- Mockito.when(beatPublisherServiceSpy.publishBeat(beatId, tenantIdentifier, applicationIdentifier, expectedBeatTimestamp)).thenReturn(false, false, true);
+ Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceMock).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier));
+ Mockito.when(beatPublisherServiceMock.publishBeat(beatId, tenantIdentifier, applicationIdentifier, expectedBeatTimestamp)).thenReturn(false, false, true);
this.testSubject.createBeat(applicationIdentifier, beat);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(applicationIdentifier, beat.getIdentifier())));
- Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(10_000).times(3)).publishBeat(beatId, tenantIdentifier, applicationIdentifier, expectedBeatTimestamp);
+ Mockito.verify(beatPublisherServiceMock, Mockito.timeout(10_000).times(3)).publishBeat(beatId, tenantIdentifier, applicationIdentifier, expectedBeatTimestamp);
}
@Test
@@ -122,4 +133,94 @@
beats.forEach(x -> Assert.assertTrue(allEntities.contains(x)));
}
+
+ @Test
+ public void shouldBeatForMissingDays() throws InterruptedException {
+ final String applicationIdentifier = "funnybusiness-v6";
+ final String beatIdentifier = "fiddlebeat";
+ createBeatForThisHour(applicationIdentifier, beatIdentifier);
+
+ final int daysAgo = 10;
+ final LocalDateTime nextBeat = setBack(applicationIdentifier, beatIdentifier, daysAgo);
+
+ for (int i = daysAgo; i > 0; i--) {
+ Mockito.verify(beatPublisherServiceMock, Mockito.timeout(4_000).times(1))
+ .publishBeat(
+ beatIdentifier,
+ tenantDataStoreContext.getTenantName(),
+ applicationIdentifier,
+ nextBeat.minusDays(i));
+ }
+ }
+
+ @Test
+ public void clockOffsetShouldEffectBeatTiming() throws InterruptedException {
+ final String tenantIdentifier = tenantDataStoreContext.getTenantName();
+ final String applicationIdentifier = "funnybusiness-v7";
+ final String beatIdentifier = "fiddlebeat0";
+
+ final ClockOffset initialClockOffset = this.testSubject.getClockOffset();
+ Assert.assertEquals(Integer.valueOf(0), initialClockOffset.getHours());
+ Assert.assertEquals(Integer.valueOf(0), initialClockOffset.getMinutes());
+ Assert.assertEquals(Integer.valueOf(0), initialClockOffset.getSeconds());
+
+ final LocalDateTime now = LocalDateTime.now(Clock.systemUTC());
+ final ClockOffset offsetToNow = new ClockOffset(now.getHour(), now.getMinute(), now.getSecond());
+ this.testSubject.setClockOffset(offsetToNow);
+
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CLOCKOFFSET, offsetToNow));
+ TimeUnit.SECONDS.sleep(1);
+ final ClockOffset changedClockOffset = this.testSubject.getClockOffset();
+ Assert.assertEquals(offsetToNow, changedClockOffset);
+
+ final Beat beat = new Beat();
+ beat.setIdentifier(beatIdentifier);
+ beat.setAlignmentHour(0);
+
+ final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, 0, offsetToNow);
+
+ Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceMock)
+ .requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationIdentifier));
+ Mockito.when(beatPublisherServiceMock
+ .publishBeat(beatIdentifier, tenantIdentifier, applicationIdentifier, expectedBeatTimestamp))
+ .thenReturn(true);
+
+ this.testSubject.createBeat(applicationIdentifier, beat);
+
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(applicationIdentifier, beat.getIdentifier())));
+
+ Mockito.verify(beatPublisherServiceMock, Mockito.timeout(10_000).times(1)).publishBeat(beatIdentifier, tenantIdentifier, applicationIdentifier, expectedBeatTimestamp);
+
+ //Set back to zero'ed clock offset so you don't break the rest of the tests.
+ this.testSubject.setClockOffset(initialClockOffset);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CLOCKOFFSET, initialClockOffset));
+ TimeUnit.SECONDS.sleep(1);
+ }
+
+ @Transactional
+ LocalDateTime setBack(
+ final String applicationIdentifier,
+ final String beatIdentifier,
+ final int daysAgo) {
+
+ final BeatEntity beatEntity = beatRepository.findByTenantIdentifierAndApplicationIdentifierAndBeatIdentifier(
+ tenantDataStoreContext.getTenantName(),
+ applicationIdentifier,
+ beatIdentifier).orElseThrow(IllegalStateException::new);
+
+ Mockito.reset(beatPublisherServiceMock);
+ Mockito.doAnswer(new Returns(true)).when(beatPublisherServiceMock)
+ .publishBeat(
+ Matchers.eq(beatIdentifier),
+ Matchers.eq(tenantDataStoreContext.getTenantName()),
+ Matchers.eq(applicationIdentifier),
+ Matchers.any(LocalDateTime.class));
+ final LocalDateTime nextBeat = beatEntity.getNextBeat();
+
+ beatEntity.setNextBeat(nextBeat.minusDays(daysAgo));
+
+ beatRepository.save(beatEntity);
+
+ return nextBeat;
+ }
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/rhythm/listener/ClockOffsetEventListener.java b/component-test/src/main/java/io/mifos/rhythm/listener/ClockOffsetEventListener.java
new file mode 100644
index 0000000..890df79
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/rhythm/listener/ClockOffsetEventListener.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
+import io.mifos.rhythm.api.v1.events.EventConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class ClockOffsetEventListener {
+ private final EventRecorder eventRecorder;
+
+ @Autowired
+ public ClockOffsetEventListener(@SuppressWarnings("SpringJavaAutowiringInspection") final EventRecorder eventRecorder) {
+ super();
+ this.eventRecorder = eventRecorder;
+ }
+
+ @JmsListener(
+ subscription = EventConstants.DESTINATION,
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_PUT_CLOCKOFFSET
+ )
+ public void onChangeClockOffset(
+ @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, EventConstants.PUT_CLOCKOFFSET, payload, ClockOffset.class);
+ }
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/command/ChangeClockOffsetCommand.java b/service/src/main/java/io/mifos/rhythm/service/internal/command/ChangeClockOffsetCommand.java
new file mode 100644
index 0000000..36104bd
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/command/ChangeClockOffsetCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.internal.command;
+
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ChangeClockOffsetCommand {
+ private final String tenantIdentifier;
+
+ private final ClockOffset instance;
+
+ public ChangeClockOffsetCommand(
+ final String tenantIdentifier,
+ final ClockOffset instance) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.instance = instance;
+ }
+
+ public String getTenantIdentifier() {
+ return tenantIdentifier;
+ }
+
+ public ClockOffset getInstance() {
+ return instance;
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeClockOffsetCommand{" +
+ "tenantIdentifier='" + tenantIdentifier + '\'' +
+ ", instance=" + instance +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java
index e0bf47b..922d831 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/BeatCommandHandler.java
@@ -18,7 +18,7 @@
import io.mifos.core.command.annotation.Aggregate;
import io.mifos.core.command.annotation.CommandHandler;
import io.mifos.core.command.annotation.CommandLogLevel;
-import io.mifos.core.lang.ServiceException;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import io.mifos.rhythm.api.v1.events.BeatEvent;
import io.mifos.rhythm.api.v1.events.EventConstants;
import io.mifos.rhythm.service.ServiceConstants;
@@ -27,14 +27,13 @@
import io.mifos.rhythm.service.internal.mapper.BeatMapper;
import io.mifos.rhythm.service.internal.repository.BeatEntity;
import io.mifos.rhythm.service.internal.repository.BeatRepository;
+import io.mifos.rhythm.service.internal.service.ClockOffsetService;
import io.mifos.rhythm.service.internal.service.IdentityPermittableGroupService;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;
-import java.util.Optional;
-
/**
* @author Myrle Krantz
*/
@@ -43,18 +42,21 @@
public class BeatCommandHandler {
private final IdentityPermittableGroupService identityPermittableGroupService;
private final BeatRepository beatRepository;
+ private final ClockOffsetService clockOffsetService;
private final EventHelper eventHelper;
private final Logger logger;
@Autowired
public BeatCommandHandler(
- final IdentityPermittableGroupService identityPermittableGroupService,
- final BeatRepository beatRepository,
- final EventHelper eventHelper,
- @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
+ final IdentityPermittableGroupService identityPermittableGroupService,
+ final BeatRepository beatRepository,
+ final ClockOffsetService clockOffsetService,
+ final EventHelper eventHelper,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
super();
this.identityPermittableGroupService = identityPermittableGroupService;
this.beatRepository = beatRepository;
+ this.clockOffsetService = clockOffsetService;
this.eventHelper = eventHelper;
this.logger = logger;
}
@@ -73,34 +75,30 @@
//stuff that should happen in the transaction.
@SuppressWarnings("WeakerAccess")
@Transactional
- public void processCreateBeatCommand(CreateBeatCommand createBeatCommand) {
+ public void processCreateBeatCommand(final CreateBeatCommand createBeatCommand) {
final boolean applicationHasRequestForAccessPermission = identityPermittableGroupService.checkThatApplicationHasRequestForAccessPermission(
- createBeatCommand.getTenantIdentifier(), createBeatCommand.getApplicationIdentifier());
+ createBeatCommand.getTenantIdentifier(), createBeatCommand.getApplicationIdentifier());
if (!applicationHasRequestForAccessPermission) {
logger.info("Rhythm needs permission to publish beats to application, but couldn't request that permission for tenant '{}' and application '{}'.",
- createBeatCommand.getTenantIdentifier(), createBeatCommand.getApplicationIdentifier());
+ createBeatCommand.getTenantIdentifier(), createBeatCommand.getApplicationIdentifier());
}
+ final ClockOffset clockOffset = clockOffsetService.findByTenantIdentifier(createBeatCommand.getTenantIdentifier());
final BeatEntity entity = BeatMapper.map(
- createBeatCommand.getTenantIdentifier(),
- createBeatCommand.getApplicationIdentifier(),
- createBeatCommand.getInstance());
+ createBeatCommand.getTenantIdentifier(),
+ createBeatCommand.getApplicationIdentifier(),
+ createBeatCommand.getInstance(),
+ clockOffset);
this.beatRepository.save(entity);
}
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.NONE)
@Transactional
public void process(final DeleteBeatCommand deleteBeatCommand) {
- final Optional<BeatEntity> toDelete = this.beatRepository.findByTenantIdentifierAndApplicationIdentifierAndBeatIdentifier(
+ this.beatRepository.deleteByTenantIdentifierAndApplicationIdentifierAndBeatIdentifier(
deleteBeatCommand.getTenantIdentifier(),
deleteBeatCommand.getApplicationIdentifier(),
deleteBeatCommand.getIdentifier());
- final BeatEntity toDeleteForReal
- = toDelete.orElseThrow(() -> ServiceException.notFound(
- "Beat for the application ''" + deleteBeatCommand.getApplicationIdentifier() +
- "'' with the identifier ''" + deleteBeatCommand.getIdentifier() + "'' not found."));
-
- this.beatRepository.delete(toDeleteForReal);
eventHelper.sendEvent(EventConstants.DELETE_BEAT, deleteBeatCommand.getTenantIdentifier(),
new BeatEvent(deleteBeatCommand.getApplicationIdentifier(), deleteBeatCommand.getIdentifier()));
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ClockOffsetCommandHandler.java b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ClockOffsetCommandHandler.java
new file mode 100644
index 0000000..08c39cf
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ClockOffsetCommandHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.internal.command.handler;
+
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.CommandLogLevel;
+import io.mifos.rhythm.api.v1.events.EventConstants;
+import io.mifos.rhythm.service.ServiceConstants;
+import io.mifos.rhythm.service.internal.command.ChangeClockOffsetCommand;
+import io.mifos.rhythm.service.internal.mapper.ClockOffsetMapper;
+import io.mifos.rhythm.service.internal.repository.ClockOffsetEntity;
+import io.mifos.rhythm.service.internal.repository.ClockOffsetRepository;
+import io.mifos.rhythm.service.internal.service.Drummer;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+
+import javax.transaction.Transactional;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Aggregate
+public class ClockOffsetCommandHandler {
+ private final ClockOffsetRepository clockOffsetRepository;
+ private final Drummer drummer;
+ private final EventHelper eventHelper;
+ private final Logger logger;
+
+ @Autowired
+ public ClockOffsetCommandHandler(
+ final ClockOffsetRepository clockOffsetRepository,
+ final Drummer drummer,
+ final EventHelper eventHelper,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
+ super();
+ this.clockOffsetRepository = clockOffsetRepository;
+ this.drummer = drummer;
+ this.eventHelper = eventHelper;
+ this.logger = logger;
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.NONE)
+ public void process(final ChangeClockOffsetCommand changeClockOffsetCommand) {
+
+ final Optional<ClockOffsetEntity> oldClockOffsetEntity =
+ clockOffsetRepository.findByTenantIdentifier(changeClockOffsetCommand.getTenantIdentifier());
+
+ final ClockOffsetEntity newOffsetEntity = ClockOffsetMapper.map(
+ changeClockOffsetCommand.getTenantIdentifier(),
+ changeClockOffsetCommand.getInstance(),
+ oldClockOffsetEntity);
+
+ clockOffsetRepository.save(newOffsetEntity);
+
+ drummer.realignAllBeatsForTenant(
+ changeClockOffsetCommand.getTenantIdentifier(),
+ oldClockOffsetEntity.orElseGet(ClockOffsetEntity::new),
+ newOffsetEntity);
+
+ logger.info("Sending change clock offset event.");
+ eventHelper.sendEvent(
+ EventConstants.PUT_CLOCKOFFSET,
+ changeClockOffsetCommand.getTenantIdentifier(),
+ changeClockOffsetCommand.getInstance());
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/mapper/BeatMapper.java b/service/src/main/java/io/mifos/rhythm/service/internal/mapper/BeatMapper.java
index 2f37f95..8e31e58 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/mapper/BeatMapper.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/mapper/BeatMapper.java
@@ -16,10 +16,12 @@
package io.mifos.rhythm.service.internal.mapper;
import io.mifos.rhythm.api.v1.domain.Beat;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import io.mifos.rhythm.service.internal.repository.BeatEntity;
+import io.mifos.rhythm.service.internal.repository.ClockOffsetEntity;
+import java.time.Clock;
import java.time.LocalDateTime;
-import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
@@ -42,14 +44,41 @@
return ret;
}
- static BeatEntity map(final String tenantIdentifier, final String applicationIdentifier, final Beat instance) {
+ static BeatEntity map(
+ final String tenantIdentifier,
+ final String applicationIdentifier,
+ final Beat instance,
+ final ClockOffset clockOffset) {
final BeatEntity ret = new BeatEntity();
ret.setBeatIdentifier(instance.getIdentifier());
ret.setTenantIdentifier(tenantIdentifier);
ret.setApplicationIdentifier(applicationIdentifier);
ret.setAlignmentHour(instance.getAlignmentHour());
- //First beat is today. If it's in the past, it will be created nearly immediately.
- ret.setNextBeat(LocalDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.DAYS).plusHours(instance.getAlignmentHour()));
+ //First beat is today. If it's in the past, it will be published nearly immediately.
+ ret.setNextBeat(alignDateTime(
+ LocalDateTime.now(Clock.systemUTC()),
+ instance.getAlignmentHour(),
+ clockOffset));
return ret;
}
+
+ static LocalDateTime alignDateTime(
+ final LocalDateTime localDateTime,
+ final int alignmentHour,
+ final ClockOffset clockOffset) {
+ return localDateTime.truncatedTo(ChronoUnit.DAYS)
+ .plusHours(alignmentHour + clockOffset.getHours())
+ .plusMinutes(clockOffset.getMinutes())
+ .plusSeconds(clockOffset.getSeconds());
+ }
+
+ static LocalDateTime alignDateTime(
+ final LocalDateTime localDateTime,
+ final int alignmentHour,
+ final ClockOffsetEntity clockOffset) {
+ return localDateTime.truncatedTo(ChronoUnit.DAYS)
+ .plusHours(alignmentHour + clockOffset.getHours())
+ .plusMinutes(clockOffset.getMinutes())
+ .plusSeconds(clockOffset.getSeconds());
+ }
}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/mapper/ClockOffsetMapper.java b/service/src/main/java/io/mifos/rhythm/service/internal/mapper/ClockOffsetMapper.java
new file mode 100644
index 0000000..2188fda
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/mapper/ClockOffsetMapper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.internal.mapper;
+
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
+import io.mifos.rhythm.service.internal.repository.ClockOffsetEntity;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+public interface ClockOffsetMapper {
+ static ClockOffset map(final ClockOffsetEntity entity) {
+ final ClockOffset ret = new ClockOffset();
+ ret.setHours(entity.getHours());
+ ret.setMinutes(entity.getMinutes());
+ ret.setSeconds(entity.getSeconds());
+ return ret;
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ static ClockOffsetEntity map(
+ final String tenantIdentifier,
+ final ClockOffset instance,
+ final Optional<ClockOffsetEntity> existingClockOffset) {
+ final ClockOffsetEntity ret = new ClockOffsetEntity();
+ existingClockOffset.ifPresent(x -> ret.setId(x.getId()));
+ ret.setTenantIdentifier(tenantIdentifier);
+ ret.setHours(instance.getHours());
+ ret.setMinutes(instance.getMinutes());
+ ret.setSeconds(instance.getSeconds());
+ return ret;
+ }
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java
index 022b64a..b195b93 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java
@@ -16,8 +16,10 @@
package io.mifos.rhythm.service.internal.repository;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Repository;
+import javax.persistence.LockModeType;
import java.util.Optional;
/**
@@ -25,6 +27,7 @@
*/
@Repository
public interface ApplicationRepository extends JpaRepository<ApplicationEntity, Long> {
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
void deleteByTenantIdentifierAndApplicationIdentifier(String tenantIdentifier, String applicationIdentifier);
Optional<ApplicationEntity> findByTenantIdentifierAndApplicationIdentifier(String tenantIdentifier, String applicationIdentifier);
}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/BeatRepository.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/BeatRepository.java
index 05f0a4e..88b51dd 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/repository/BeatRepository.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/BeatRepository.java
@@ -16,8 +16,10 @@
package io.mifos.rhythm.service.internal.repository;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Repository;
+import javax.persistence.LockModeType;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@@ -28,11 +30,20 @@
*/
@Repository
public interface BeatRepository extends JpaRepository<BeatEntity, Long> {
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
void deleteByTenantIdentifierAndApplicationIdentifier
- (String tenantIdentifier, String applicationIdentifier);
+ (String tenantIdentifier, String applicationIdentifier);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ void deleteByTenantIdentifierAndApplicationIdentifierAndBeatIdentifier
+ (String tenantIdentifier,
+ String applicationIdentifier,
+ String beatIdentifier);
List<BeatEntity> findByTenantIdentifierAndApplicationIdentifier
(String tenantIdentifier, String applicationIdentifier);
Optional<BeatEntity> findByTenantIdentifierAndApplicationIdentifierAndBeatIdentifier
(String tenantIdentifier, String applicationIdentifier, String beatIdentifier);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
Stream<BeatEntity> findByNextBeatBefore(LocalDateTime currentTime);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ Stream<BeatEntity> findByTenantIdentifier(String tenantIdentifier);
}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ClockOffsetEntity.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ClockOffsetEntity.java
new file mode 100644
index 0000000..b7ebe36
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ClockOffsetEntity.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.internal.repository;
+
+import javax.persistence.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+@Entity
+@Table(name = "khepri_clockoffsets")
+public class ClockOffsetEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "tenant_identifier", nullable = false)
+ private String tenantIdentifier;
+
+ @Column(name = "hours", nullable = false)
+ private Integer hours = 0;
+
+ @Column(name = "minutes", nullable = false)
+ private Integer minutes = 0;
+
+ @Column(name = "seconds", nullable = false)
+ private Integer seconds = 0;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTenantIdentifier() {
+ return tenantIdentifier;
+ }
+
+ public void setTenantIdentifier(String tenantIdentifier) {
+ this.tenantIdentifier = tenantIdentifier;
+ }
+
+ public Integer getHours() {
+ return hours;
+ }
+
+ public void setHours(Integer hours) {
+ this.hours = hours;
+ }
+
+ public Integer getMinutes() {
+ return minutes;
+ }
+
+ public void setMinutes(Integer minutes) {
+ this.minutes = minutes;
+ }
+
+ public Integer getSeconds() {
+ return seconds;
+ }
+
+ public void setSeconds(Integer seconds) {
+ this.seconds = seconds;
+ }
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ClockOffsetRepository.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ClockOffsetRepository.java
new file mode 100644
index 0000000..3005457
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ClockOffsetRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface ClockOffsetRepository extends JpaRepository<ClockOffsetEntity, Long> {
+ Optional<ClockOffsetEntity> findByTenantIdentifier(String tenantIdentifier);
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java
index a070b20..14ef663 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/BeatPublisherService.java
@@ -18,6 +18,7 @@
import io.mifos.anubis.api.v1.domain.AllowedOperation;
import io.mifos.core.api.context.AutoUserContext;
import io.mifos.core.api.util.ApiFactory;
+import io.mifos.core.api.util.InvalidTokenException;
import io.mifos.core.lang.ApplicationName;
import io.mifos.core.lang.AutoTenantContext;
import io.mifos.core.lang.DateConverter;
@@ -97,12 +98,15 @@
publishBeatPermission.setPermittableEndpointGroupIdentifier(consumerPermittableGroupIdentifier);
try {
applicationPermissionRequestCreator.createApplicationPermission(rhythmApplicationName.toString(), publishBeatPermission);
+ logger.debug("Successfully requested permission to send beats to application '{}' under tenant '{}'.", applicationIdentifier, tenantIdentifier);
+ }
+ catch (final InvalidTokenException e) {
+ logger.error("Failed to request permission for application {}, in tenant {} because rhythm does not have permission to access identity.", applicationIdentifier, tenantIdentifier, e);
}
catch (final ApplicationPermissionAlreadyExistsException e) {
logger.debug("Failed to request permission for application {}, in tenant {} because the request already exists. {} was thrown.", applicationIdentifier, tenantIdentifier, e);
}
- logger.debug("Successfully requested permission to send beats to application '{}' under tenant '{}'.", applicationIdentifier, tenantIdentifier);
return Optional.of(consumerPermittableGroupIdentifier);
}
}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/ClockOffsetService.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/ClockOffsetService.java
new file mode 100644
index 0000000..1700f5a
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/ClockOffsetService.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.internal.service;
+
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
+import io.mifos.rhythm.service.internal.mapper.ClockOffsetMapper;
+import io.mifos.rhythm.service.internal.repository.ClockOffsetRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ClockOffsetService {
+ final private ClockOffsetRepository clockOffsetRepository;
+
+ @Autowired
+ public ClockOffsetService(final ClockOffsetRepository clockOffsetRepository) {
+ this.clockOffsetRepository = clockOffsetRepository;
+ }
+
+ public ClockOffset findByTenantIdentifier(final String tenantIdentifier) {
+ return clockOffsetRepository.findByTenantIdentifier(tenantIdentifier)
+ .map(ClockOffsetMapper::map)
+ .orElseGet(ClockOffset::new); //If none is set, use 0,0,0
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java
index 88a21ca..c5802df 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java
@@ -15,9 +15,12 @@
*/
package io.mifos.rhythm.service.internal.service;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import io.mifos.rhythm.service.ServiceConstants;
+import io.mifos.rhythm.service.internal.mapper.BeatMapper;
import io.mifos.rhythm.service.internal.repository.BeatEntity;
import io.mifos.rhythm.service.internal.repository.BeatRepository;
+import io.mifos.rhythm.service.internal.repository.ClockOffsetEntity;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -26,44 +29,44 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
-import javax.annotation.Nonnull;
+import java.time.Clock;
import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.temporal.ChronoUnit;
-import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* @author Myrle Krantz
*/
-@SuppressWarnings({"unused", "WeakerAccess"})
@Component
public class Drummer {
private final IdentityPermittableGroupService identityPermittableGroupService;
private final BeatPublisherService beatPublisherService;
private final BeatRepository beatRepository;
+ private final ClockOffsetService clockOffsetService;
private final Logger logger;
@Autowired
public Drummer(
- final IdentityPermittableGroupService identityPermittableGroupService,
- final BeatPublisherService beatPublisherService,
- final BeatRepository beatRepository,
- @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
+ final IdentityPermittableGroupService identityPermittableGroupService,
+ final BeatPublisherService beatPublisherService,
+ final BeatRepository beatRepository,
+ final ClockOffsetService clockOffsetService,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
this.identityPermittableGroupService = identityPermittableGroupService;
this.beatPublisherService = beatPublisherService;
this.beatRepository = beatRepository;
+ this.clockOffsetService = clockOffsetService;
this.logger = logger;
}
@Scheduled(initialDelayString = "${rhythm.beatCheckRate}", fixedRateString = "${rhythm.beatCheckRate}")
@Transactional
public synchronized void checkForBeatsNeeded() {
+ logger.info("checkForBeatsNeeded begin.");
//In it's current form this function cannot be run in multiple instances of the same service. We need to get
//locking on selected entries corrected here, before this will work.
try {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
+ final LocalDateTime now = LocalDateTime.now(Clock.systemUTC());
//Get beats from the last two hours in case restart/start happens close to hour begin.
final Stream<BeatEntity> beats = beatRepository.findByNextBeatBefore(now);
beats.forEach((beat) -> {
@@ -75,17 +78,18 @@
}
else {
logger.info("Checking if beat {} needs publishing.", beat);
- final Optional<LocalDateTime> nextBeat = checkBeatForPublish(
+ final LocalDateTime nextBeat = checkBeatForPublish(
now,
beat.getBeatIdentifier(),
beat.getTenantIdentifier(),
beat.getApplicationIdentifier(),
beat.getAlignmentHour(),
beat.getNextBeat());
- nextBeat.ifPresent(y -> {
- beat.setNextBeat(y);
+ if (!nextBeat.equals(beat.getNextBeat())) {
+ beat.setNextBeat(nextBeat);
beatRepository.save(beat);
- });
+ }
+ logger.info("Beat updated to {}.", beat);
}
});
@@ -94,50 +98,67 @@
logger.info("InvalidDataAccessResourceUsageException in check for scheduled beats, probably " +
"because initialize hasn't been called yet. {}", e);
}
+ logger.info("checkForBeatsNeeded end.");
}
- public Optional<LocalDateTime> checkBeatForPublish(
- final LocalDateTime now,
- final String beatIdentifier,
- final String tenantIdentifier,
- final String applicationIdentifier,
- final Integer alignmentHour,
- final LocalDateTime nextBeat) {
- return checkBeatForPublishHelper(now, alignmentHour, nextBeat,
+ @Transactional
+ public synchronized void realignAllBeatsForTenant(
+ final String tenantIdentifier,
+ final ClockOffsetEntity oldClockOffset,
+ final ClockOffsetEntity newClockOffset)
+ {
+ final Stream<BeatEntity> beatsToAdjust = beatRepository.findByTenantIdentifier(tenantIdentifier);
+ beatsToAdjust.forEach(x -> {
+ //Need to subtract old clock offset, because for large clock offsets and large alignments,
+ //time can "skip" into the next day through realignment.
+ final LocalDateTime oldBeatNextBeat = x.getNextBeat()
+ .minusHours(oldClockOffset.getHours())
+ .minusMinutes(oldClockOffset.getMinutes())
+ .minusSeconds(oldClockOffset.getSeconds());
+ x.setNextBeat(BeatMapper.alignDateTime(
+ oldBeatNextBeat,
+ x.getAlignmentHour(),
+ newClockOffset));
+ beatRepository.save(x);
+ });
+ }
+
+ private LocalDateTime checkBeatForPublish(
+ final LocalDateTime now,
+ final String beatIdentifier,
+ final String tenantIdentifier,
+ final String applicationIdentifier,
+ final Integer alignmentHour,
+ final LocalDateTime nextBeat) {
+ final ClockOffset clockOffset = clockOffsetService.findByTenantIdentifier(tenantIdentifier);
+ return checkBeatForPublishHelper(now, alignmentHour, nextBeat, clockOffset,
x -> beatPublisherService.publishBeat(beatIdentifier, tenantIdentifier, applicationIdentifier, x));
}
//Helper is separated from original function so that it can be unit-tested separately from publishBeat.
- static Optional<LocalDateTime> checkBeatForPublishHelper(
+ static LocalDateTime checkBeatForPublishHelper(
final LocalDateTime now,
final Integer alignmentHour,
final LocalDateTime nextBeat,
+ final ClockOffset clockOffset,
final Predicate<LocalDateTime> publishSucceeded) {
- final long numberOfBeatPublishesNeeded = getNumberOfBeatPublishesNeeded(now, nextBeat);
- if (numberOfBeatPublishesNeeded == 0)
- return Optional.empty();
+ LocalDateTime beatToPublish = nextBeat;
+ for (;
+ !beatToPublish.isAfter(now);
+ beatToPublish = incrementToAlignment(beatToPublish, alignmentHour, clockOffset))
+ {
+ if (!publishSucceeded.test(beatToPublish))
+ break;
+ }
- final Optional<LocalDateTime> firstFailedBeat = Stream.iterate(nextBeat,
- x -> incrementToAlignment(x, alignmentHour))
- .limit(numberOfBeatPublishesNeeded)
- .filter(x -> !publishSucceeded.test(x))
- .findFirst();
-
- if (firstFailedBeat.isPresent())
- return firstFailedBeat;
- else
- return Optional.of(incrementToAlignment(now, alignmentHour));
+ return beatToPublish;
}
- static long getNumberOfBeatPublishesNeeded(final LocalDateTime now, final @Nonnull LocalDateTime nextBeat) {
- if (nextBeat.isAfter(now))
- return 0;
-
- return Math.max(1, nextBeat.until(now, ChronoUnit.DAYS));
- }
-
- static LocalDateTime incrementToAlignment(final LocalDateTime toIncrement, final Integer alignmentHour)
+ static LocalDateTime incrementToAlignment(
+ final LocalDateTime toIncrement,
+ final Integer alignmentHour,
+ final ClockOffset clockOffset)
{
- return toIncrement.plusDays(1).truncatedTo(ChronoUnit.DAYS).plusHours(alignmentHour);
+ return BeatMapper.alignDateTime(toIncrement.plusDays(1), alignmentHour, clockOffset);
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java b/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java
index 441def7..5c33f4c 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java
@@ -15,9 +15,12 @@
*/
package io.mifos.rhythm.service.internal.service;
+import io.mifos.rhythm.service.ServiceConstants;
import io.mifos.rhythm.service.internal.repository.ApplicationEntity;
import io.mifos.rhythm.service.internal.repository.ApplicationRepository;
+import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
@@ -32,19 +35,23 @@
public class IdentityPermittableGroupService {
private final ApplicationRepository applicationRepository;
private final BeatPublisherService beatPublisherService;
+ private final Logger logger;
@Autowired
public IdentityPermittableGroupService(
- final ApplicationRepository applicationRepository,
- final BeatPublisherService beatPublisherService) {
+ final ApplicationRepository applicationRepository,
+ final BeatPublisherService beatPublisherService,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
this.applicationRepository = applicationRepository;
this.beatPublisherService = beatPublisherService;
+ this.logger = logger;
}
public synchronized boolean checkThatApplicationHasRequestForAccessPermission(
final String tenantIdentifier,
final String applicationIdentifier) {
try {
+ logger.info("checkThatApplicationHasRequestForAccessPermission begin");
return checkThatApplicationHasRequestForAccessPermissionHelper(tenantIdentifier, applicationIdentifier);
}
catch (final DataIntegrityViolationException e) {
diff --git a/service/src/main/java/io/mifos/rhythm/service/rest/BeatRestController.java b/service/src/main/java/io/mifos/rhythm/service/rest/BeatRestController.java
index a795a8b..63ab292 100644
--- a/service/src/main/java/io/mifos/rhythm/service/rest/BeatRestController.java
+++ b/service/src/main/java/io/mifos/rhythm/service/rest/BeatRestController.java
@@ -81,7 +81,7 @@
@PathVariable("beatidentifier") final String beatIdentifier) {
return this.beatService.findByIdentifier(tenantIdentifier, applicationIdentifier, beatIdentifier)
.map(ResponseEntity::ok)
- .orElseThrow(() -> ServiceException.notFound("Instance with identifier " + applicationIdentifier + " doesn't exist."));
+ .orElseThrow(() -> ServiceException.notFound("Instance with identifier ''" + applicationIdentifier + "'' doesn''t exist."));
}
@Permittable(value = AcceptedTokenType.SYSTEM, permittedEndpoint = "/applications/{applicationidentifier}/beats", acceptTokenIntendedForForeignApplication = true) //Allow apps to use this endpoint in their provisioning code.
diff --git a/service/src/main/java/io/mifos/rhythm/service/rest/ClockOffsetRestController.java b/service/src/main/java/io/mifos/rhythm/service/rest/ClockOffsetRestController.java
new file mode 100644
index 0000000..9793731
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/rest/ClockOffsetRestController.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.rhythm.service.rest;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
+import io.mifos.rhythm.service.internal.command.ChangeClockOffsetCommand;
+import io.mifos.rhythm.service.internal.service.ClockOffsetService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+import static io.mifos.core.lang.config.TenantHeaderFilter.TENANT_HEADER;
+
+/**
+ * @author Myrle Krantz
+ */
+@RestController
+@RequestMapping("/clockoffset")
+public class ClockOffsetRestController {
+ private final CommandGateway commandGateway;
+ private final ClockOffsetService clockOffsetService;
+
+ @Autowired
+ public ClockOffsetRestController(
+ final CommandGateway commandGateway,
+ final ClockOffsetService clockOffsetService) {
+ super();
+ this.commandGateway = commandGateway;
+ this.clockOffsetService = clockOffsetService;
+ }
+
+ @Permittable(value = AcceptedTokenType.SYSTEM)
+ @RequestMapping(
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public
+ @ResponseBody
+ ResponseEntity<ClockOffset> getClockOffset(@RequestHeader(TENANT_HEADER) final String tenantIdentifier) {
+ return ResponseEntity.ok(this.clockOffsetService.findByTenantIdentifier(tenantIdentifier));
+ }
+
+ @Permittable(value = AcceptedTokenType.SYSTEM)
+ @RequestMapping(
+ method = RequestMethod.PUT,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public
+ @ResponseBody
+ ResponseEntity<Void> setClockOffset(
+ @RequestHeader(TENANT_HEADER) final String tenantIdentifier,
+ @RequestBody @Valid final ClockOffset instance) throws InterruptedException {
+ this.commandGateway.process(new ChangeClockOffsetCommand(tenantIdentifier, instance));
+ return ResponseEntity.accepted().build();
+ }
+}
diff --git a/service/src/main/resources/db/migrations/mariadb/V2__tenant_clock_offset.sql b/service/src/main/resources/db/migrations/mariadb/V2__tenant_clock_offset.sql
new file mode 100644
index 0000000..c6b3662
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V2__tenant_clock_offset.sql
@@ -0,0 +1,25 @@
+--
+-- Copyright 2017 Kuelap, Inc.
+--
+-- Licensed 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.
+--
+
+CREATE TABLE khepri_clockoffsets (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_identifier VARCHAR(32) NOT NULL,
+ hours INT NOT NULL,
+ minutes INT NOT NULL,
+ seconds INT NOT NULL,
+ CONSTRAINT khepri_clockoffsets_uq UNIQUE (tenant_identifier),
+ CONSTRAINT khepri_clockoffsets_pk PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java b/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
index 3cb376d..b9e899c 100644
--- a/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
+++ b/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
@@ -15,77 +15,191 @@
*/
package io.mifos.rhythm.service.internal.service;
+import io.mifos.rhythm.api.v1.domain.ClockOffset;
import org.junit.Assert;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import org.mockito.Mockito;
+import java.time.Clock;
import java.time.LocalDateTime;
-import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
import java.util.function.Predicate;
/**
* @author Myrle Krantz
*/
+@RunWith(Parameterized.class)
public class DrummerTest {
+ static class TestCase {
+ final String description;
+ LocalDateTime now = LocalDateTime.now(Clock.systemUTC());
+ LocalDateTime nextBeat = now.minusDays(1).truncatedTo(ChronoUnit.DAYS);
+ int alignmentHour = 0;
+ ClockOffset clockOffset = new ClockOffset();
+ LocalDateTime expectedIncrementedBeat = nextBeat.plusDays(1);
+ LocalDateTime expectedNextBeatAfterPublish = now.plusDays(1).truncatedTo(ChronoUnit.DAYS);
+ int expectedBeatPublishCount = 2;
+
+ TestCase(final String description) {
+ this.description = description;
+ }
+
+ TestCase now(final LocalDateTime now) {
+ this.now = now;
+ return this;
+ }
+
+ TestCase nextBeat(final LocalDateTime newVal) {
+ this.nextBeat = newVal;
+ return this;
+ }
+
+ TestCase alignmentHour(final int newVal) {
+ this.alignmentHour = newVal;
+ return this;
+ }
+
+ TestCase clockOffset(final ClockOffset newVal) {
+ this.clockOffset = newVal;
+ return this;
+ }
+
+ TestCase expectedIncrementedBeat(final LocalDateTime newVal) {
+ this.expectedIncrementedBeat = newVal;
+ return this;
+ }
+
+ TestCase expectedNextBeatAfterPublish(final LocalDateTime newVal) {
+ this.expectedNextBeatAfterPublish = newVal;
+ return this;
+ }
+
+ TestCase expectedBeatPublishCount(final int newVal) {
+ this.expectedBeatPublishCount = newVal;
+ return this;
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<TestCase> ret = new ArrayList<>();
+ final TestCase basicCase = new TestCase("basicCase");
+ ret.add(basicCase);
+ ret.add(new TestCase("3daysBack")
+ .nextBeat(basicCase.now.minusDays(3))
+ .expectedIncrementedBeat(basicCase.now.minusDays(2).truncatedTo(ChronoUnit.DAYS))
+ .expectedBeatPublishCount(4)); //Four because "now" gets published too.
+ ret.add(new TestCase("inFuture")
+ .nextBeat(basicCase.now.plusDays(1))
+ .expectedIncrementedBeat(basicCase.now.plusDays(2).truncatedTo(ChronoUnit.DAYS))
+ .expectedNextBeatAfterPublish(basicCase.now.plusDays(1))
+ .expectedBeatPublishCount(0));
+ ret.add(new TestCase("nonZeroClockOffset")
+ .now(LocalDateTime.of(2017, 12, 18, 15, 5, 2))
+ .nextBeat(LocalDateTime.of(2017, 12, 17, 15, 0, 0))
+ .clockOffset(new ClockOffset(15, 5, 2))
+ .expectedIncrementedBeat(LocalDateTime.of(2017, 12, 18, 15, 5, 2))
+ .expectedNextBeatAfterPublish(LocalDateTime.of(2017, 12, 19, 15, 5, 2))
+ .expectedBeatPublishCount(2));
+ ret.add(new TestCase("nonZeroAlignmentHour")
+ .now(LocalDateTime.of(2017, 12, 18, 15, 5, 2))
+ .nextBeat(LocalDateTime.of(2017, 12, 17, 15, 0, 0))
+ .alignmentHour(4)
+ .expectedIncrementedBeat(LocalDateTime.of(2017, 12, 18, 4, 0, 0))
+ .expectedNextBeatAfterPublish(LocalDateTime.of(2017, 12, 19, 4, 0, 0))
+ .expectedBeatPublishCount(2));
+ ret.add(new TestCase("clockOffsetAndAlignmentHour")
+ .now(LocalDateTime.of(2017, 12, 18, 15, 5, 2))
+ .nextBeat(LocalDateTime.of(2017, 12, 17, 15, 0, 0))
+ .alignmentHour(5)
+ .clockOffset(new ClockOffset(15, 5, 2))
+ .expectedIncrementedBeat(LocalDateTime.of(2017, 12, 18, 20, 5, 2))
+ .expectedNextBeatAfterPublish(LocalDateTime.of(2017, 12, 18, 20, 5, 2))
+ .expectedBeatPublishCount(1));
+ return ret;
+ }
+
+ private TestCase testCase;
+
+ public DrummerTest(final TestCase testCase) {
+ this.testCase = testCase;
+ }
@Test
public void incrementToAlignment() {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final LocalDateTime tomorrow = Drummer.incrementToAlignment(now, 3);
+ final LocalDateTime incrementedBeat = Drummer.incrementToAlignment(
+ testCase.nextBeat,
+ testCase.alignmentHour,
+ testCase.clockOffset);
- Assert.assertEquals(tomorrow.minusDays(1).truncatedTo(ChronoUnit.DAYS), now.truncatedTo(ChronoUnit.DAYS));
- Assert.assertEquals(3, tomorrow.getHour());
+ Assert.assertEquals(
+ "expectedIncrementedBeat",
+ testCase.expectedIncrementedBeat,
+ incrementedBeat);
}
@Test
- public void getNumberOfBeatPublishesNeeded() {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final long eventsNeeded3 = Drummer.getNumberOfBeatPublishesNeeded(now, now.minus(3, ChronoUnit.DAYS));
- Assert.assertEquals(3, eventsNeeded3);
+ public void checkBeatForPublishHelper()
+ {
+ final Set<LocalDateTime> calledForTimes = new HashSet<>();
+ final LocalDateTime nextBeatAfterPublish = Drummer.checkBeatForPublishHelper(
+ testCase.now,
+ testCase.alignmentHour,
+ testCase.nextBeat,
+ testCase.clockOffset,
+ x -> {
+ calledForTimes.add(x);
+ return true;
+ });
+ Assert.assertEquals(
+ "expectedNextBeatAfterPublish",
+ testCase.expectedNextBeatAfterPublish,
+ nextBeatAfterPublish);
+ Assert.assertEquals(
+ "expectedBeatPublishCount",
+ testCase.expectedBeatPublishCount,
+ calledForTimes.size());
- final long eventsNeededPast = Drummer.getNumberOfBeatPublishesNeeded(now, now.plus(1, ChronoUnit.DAYS));
- Assert.assertEquals(0, eventsNeededPast);
-
- final long eventsNeededNow = Drummer.getNumberOfBeatPublishesNeeded(now, now.minus(2, ChronoUnit.MINUTES));
- Assert.assertEquals(1, eventsNeededNow);
}
@Test
- public void checkBeatForPublishAllBeatsSucceed() {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, now.minus(3, ChronoUnit.DAYS), x -> true);
- Assert.assertEquals(Optional.of(Drummer.incrementToAlignment(now, 0)), ret);
- }
-
- @Test
- public void checkBeatForPublishFirstFails() {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final LocalDateTime nextBeat = now.minus(3, ChronoUnit.DAYS);
- @SuppressWarnings("unchecked") final Predicate<LocalDateTime> produceBeatsMock = Mockito.mock(Predicate.class);
- Mockito.when(produceBeatsMock.test(nextBeat)).thenReturn(false);
- final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
- Assert.assertEquals(Optional.of(nextBeat), ret);
+ public void checkBeatForPublishHelperFirstFails() {
+ @SuppressWarnings("unchecked")
+ final Predicate<LocalDateTime> produceBeatsMock = Mockito.mock(Predicate.class);
+ Mockito.when(produceBeatsMock.test(testCase.nextBeat)).thenReturn(false);
+ final LocalDateTime nextBeatAfterPublish = Drummer.checkBeatForPublishHelper(
+ testCase.now,
+ testCase.alignmentHour,
+ testCase.nextBeat,
+ testCase.clockOffset,
+ produceBeatsMock);
+ Assert.assertEquals("nextBeat", testCase.nextBeat, nextBeatAfterPublish);
}
@Test
public void checkBeatForPublishSecondFails() {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final LocalDateTime nextBeat = now.minus(3, ChronoUnit.DAYS);
- final LocalDateTime secondBeat = Drummer.incrementToAlignment(nextBeat, 0);
- @SuppressWarnings("unchecked") final Predicate<LocalDateTime> produceBeatsMock = Mockito.mock(Predicate.class);
- Mockito.when(produceBeatsMock.test(nextBeat)).thenReturn(true);
- Mockito.when(produceBeatsMock.test(secondBeat)).thenReturn(false);
- final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
- Assert.assertEquals(Optional.of(secondBeat), ret);
- }
+ if (testCase.expectedBeatPublishCount < 2)
+ return;
- @Test
- public void checkBeatForPublishNoneNeeded() {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, now.plus(1, ChronoUnit.DAYS),
- x -> { Assert.fail("Pubish shouldn't be called"); return true; });
- Assert.assertEquals(Optional.empty(), ret);
+ final LocalDateTime secondBeat = Drummer.incrementToAlignment(
+ testCase.nextBeat,
+ testCase.alignmentHour,
+ testCase.clockOffset);
+ @SuppressWarnings("unchecked") final Predicate<LocalDateTime> produceBeatsMock = Mockito.mock(Predicate.class);
+ Mockito.when(produceBeatsMock.test(testCase.nextBeat)).thenReturn(true);
+ Mockito.when(produceBeatsMock.test(secondBeat)).thenReturn(false);
+ final LocalDateTime nextBeatAfterPublish = Drummer.checkBeatForPublishHelper(
+ testCase.now,
+ testCase.alignmentHour,
+ testCase.nextBeat,
+ testCase.clockOffset,
+ produceBeatsMock);
+ Assert.assertEquals(secondBeat, nextBeatAfterPublish);
}
}
\ No newline at end of file
diff --git a/shared.gradle b/shared.gradle
index dc7dd71..0170643 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -65,7 +65,9 @@
xml = 'XML_STYLE'
yml = 'SCRIPT_STYLE'
yaml = 'SCRIPT_STYLE'
+ uxf = 'XML_STYLE'
}
ext.year = Calendar.getInstance().get(Calendar.YEAR)
- ext.name = 'The Mifos Initiative'
+ ext.name = 'Kuelap, Inc'
+ skipExistingHeaders true
}