Merge pull request #6 from myrle-krantz/develop
Fixing up authentication. Not finished integration testing yet though.
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 17862e5..dad54b1 100644
--- a/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
+++ b/component-test/src/main/java/io/mifos/rhythm/AbstractRhythmTest.java
@@ -40,7 +40,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
@@ -52,8 +52,7 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
-
-import static org.mockito.Matchers.*;
+import java.util.Optional;
/**
* @author Myrle Krantz
@@ -109,7 +108,7 @@
@Autowired
EventRecorder eventRecorder;
- @SpyBean
+ @MockBean
BeatPublisherService beatPublisherServiceSpy;
@Before
@@ -132,6 +131,7 @@
}
Beat createBeat(final String applicationName, final String beatIdentifier) throws InterruptedException {
+ final String tenantIdentifier = tenantDataStoreContext.getTenantName();
final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
final Beat beat = new Beat();
@@ -139,13 +139,15 @@
beat.setAlignmentHour(now.getHour());
final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, beat.getAlignmentHour());
- Mockito.doReturn(true).when(beatPublisherServiceSpy).publishBeat(Matchers.eq(beatIdentifier), Matchers.eq(tenantDataStoreContext.getTenantName()), Matchers.eq(applicationName),
- AdditionalMatchers.or(Matchers.eq(expectedBeatTimestamp), Matchers.eq(getNextTimeStamp(expectedBeatTimestamp))));
+ Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceSpy).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(applicationName));
+ Mockito.doReturn(true).when(beatPublisherServiceSpy).publishBeat(Matchers.eq(beatIdentifier), Matchers.eq(tenantIdentifier), Matchers.eq(applicationName),
+ AdditionalMatchers.or(Matchers.eq(expectedBeatTimestamp), Matchers.eq(getNextTimeStamp(expectedBeatTimestamp))));
this.testSubject.createBeat(applicationName, beat);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(applicationName, beat.getIdentifier())));
+ Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(2_000).times(1)).requestPermissionForBeats(tenantIdentifier, applicationName);
Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(2_000).times(1)).publishBeat(beatIdentifier, tenantDataStoreContext.getTenantName(), applicationName, expectedBeatTimestamp);
return beat;
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 352005b..9008f53 100644
--- a/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
+++ b/component-test/src/main/java/io/mifos/rhythm/TestBeats.java
@@ -21,13 +21,13 @@
import io.mifos.rhythm.api.v1.events.EventConstants;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.Matchers;
import org.mockito.Mockito;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
-
-import static org.mockito.Matchers.*;
+import java.util.Optional;
/**
* @author Myrle Krantz
@@ -79,6 +79,7 @@
@Test
public void shouldRetryBeatPublishIfFirstAttemptFails() throws InterruptedException {
+ final String tenantIdentifier = tenantDataStoreContext.getTenantName();
final String appName = "funnybusiness-v4";
final String beatId = "bebopthedowop";
@@ -90,12 +91,13 @@
final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, beat.getAlignmentHour());
- Mockito.when(beatPublisherServiceSpy.publishBeat(beatId, tenantDataStoreContext.getTenantName(), appName, expectedBeatTimestamp)).thenReturn(false, false, true);
+ Mockito.doReturn(Optional.of("boop")).when(beatPublisherServiceSpy).requestPermissionForBeats(Matchers.eq(tenantIdentifier), Matchers.eq(appName));
+ Mockito.when(beatPublisherServiceSpy.publishBeat(beatId, tenantIdentifier, appName, expectedBeatTimestamp)).thenReturn(false, false, true);
this.testSubject.createBeat(appName, beat);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BEAT, new BeatEvent(appName, beat.getIdentifier())));
- Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(10_000).times(3)).publishBeat(beatId, tenantDataStoreContext.getTenantName(), appName, expectedBeatTimestamp);
+ Mockito.verify(beatPublisherServiceSpy, Mockito.timeout(10_000).times(3)).publishBeat(beatId, tenantIdentifier, appName, expectedBeatTimestamp);
}
}
\ No newline at end of file
diff --git a/service/build.gradle b/service/build.gradle
index 26015c3..98c8036 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -33,6 +33,7 @@
[group: 'io.mifos.rhythm', name: 'api', version: project.version],
[group: 'io.mifos.rhythm', name: 'spi', version: project.version],
[group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+ [group: 'io.mifos.identity', name: 'api', version: versions.identity],
[group: 'io.mifos.permitted-feign-client', name: 'library', version: versions.frameworkpermittedfeignclient],
[group: 'com.google.code.gson', name: 'gson'],
[group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
diff --git a/service/src/main/java/io/mifos/rhythm/service/RhythmApplication.java b/service/src/main/java/io/mifos/rhythm/service/RhythmApplication.java
new file mode 100644
index 0000000..fa6f5ec
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/RhythmApplication.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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;
+
+import org.springframework.boot.SpringApplication;
+
+/**
+ * @author Myrle Krantz
+ */
+public class RhythmApplication {
+
+ public RhythmApplication() {
+ super();
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(RhythmConfiguration.class, args);
+ }
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java b/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java
index 1cf17a9..469df5c 100644
--- a/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java
+++ b/service/src/main/java/io/mifos/rhythm/service/RhythmConfiguration.java
@@ -20,14 +20,18 @@
import io.mifos.core.async.config.EnableAsync;
import io.mifos.core.cassandra.config.EnableCassandra;
import io.mifos.core.command.config.EnableCommandProcessing;
+import io.mifos.core.lang.config.EnableApplicationName;
import io.mifos.core.lang.config.EnableServiceException;
import io.mifos.core.lang.config.EnableTenantContext;
import io.mifos.core.mariadb.config.EnableMariaDB;
import io.mifos.permittedfeignclient.config.EnablePermissionRequestingFeignClient;
+import io.mifos.rhythm.service.internal.identity.ApplicationPermissionRequestCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@@ -52,13 +56,15 @@
@EnableServiceException
@EnableScheduling
@EnableTenantContext
-@EnablePermissionRequestingFeignClient
+@EnablePermissionRequestingFeignClient(feignClasses = {ApplicationPermissionRequestCreator.class})
+@RibbonClient(name = "rhythm-v1")
+@EnableApplicationName
+@EnableFeignClients(clients = {ApplicationPermissionRequestCreator.class})
@ComponentScan({
"io.mifos.rhythm.service.rest",
"io.mifos.rhythm.service.config",
"io.mifos.rhythm.service.internal.service",
"io.mifos.rhythm.service.internal.repository",
- "io.mifos.rhythm.service.internal.scheduler",
"io.mifos.rhythm.service.internal.command.handler"
})
@EnableJpaRepositories({
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java
index 7b896fa..79476f0 100644
--- a/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/command/handler/ApplicationCommandHandler.java
@@ -19,6 +19,7 @@
import io.mifos.core.command.annotation.CommandHandler;
import io.mifos.rhythm.api.v1.events.EventConstants;
import io.mifos.rhythm.service.internal.command.DeleteApplicationCommand;
+import io.mifos.rhythm.service.internal.repository.ApplicationRepository;
import io.mifos.rhythm.service.internal.repository.BeatRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
@@ -29,12 +30,17 @@
@SuppressWarnings("unused")
@Aggregate
public class ApplicationCommandHandler {
+ private final ApplicationRepository applicationRepository;
private final BeatRepository beatRepository;
private final EventHelper eventHelper;
@Autowired
- public ApplicationCommandHandler(final BeatRepository beatRepository, final EventHelper eventHelper) {
+ public ApplicationCommandHandler(
+ final ApplicationRepository applicationRepository,
+ final BeatRepository beatRepository,
+ final EventHelper eventHelper) {
super();
+ this.applicationRepository = applicationRepository;
this.beatRepository = beatRepository;
this.eventHelper = eventHelper;
}
@@ -42,6 +48,7 @@
@CommandHandler
@Transactional
public void process(final DeleteApplicationCommand deleteApplicationCommand) {
+ this.applicationRepository.deleteByTenantIdentifierAndApplicationName(deleteApplicationCommand.getTenantIdentifier(), deleteApplicationCommand.getApplicationName());
this.beatRepository.deleteByTenantIdentifierAndApplicationName(deleteApplicationCommand.getTenantIdentifier(), deleteApplicationCommand.getApplicationName());
eventHelper.sendEvent(EventConstants.DELETE_APPLICATION, deleteApplicationCommand.getTenantIdentifier(), deleteApplicationCommand.getApplicationName());
}
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 f287f86..07480a3 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
@@ -20,12 +20,16 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.rhythm.api.v1.events.BeatEvent;
import io.mifos.rhythm.api.v1.events.EventConstants;
+import io.mifos.rhythm.service.ServiceConstants;
import io.mifos.rhythm.service.internal.command.CreateBeatCommand;
import io.mifos.rhythm.service.internal.command.DeleteBeatCommand;
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.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;
@@ -36,20 +40,33 @@
@SuppressWarnings("unused")
@Aggregate
public class BeatCommandHandler {
-
+ private final IdentityPermittableGroupService identityPermittableGroupService;
private final BeatRepository beatRepository;
private final EventHelper eventHelper;
+ private final Logger logger;
@Autowired
- public BeatCommandHandler(final BeatRepository beatRepository, final EventHelper eventHelper) {
+ public BeatCommandHandler(
+ final IdentityPermittableGroupService identityPermittableGroupService,
+ final BeatRepository beatRepository,
+ final EventHelper eventHelper,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
super();
+ this.identityPermittableGroupService = identityPermittableGroupService;
this.beatRepository = beatRepository;
this.eventHelper = eventHelper;
+ this.logger = logger;
}
@CommandHandler
@Transactional
public void process(final CreateBeatCommand createBeatCommand) {
+ final boolean applicationHasRequestForAccessPermission = identityPermittableGroupService.checkThatApplicationHasRequestForAccessPermission(
+ createBeatCommand.getTenantIdentifier(), createBeatCommand.getApplicationName());
+ if (!applicationHasRequestForAccessPermission) {
+ logger.warn("Rhythm needs permission to publish beats to application, but couldn't request that permission for tenant '{}' and application '{}'.",
+ createBeatCommand.getApplicationName(), createBeatCommand.getTenantIdentifier());
+ }
final BeatEntity entity = BeatMapper.map(
createBeatCommand.getTenantIdentifier(),
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/identity/ApplicationPermissionRequestCreator.java b/service/src/main/java/io/mifos/rhythm/service/internal/identity/ApplicationPermissionRequestCreator.java
new file mode 100644
index 0000000..498638b
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/identity/ApplicationPermissionRequestCreator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.identity;
+
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.api.annotation.ThrowsException;
+import io.mifos.identity.api.v1.client.ApplicationPermissionAlreadyExistsException;
+import io.mifos.identity.api.v1.domain.Permission;
+import io.mifos.permittedfeignclient.annotation.EndpointSet;
+import io.mifos.permittedfeignclient.annotation.PermittedFeignClientsConfiguration;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * @author Myrle Krantz
+ */
+@EndpointSet(identifier = "rhythm__v1__identity__v1")
+@FeignClient(name="identity-v1", path="/identity/v1", configuration=PermittedFeignClientsConfiguration.class)
+public interface ApplicationPermissionRequestCreator {
+
+ @RequestMapping(value = "/applications/{applicationidentifier}/permissions", method = RequestMethod.POST,
+ consumes = {MediaType.APPLICATION_JSON_VALUE},
+ produces = {MediaType.ALL_VALUE})
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ApplicationPermissionAlreadyExistsException.class)
+ @Permittable(groupId = io.mifos.identity.api.v1.PermittableGroupIds.APPLICATION_SELF_MANAGEMENT)
+ void createApplicationPermission(@PathVariable("applicationidentifier") String applicationIdentifier, Permission permission);
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationEntity.java b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationEntity.java
new file mode 100644
index 0000000..5cb48dc
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationEntity.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.*;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+@Entity
+@Table(name = "khepri_apps")
+public class ApplicationEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "tenant_identifier", nullable = false)
+ private String tenantIdentifier;
+
+ @Column(name = "application_name", nullable = false)
+ private String applicationName;
+
+ @Column(name = "permittable_identifier")
+ private String consumerPermittableGroupIdentifier;
+
+ public ApplicationEntity() {
+ }
+
+ public ApplicationEntity(String tenantIdentifier, String applicationName, String consumerPermittableGroupIdentifier) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.applicationName = applicationName;
+ this.consumerPermittableGroupIdentifier = consumerPermittableGroupIdentifier;
+ }
+
+ 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 String getApplicationName() {
+ return applicationName;
+ }
+
+ public void setApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ }
+
+ public String getConsumerPermittableGroupIdentifier() {
+ return consumerPermittableGroupIdentifier;
+ }
+
+ public void setConsumerPermittableGroupIdentifier(String consumerPermittableGroupIdentifier) {
+ this.consumerPermittableGroupIdentifier = consumerPermittableGroupIdentifier;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ApplicationEntity that = (ApplicationEntity) o;
+ return Objects.equals(tenantIdentifier, that.tenantIdentifier) &&
+ Objects.equals(applicationName, that.applicationName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tenantIdentifier, applicationName);
+ }
+
+ @Override
+ public String toString() {
+ return "ApplicationEntity{" +
+ "id=" + id +
+ ", tenantIdentifier='" + tenantIdentifier + '\'' +
+ ", applicationName='" + applicationName + '\'' +
+ ", consumerPermittableGroupIdentifier='" + consumerPermittableGroupIdentifier + '\'' +
+ '}';
+ }
+}
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
new file mode 100644
index 0000000..da3e821
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/repository/ApplicationRepository.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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 org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Repository
+public interface ApplicationRepository extends JpaRepository<ApplicationEntity, Long> {
+ void deleteByTenantIdentifierAndApplicationName(String tenantIdentifier, String applicationName);
+ Optional<ApplicationEntity> findByTenantIdentifierAndApplicationName(String tenantIdentifier, String applicationName);
+}
diff --git a/service/src/main/java/io/mifos/rhythm/service/internal/scheduler/Drummer.java b/service/src/main/java/io/mifos/rhythm/service/internal/scheduler/Drummer.java
deleted file mode 100644
index 4f84037..0000000
--- a/service/src/main/java/io/mifos/rhythm/service/internal/scheduler/Drummer.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2017 The Mifos Initiative.
- *
- * 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.scheduler;
-
-import io.mifos.rhythm.service.ServiceConstants;
-import io.mifos.rhythm.service.internal.repository.BeatEntity;
-import io.mifos.rhythm.service.internal.repository.BeatRepository;
-import io.mifos.rhythm.service.internal.service.BeatPublisherService;
-import org.slf4j.Logger;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.dao.InvalidDataAccessResourceUsageException;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-/**
- * @author Myrle Krantz
- */
-@SuppressWarnings({"unused", "WeakerAccess"})
-@Component
-public class Drummer {
- private final BeatPublisherService beatPublisherService;
- private final BeatRepository beatRepository;
- private final Logger logger;
-
- @Autowired
- public Drummer(
- final BeatPublisherService beatPublisherService,
- final BeatRepository beatRepository,
- @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
- this.beatPublisherService = beatPublisherService;
- this.beatRepository = beatRepository;
- this.logger = logger;
- }
-
- @Scheduled(initialDelayString = "${rhythm.beatCheckRate}", fixedRateString = "${rhythm.beatCheckRate}")
- @Transactional
- public void checkForBeatsNeeded() {
- try {
- final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- //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) -> {
- logger.info("Checking if beat {} needs publishing.", beat);
- final Optional<LocalDateTime> nextBeat = beatPublisherService.checkBeatForPublish(now, beat.getBeatIdentifier(), beat.getTenantIdentifier(), beat.getApplicationName(), beat.getAlignmentHour(), beat.getNextBeat());
- nextBeat.ifPresent(x -> {
- beat.setNextBeat(x);
- beatRepository.save(beat);
- });
- });
-
- }
- catch (final InvalidDataAccessResourceUsageException e) {
- logger.info("InvalidDataAccessResourceUsageException in check for scheduled beats, probably " +
- "because initialize hasn't been called yet. {}", e);
- }
- }
-}
\ No newline at end of file
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 3d57c9a..6495db2 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
@@ -15,12 +15,18 @@
*/
package io.mifos.rhythm.service.internal.service;
+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.lang.ApplicationName;
import io.mifos.core.lang.AutoTenantContext;
import io.mifos.core.lang.DateConverter;
+import io.mifos.identity.api.v1.client.ApplicationPermissionAlreadyExistsException;
+import io.mifos.identity.api.v1.domain.Permission;
import io.mifos.permittedfeignclient.service.ApplicationAccessTokenService;
import io.mifos.rhythm.service.config.RhythmProperties;
+import io.mifos.rhythm.service.internal.identity.ApplicationPermissionRequestCreator;
+import io.mifos.rhythm.spi.v1.PermittableGroupIds;
import io.mifos.rhythm.spi.v1.client.BeatListener;
import io.mifos.rhythm.spi.v1.domain.BeatPublish;
import org.slf4j.Logger;
@@ -30,23 +36,23 @@
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
-import javax.annotation.Nonnull;
import java.time.LocalDateTime;
-import java.time.temporal.ChronoUnit;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.stream.Stream;
import static io.mifos.rhythm.service.ServiceConstants.LOGGER_NAME;
/**
* @author Myrle Krantz
*/
+@SuppressWarnings("WeakerAccess")
@Service
public class BeatPublisherService {
private final DiscoveryClient discoveryClient;
+ private final ApplicationPermissionRequestCreator applicationPermissionRequestCreator;
private final ApplicationAccessTokenService applicationAccessTokenService;
+ private final ApplicationName rhythmApplicationName;
private final ApiFactory apiFactory;
private final RhythmProperties properties;
private final Logger logger;
@@ -54,19 +60,54 @@
@Autowired
public BeatPublisherService(
@SuppressWarnings("SpringJavaAutowiringInspection") final DiscoveryClient discoveryClient,
+ @SuppressWarnings("SpringJavaAutowiringInspection") final ApplicationPermissionRequestCreator applicationPermissionRequestCreator,
@SuppressWarnings("SpringJavaAutowiringInspection") final ApplicationAccessTokenService applicationAccessTokenService,
+ final ApplicationName rhythmApplicationName,
final ApiFactory apiFactory,
final RhythmProperties properties,
@Qualifier(LOGGER_NAME) final Logger logger) {
this.discoveryClient = discoveryClient;
+ this.applicationPermissionRequestCreator = applicationPermissionRequestCreator;
this.applicationAccessTokenService = applicationAccessTokenService;
+ this.rhythmApplicationName = rhythmApplicationName;
this.apiFactory = apiFactory;
this.properties = properties;
this.logger = logger;
}
/**
- * Authenticate with identity and publish the beat to the application. This function performs all the internal
+ * Register a request to access an endpoint with identity. This only creates the request. The request must be
+ * accepted by the user named in rhythm's configuration before beats can actually be sent. This function makes
+ * calls to identity, and therefore most be mocked in unit and component tests.
+ *
+ * @param tenantIdentifier The tenant identifier as provided via the tenant header when the beat was created.
+ * @param applicationName The name of the application the beat should be sent to.
+ *
+ * @return true if the beat was published. false if the beat was not published, or we just don't know.
+ */
+ @SuppressWarnings("WeakerAccess") //Access is public for mocking in component test.
+ public Optional<String> requestPermissionForBeats(final String tenantIdentifier, final String applicationName) {
+ try (final AutoTenantContext ignored = new AutoTenantContext(tenantIdentifier)) {
+ try (final AutoUserContext ignored2 = new AutoUserContext(properties.getUser(), "")) {
+ final String consumerPermittableGroupIdentifier = PermittableGroupIds.forApplication(applicationName);
+ final Permission publishBeatPermission = new Permission();
+ publishBeatPermission.setAllowedOperations(Collections.singleton(AllowedOperation.CHANGE));
+ publishBeatPermission.setPermittableEndpointGroupIdentifier(consumerPermittableGroupIdentifier);
+ try {
+ applicationPermissionRequestCreator.createApplicationPermission(rhythmApplicationName.toString(), publishBeatPermission);
+ }
+ catch (final ApplicationPermissionAlreadyExistsException ignored3) { }
+
+ return Optional.of(consumerPermittableGroupIdentifier);
+ }
+ }
+ catch (final Throwable e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Authenticate with identity and publish the beat to the application. This function performs all the scheduled
* interprocess communication in rhythm, and therefore most be mocked in unit and component tests.
*
* @param beatIdentifier The identifier of the beat as provided when the beat was created.
@@ -76,7 +117,7 @@
*
* @return true if the beat was published. false if the beat was not published, or we just don't know.
*/
- @SuppressWarnings("WeakerAccess") //Access is public for spying in component test.
+ @SuppressWarnings("WeakerAccess") //Access is public for mocking in component test.
public boolean publishBeat(
final String beatIdentifier,
final String tenantIdentifier,
@@ -92,64 +133,27 @@
final ServiceInstance beatListenerService = applicationsByName.get(0);
final BeatListener beatListener = apiFactory.create(BeatListener.class, beatListenerService.getUri().toString());
- final String accessToken = applicationAccessTokenService.getAccessToken(
- properties.getUser(), getEndointSetIdentifier(applicationName));
- try (final AutoUserContext ignored2 = new AutoUserContext(properties.getUser(), accessToken)) {
- try (final AutoTenantContext ignored = new AutoTenantContext(tenantIdentifier)) {
+ try (final AutoTenantContext ignored = new AutoTenantContext(tenantIdentifier)) {
+ final String accessToken;
+ try {
+ accessToken = applicationAccessTokenService.getAccessToken(
+ properties.getUser(), tenantIdentifier);
+ }
+ catch (final Exception e) {
+ logger.warn("Unable to publish beat '{}' to application '{}' for tenant '{}', " +
+ "because access token could not be acquired from identity. Exception was {}.",
+ beatIdentifier, applicationName, tenantIdentifier, e);
+ return false;
+ }
+ try (final AutoUserContext ignored2 = new AutoUserContext(properties.getUser(), accessToken)) {
beatListener.publishBeat(beatPublish);
return true;
}
}
catch (final Throwable e) {
+ logger.warn("Unable to publish beat '{}' to application '{}' for tenant '{}', " +
+ "because exception was thrown in publish {}.", beatIdentifier, applicationName, tenantIdentifier, e);
return false;
}
}
-
- private static String getEndointSetIdentifier(final String applicationName) {
- return applicationName.replace("-", "__") + "__khepri";
- }
-
- public Optional<LocalDateTime> checkBeatForPublish(
- final LocalDateTime now,
- final String beatIdentifier,
- final String tenantIdentifier,
- final String applicationName,
- final Integer alignmentHour,
- final LocalDateTime nextBeat) {
- return checkBeatForPublishHelper(now, alignmentHour, nextBeat, x -> publishBeat(beatIdentifier, tenantIdentifier, applicationName, x));
- }
-
- //Helper is separated from original function so that it can be unit-tested separately from publishBeat.
- static Optional<LocalDateTime> checkBeatForPublishHelper(
- final LocalDateTime now,
- final Integer alignmentHour,
- final LocalDateTime nextBeat,
- final Predicate<LocalDateTime> publishSucceeded) {
- final long numberOfBeatPublishesNeeded = getNumberOfBeatPublishesNeeded(now, nextBeat);
- if (numberOfBeatPublishesNeeded == 0)
- return Optional.empty();
-
- 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));
- }
-
- 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)
- {
- return toIncrement.plusDays(1).truncatedTo(ChronoUnit.DAYS).plusHours(alignmentHour);
- }
}
\ 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
new file mode 100644
index 0000000..e8cda57
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/Drummer.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.service.ServiceConstants;
+import io.mifos.rhythm.service.internal.repository.BeatEntity;
+import io.mifos.rhythm.service.internal.repository.BeatRepository;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.dao.InvalidDataAccessResourceUsageException;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Nonnull;
+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 Logger logger;
+
+ @Autowired
+ public Drummer(
+ final IdentityPermittableGroupService identityPermittableGroupService,
+ final BeatPublisherService beatPublisherService,
+ final BeatRepository beatRepository,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
+ this.identityPermittableGroupService = identityPermittableGroupService;
+ this.beatPublisherService = beatPublisherService;
+ this.beatRepository = beatRepository;
+ this.logger = logger;
+ }
+
+ @Scheduled(initialDelayString = "${rhythm.beatCheckRate}", fixedRateString = "${rhythm.beatCheckRate}")
+ @Transactional
+ public void checkForBeatsNeeded() {
+ try {
+ final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
+ //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) -> {
+ final boolean applicationHasRequestForAccessPermission
+ = identityPermittableGroupService.checkThatApplicationHasRequestForAccessPermission(
+ beat.getTenantIdentifier(), beat.getApplicationName());
+ if (!applicationHasRequestForAccessPermission) {
+ logger.info("Not checking if beat {} needs publishing, because application access needed to publish is not available.", beat);
+ }
+ else {
+ logger.info("Checking if beat {} needs publishing.", beat);
+ final Optional<LocalDateTime> nextBeat = checkBeatForPublish(
+ now,
+ beat.getBeatIdentifier(),
+ beat.getTenantIdentifier(),
+ beat.getApplicationName(),
+ beat.getAlignmentHour(),
+ beat.getNextBeat());
+ nextBeat.ifPresent(y -> {
+ beat.setNextBeat(y);
+ beatRepository.save(beat);
+ });
+ }
+ });
+
+ }
+ catch (final InvalidDataAccessResourceUsageException e) {
+ logger.info("InvalidDataAccessResourceUsageException in check for scheduled beats, probably " +
+ "because initialize hasn't been called yet. {}", e);
+ }
+ }
+
+ public Optional<LocalDateTime> checkBeatForPublish(
+ final LocalDateTime now,
+ final String beatIdentifier,
+ final String tenantIdentifier,
+ final String applicationName,
+ final Integer alignmentHour,
+ final LocalDateTime nextBeat) {
+ return checkBeatForPublishHelper(now, alignmentHour, nextBeat,
+ x -> beatPublisherService.publishBeat(beatIdentifier, tenantIdentifier, applicationName, x));
+ }
+
+ //Helper is separated from original function so that it can be unit-tested separately from publishBeat.
+ static Optional<LocalDateTime> checkBeatForPublishHelper(
+ final LocalDateTime now,
+ final Integer alignmentHour,
+ final LocalDateTime nextBeat,
+ final Predicate<LocalDateTime> publishSucceeded) {
+ final long numberOfBeatPublishesNeeded = getNumberOfBeatPublishesNeeded(now, nextBeat);
+ if (numberOfBeatPublishesNeeded == 0)
+ return Optional.empty();
+
+ 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));
+ }
+
+ 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)
+ {
+ return toIncrement.plusDays(1).truncatedTo(ChronoUnit.DAYS).plusHours(alignmentHour);
+ }
+}
\ 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
new file mode 100644
index 0000000..f8dab7f
--- /dev/null
+++ b/service/src/main/java/io/mifos/rhythm/service/internal/service/IdentityPermittableGroupService.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.service.internal.repository.ApplicationEntity;
+import io.mifos.rhythm.service.internal.repository.ApplicationRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class IdentityPermittableGroupService {
+ private final ApplicationRepository applicationRepository;
+ private final BeatPublisherService beatPublisherService;
+
+ @Autowired
+ public IdentityPermittableGroupService(
+ final ApplicationRepository applicationRepository,
+ final BeatPublisherService beatPublisherService) {
+ this.applicationRepository = applicationRepository;
+ this.beatPublisherService = beatPublisherService;
+ }
+
+ @Transactional
+ public boolean checkThatApplicationHasRequestForAccessPermission(
+ final String tenantIdentifier,
+ final String applicationName) {
+ final Optional<ApplicationEntity> findApplication = applicationRepository.findByTenantIdentifierAndApplicationName(
+ tenantIdentifier,
+ applicationName);
+ if (findApplication.isPresent())
+ return true;
+ else {
+ final Optional<String> ret = beatPublisherService.requestPermissionForBeats(tenantIdentifier, applicationName);
+
+ ret.ifPresent(x -> {
+ final ApplicationEntity saveApplication = new ApplicationEntity();
+ saveApplication.setTenantIdentifier(tenantIdentifier);
+ saveApplication.setApplicationName(applicationName);
+ saveApplication.setConsumerPermittableGroupIdentifier(x);
+ applicationRepository.save(saveApplication);
+ });
+
+ return ret.isPresent();
+ }
+ }
+}
diff --git a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
index bd253d9..8b6ebe2 100644
--- a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
+++ b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
@@ -19,7 +19,7 @@
CREATE TABLE khepri_beats (
id BIGINT NOT NULL AUTO_INCREMENT,
- tenant_identifier VARCHAR(64) NOT NULL,
+ tenant_identifier VARCHAR(32) NOT NULL,
application_name VARCHAR(64) NOT NULL,
beat_identifier VARCHAR(32) NOT NULL,
alignment_hour INT NOT NULL,
@@ -27,3 +27,12 @@
CONSTRAINT khepri_beats_uq UNIQUE (tenant_identifier, application_name, beat_identifier),
CONSTRAINT khepri_beats_pk PRIMARY KEY (id)
);
+
+CREATE TABLE khepri_apps (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_identifier VARCHAR(32) NOT NULL,
+ application_name VARCHAR(32) NOT NULL,
+ permittable_identifier VARCHAR(32) NOT NULL,
+ CONSTRAINT khepri_apps_uq UNIQUE (tenant_identifier, application_name),
+ CONSTRAINT khepri_apps_pk PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/service/src/main/resources/logback.xml b/service/src/main/resources/logback.xml
new file mode 100644
index 0000000..16f5144
--- /dev/null
+++ b/service/src/main/resources/logback.xml
@@ -0,0 +1,55 @@
+<!--
+
+ Copyright 2017 The Mifos Initiative.
+
+ 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.
+
+-->
+<configuration>
+ <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>logs/rhythm.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>rhythm.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <maxHistory>7</maxHistory>
+ <totalSizeCap>2GB</totalSizeCap>
+ </rollingPolicy>
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="com" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="org" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="io" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="net" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <root level="DEBUG">
+ <appender-ref ref="FILE"/>
+ </root>
+</configuration>
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/rhythm/service/internal/service/BeatPublisherServiceTest.java b/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
similarity index 70%
rename from service/src/test/java/io/mifos/rhythm/service/internal/service/BeatPublisherServiceTest.java
rename to service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
index 3d82b75..3cb376d 100644
--- a/service/src/test/java/io/mifos/rhythm/service/internal/service/BeatPublisherServiceTest.java
+++ b/service/src/test/java/io/mifos/rhythm/service/internal/service/DrummerTest.java
@@ -28,12 +28,12 @@
/**
* @author Myrle Krantz
*/
-public class BeatPublisherServiceTest {
+public class DrummerTest {
@Test
public void incrementToAlignment() {
final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final LocalDateTime tomorrow = BeatPublisherService.incrementToAlignment(now, 3);
+ final LocalDateTime tomorrow = Drummer.incrementToAlignment(now, 3);
Assert.assertEquals(tomorrow.minusDays(1).truncatedTo(ChronoUnit.DAYS), now.truncatedTo(ChronoUnit.DAYS));
Assert.assertEquals(3, tomorrow.getHour());
@@ -42,21 +42,21 @@
@Test
public void getNumberOfBeatPublishesNeeded() {
final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final long eventsNeeded3 = BeatPublisherService.getNumberOfBeatPublishesNeeded(now, now.minus(3, ChronoUnit.DAYS));
+ final long eventsNeeded3 = Drummer.getNumberOfBeatPublishesNeeded(now, now.minus(3, ChronoUnit.DAYS));
Assert.assertEquals(3, eventsNeeded3);
- final long eventsNeededPast = BeatPublisherService.getNumberOfBeatPublishesNeeded(now, now.plus(1, ChronoUnit.DAYS));
+ final long eventsNeededPast = Drummer.getNumberOfBeatPublishesNeeded(now, now.plus(1, ChronoUnit.DAYS));
Assert.assertEquals(0, eventsNeededPast);
- final long eventsNeededNow = BeatPublisherService.getNumberOfBeatPublishesNeeded(now, now.minus(2, ChronoUnit.MINUTES));
+ 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 = BeatPublisherService.checkBeatForPublishHelper(now, 0, now.minus(3, ChronoUnit.DAYS), x -> true);
- Assert.assertEquals(Optional.of(BeatPublisherService.incrementToAlignment(now, 0)), ret);
+ 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
@@ -65,7 +65,7 @@
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 = BeatPublisherService.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
+ final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
Assert.assertEquals(Optional.of(nextBeat), ret);
}
@@ -73,18 +73,18 @@
public void checkBeatForPublishSecondFails() {
final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
final LocalDateTime nextBeat = now.minus(3, ChronoUnit.DAYS);
- final LocalDateTime secondBeat = BeatPublisherService.incrementToAlignment(nextBeat, 0);
+ 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 = BeatPublisherService.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
+ final Optional<LocalDateTime> ret = Drummer.checkBeatForPublishHelper(now, 0, nextBeat, produceBeatsMock);
Assert.assertEquals(Optional.of(secondBeat), ret);
}
@Test
public void checkBeatForPublishNoneNeeded() {
final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
- final Optional<LocalDateTime> ret = BeatPublisherService.checkBeatForPublishHelper(now, 0, now.plus(1, ChronoUnit.DAYS),
+ 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);
}
diff --git a/shared.gradle b/shared.gradle
index 4403324..dc7dd71 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -1,5 +1,5 @@
group 'io.mifos.rhythm'
-version '0.1.0.BUILD-SNAPSHOT'
+version '0.1.0-BUILD-SNAPSHOT'
ext.versions = [
frameworkapi : '0.1.0-BUILD-SNAPSHOT',
@@ -11,6 +11,7 @@
frameworktest: '0.1.0-BUILD-SNAPSHOT',
frameworkanubis: '0.1.0-BUILD-SNAPSHOT',
frameworkpermittedfeignclient: '0.1.0-BUILD-SNAPSHOT',
+ identity: '0.1.0-BUILD-SNAPSHOT',
validator : '5.3.0.Final'
]
diff --git a/spi/build.gradle b/spi/build.gradle
index 17c6648..35ae7b7 100644
--- a/spi/build.gradle
+++ b/spi/build.gradle
@@ -18,6 +18,8 @@
compile(
[group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
[group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+ [group: 'io.mifos.core', name: 'cassandra', version: versions.frameworkcassandra],
+ [group: 'io.mifos.core', name: 'command', version: versions.frameworkcommand],
[group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
[group: 'org.hibernate', name: 'hibernate-validator-annotation-processor', version: versions.validator]
)
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/PermittableGroupIds.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/PermittableGroupIds.java
new file mode 100644
index 0000000..cd2a35c
--- /dev/null
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/PermittableGroupIds.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.spi.v1;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public interface PermittableGroupIds {
+ static String forApplication(final String beatConsumingApplicationName) {
+ return beatConsumingApplicationName.replace("-", "__") + "__khepri";
+ }
+}
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java
index c94a5d1..0e408e6 100644
--- a/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/client/BeatListener.java
@@ -18,7 +18,6 @@
import io.mifos.rhythm.spi.v1.domain.BeatPublish;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -26,10 +25,12 @@
* @author Myrle Krantz
*/
@SuppressWarnings("unused")
-@FeignClient(path="/beatlistener/v1")
+@FeignClient
public interface BeatListener {
+ String PUBLISH_BEAT_PATH = "/beatlistener/v1/publishedbeats";
+
@RequestMapping(
- value = "/publishedbeats",
+ value = PUBLISH_BEAT_PATH,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/events/BeatPublishEvent.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/BeatPublishEvent.java
new file mode 100644
index 0000000..c07a20d
--- /dev/null
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/BeatPublishEvent.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.spi.v1.events;
+
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class BeatPublishEvent {
+ String applicationName;
+ String beatIdentifier;
+ String forTime;
+
+ public BeatPublishEvent() {
+ }
+
+ public BeatPublishEvent(String applicationName, String beatIdentifier, String forTime) {
+ this.applicationName = applicationName;
+ this.beatIdentifier = beatIdentifier;
+ this.forTime = forTime;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ public void setApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ }
+
+ public String getBeatIdentifier() {
+ return beatIdentifier;
+ }
+
+ public void setBeatIdentifier(String beatIdentifier) {
+ this.beatIdentifier = beatIdentifier;
+ }
+
+ public String getForTime() {
+ return forTime;
+ }
+
+ public void setForTime(String forTime) {
+ this.forTime = forTime;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BeatPublishEvent that = (BeatPublishEvent) o;
+ return Objects.equals(applicationName, that.applicationName) &&
+ Objects.equals(beatIdentifier, that.beatIdentifier) &&
+ Objects.equals(forTime, that.forTime);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(applicationName, beatIdentifier, forTime);
+ }
+
+ @Override
+ public String toString() {
+ return "BeatPublishEvent{" +
+ "applicationName='" + applicationName + '\'' +
+ ", beatIdentifier='" + beatIdentifier + '\'' +
+ ", forTime='" + forTime + '\'' +
+ '}';
+ }
+}
diff --git a/spi/src/main/java/io/mifos/rhythm/spi/v1/events/EventConstants.java b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/EventConstants.java
new file mode 100644
index 0000000..25869ba
--- /dev/null
+++ b/spi/src/main/java/io/mifos/rhythm/spi/v1/events/EventConstants.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.spi.v1.events;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public interface EventConstants {
+ String DESTINATION = "beats-v1";
+ String SELECTOR_NAME = "action";
+ String POST_PUBLISHEDBEAT = "post-publishedbeat";
+ String SELECTOR_POST_PUBLISHEDBEAT = SELECTOR_NAME + " = '" + POST_PUBLISHEDBEAT + "'";
+}