Merge pull request #5 from jazzm0/feature/device-service

This commit is contained in:
jazzm0 2019-12-11 12:13:14 +01:00 committed by GitHub
commit 2caf1a5b38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 805 additions and 0 deletions

4
src/.gitignore vendored
View file

@ -3,3 +3,7 @@
# run "dep ensure --vendor-only" to download the dependencies to vendor/ based
# on the Gopkg.{toml,lock} files in that directory.
vendor/
tamagotchi-service/target
tamagotchi-service/.idea
*.iml

32
src/tamagotchi-service/.gitignore vendored Normal file
View file

@ -0,0 +1,32 @@
.mta
.che
/**/node_modules
/**/target
/**/dist
/**/.project
/**/.settings
/**/.classpath
/**/.springBeans
/**/.factorypath
/**/.idea
// Generated Artifacts
/**/edmx/
/**/gen
/**/src/generated
/**/dist
/**/webapp/localService/metadata.xml
// Files and folders generated by WebIDE build and not to be committed
/uideployer/resources/
/uideployer/deploymentTemp/
/**/package-lock.json
/**/*.mtar

View file

@ -0,0 +1,8 @@
runtime: java11
entrypoint: java -jar target/tamagotchi-service-0.1.0.jar
inbound_services:
- warmup
automatic_scaling:
max_instances: 1
min_instances: 1

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sap</groupId>
<artifactId>tamagotchi-service</artifactId>
<version>0.1.0</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>2.9.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-pubsub</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.google.appengine</groupId>
<artifactId>appengine-maven-plugin</artifactId>
<version>1.9.77</version>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,11 @@
package com.sap.tamagotchi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View file

@ -0,0 +1,24 @@
package com.sap.tamagotchi.configuration;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationConfiguration {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
return objectMapper;
}
}

View file

@ -0,0 +1,65 @@
package com.sap.tamagotchi.controller;
import static com.sap.tamagotchi.model.Color.RED;
import static com.sap.tamagotchi.model.Color.YELLOW;
import static org.springframework.http.ResponseEntity.ok;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.sap.tamagotchi.model.Color;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.sap.tamagotchi.model.CreateDevicePayload;
import com.sap.tamagotchi.model.Device;
import com.sap.tamagotchi.service.TamagotchiService;
@RestController
public class DeviceController {
private final TamagotchiService tamagotchiService;
@Autowired
public DeviceController(TamagotchiService tamagotchiService) {
this.tamagotchiService = tamagotchiService;
}
private static Color mapColor(String productId) {
switch (productId) {
case "66VCHSJNUP":
return RED;
default:
return YELLOW;
}
}
@RequestMapping("/devices/{deviceId}")
public Device getDevice(String deviceId) {
return tamagotchiService.getDevice(deviceId);
}
@RequestMapping("/devices")
public Collection<Device> getDevices() {
return tamagotchiService.getDevices();
}
@PostMapping("/devices")
public ResponseEntity createDevice(@RequestBody Collection<CreateDevicePayload> payload) {
List<Device> devices = new ArrayList<>();
for (CreateDevicePayload p : payload) {
devices.add(tamagotchiService.createDevice(new Device(p.getOwner(), mapColor(p.getProductId()))));
}
return ok(devices);
}
@RequestMapping("/_ah/warmup")
public String warmup() {
return "warming up";
}
}

View file

@ -0,0 +1,24 @@
/**
* Copyright (c) 2019, SAP SE, All rights reserved.
*/
package com.sap.tamagotchi.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.sap.tamagotchi.model.Owner;
@RestController
@RequestMapping("/owner")
public class OwnerController {
@Autowired
Owner owner;
@GetMapping()
public void killTamagotchi() {
owner.killRendomDevice();
}
}

View file

@ -0,0 +1,24 @@
package com.sap.tamagotchi.model;
public class Care {
private int feed = 0;
private int pet = 0;
public int getFeed() {
return feed;
}
public void setFeed(int feed) {
this.feed = feed;
}
public int getPet() {
return pet;
}
public void setPet(int pet) {
this.pet = pet;
}
}

View file

@ -0,0 +1,5 @@
package com.sap.tamagotchi.model;
public enum Color {
RED, YELLOW, BLUE, GREEN
}

View file

@ -0,0 +1,25 @@
package com.sap.tamagotchi.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class CreateDevicePayload {
private final String userId;
private final String productId;
@JsonCreator
public CreateDevicePayload(@JsonProperty("userId") String userId, @JsonProperty("productId") String productId) {
this.userId = userId;
this.productId = productId;
}
@JsonProperty("userId")
public String getOwner() {
return userId;
}
@JsonProperty("productId")
public String getProductId() {
return productId;
}
}

View file

@ -0,0 +1,25 @@
package com.sap.tamagotchi.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DefunctNotification implements IoTMessage {
@JsonProperty("message")
private final String message;
public DefunctNotification(String message) {
this.message = message;
}
@JsonIgnore
@Override
public String getTopic() {
return "tamagotchi-defunct";
}
public String getMessage() {
return message;
}
}

View file

@ -0,0 +1,87 @@
package com.sap.tamagotchi.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Device {
@JsonProperty("id")
private final String id = UUID.randomUUID().toString();
@JsonProperty("owner")
private final String owner;
@JsonProperty("color")
private final Color color;
@JsonProperty("born")
private final Instant born = Instant.now();
@JsonProperty("healthScore")
private int healthScore = 100;
private final Queue<IoTMessage> messages = new ConcurrentLinkedQueue<>();
public Device(String owner, Color color) {
this.owner = owner;
this.color = color;
}
@JsonProperty("id")
public String getId() {
return id;
}
@JsonProperty("owner")
public String getOwner() {
return owner;
}
@JsonProperty("color")
public Color getColor() {
return color;
}
@JsonProperty("born")
public String getBorn() {
return born.toString();
}
@JsonProperty("healthScore")
public int getHealthScore() {
return healthScore;
}
@JsonProperty("isAlive")
public boolean isAlive() {
return healthScore > 1;
}
@JsonIgnore
public void changeHealthScore(int delta) {
int oldScore = healthScore;
if (healthScore >= 1) {
healthScore += delta;
if (healthScore > 150)
healthScore /= 10;
}
if (healthScore < 1) {
healthScore = 0;
messages.add(new DeviceEvent(id, owner, color, born, healthScore, oldScore, Instant.now()));
} else
messages.add(new DeviceEvent(id, owner, color, born, healthScore, null, Instant.now()));
}
@JsonIgnore
public boolean hasMessages() {
return !messages.isEmpty();
}
@JsonIgnore
public Queue<IoTMessage> getMessages() {
return messages;
}
}

View file

@ -0,0 +1,95 @@
package com.sap.tamagotchi.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
public class DeviceEvent implements IoTMessage {
@JsonProperty("id")
private final String id;
@JsonProperty("owner")
private final String owner;
@JsonProperty("color")
private final Color color;
@JsonProperty("born")
private final Instant born;
@JsonProperty("healthScore")
private final Integer healthScore;
@JsonProperty("lastHealthScore")
private final Integer lastHealthScore;
@JsonProperty("eventTime")
private final Instant eventTime;
public DeviceEvent(String id, String owner, Color color, Instant born, Integer healthScore, Integer lastHealthScore, Instant eventTime) {
this.id = id;
this.owner = owner;
this.color = color;
this.born = born;
this.healthScore = healthScore;
this.lastHealthScore = lastHealthScore;
this.eventTime = eventTime;
}
@JsonProperty("id")
public String getId() {
return id;
}
@JsonProperty("owner")
public String getOwner() {
return owner;
}
@JsonProperty("color")
public Color getColor() {
return color;
}
@JsonProperty("born")
public String getBorn() {
return born.toString();
}
@JsonProperty("healthScore")
public Integer getHealthScore() {
return healthScore;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonProperty("lastHealthScore")
public Integer getLastHealthScore() {
return lastHealthScore;
}
@JsonProperty("eventTime")
public String getEventTime() {
return eventTime.toString();
}
@JsonProperty("isAlive")
public boolean isAlive() {
return healthScore > 1;
}
@JsonIgnore
@Override
public String getTopic() {
return "tamagotchi-events";
}
@Override
public String toString() {
return "DeviceEvent{" +
"id='" + id + '\'' +
", owner='" + owner + '\'' +
", color=" + color +
", born=" + born +
", healthScore=" + healthScore +
", eventTime=" + eventTime +
'}';
}
}

View file

@ -0,0 +1,5 @@
package com.sap.tamagotchi.model;
public interface IoTMessage {
String getTopic();
}

View file

@ -0,0 +1,64 @@
/**
* Copyright (c) 2019, SAP SE, All rights reserved.
*/
package com.sap.tamagotchi.model;
import static com.sap.tamagotchi.service.TamagotchiService.DEVICE_EVENT_PROCESSOR_SCHEDULE;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.sap.tamagotchi.service.TamagotchiService;
@Service
@EnableScheduling
public class Owner {
private TamagotchiService tamagotchiService;
@Autowired
public Owner(TamagotchiService tamagotchiService) {
this.tamagotchiService = tamagotchiService;
}
@Scheduled(fixedDelay = DEVICE_EVENT_PROCESSOR_SCHEDULE)
public void setData() {
for (Device d : tamagotchiService.getDevices()) {
String userName = d.getOwner().substring(0, d.getOwner().indexOf("@")).toUpperCase();
int careAboutThePet = 0;
for (int i = 0; i < userName.length(); i++) {
if (d.getColor().toString().indexOf(userName.charAt(i)) != -1) {
careAboutThePet++;
}
}
Care care = new Care();
if (careAboutThePet == 0) {
care.setFeed(-5);
} else {
care.setFeed(5);
}
tamagotchiService.takeCare(d.getId(), care);
}
}
@Scheduled(fixedDelay = 30000)
public void killRendomDevice() {
Collection<Device> devices = tamagotchiService.getDevices();
if (devices != null && devices.iterator().hasNext()) {
Device first = devices.iterator().next();
Care care = new Care();
care.setFeed(-100000);
tamagotchiService.takeCare(first.getId(), care);
}
}
}

View file

@ -0,0 +1,82 @@
package com.sap.tamagotchi.publisher;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutures;
import com.google.cloud.ServiceOptions;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.ProjectTopicName;
import com.google.pubsub.v1.PubsubMessage;
import com.sap.tamagotchi.model.IoTMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
@Service
public class PublisherService {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
// use the default project id
private static final String PROJECT_ID = ServiceOptions.getDefaultProjectId();
private final ObjectMapper mapper;
@Autowired
public PublisherService(ObjectMapper mapper) {
this.mapper = mapper;
}
public void publish(IoTMessage message) throws Exception {
if (message == null) {
LOGGER.info("received null message");
return;
}
String topicId = message.getTopic();
ProjectTopicName topicName = ProjectTopicName.of(PROJECT_ID, topicId);
Publisher publisher = null;
List<ApiFuture<String>> futures = new ArrayList<>();
try {
String stringMessage = mapper.writeValueAsString(message);
// Create a publisher instance with default settings bound to the topic
publisher = Publisher.newBuilder(topicName).build();
LOGGER.info("publish to topic" + publisher.getTopicNameString());
// convert message to bytes
ByteString data = ByteString.copyFromUtf8(stringMessage);
PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
.setData(data)
.build();
LOGGER.info("publish to message" + stringMessage);
// Schedule a message to be published. Messages are automatically batched.
ApiFuture<String> future = publisher.publish(pubsubMessage);
futures.add(future);
} finally {
// Wait on any pending requests
List<String> messageIds = ApiFutures.allAsList(futures).get();
for (String messageId : messageIds) {
System.out.println(messageId);
LOGGER.info("publish successful : " + messageId);
}
if (publisher != null) {
// When finished with the publisher, shutdown to free up resources.
publisher.shutdown();
}
if (messageIds.isEmpty())
LOGGER.info("no messages published ");
}
}
}

View file

@ -0,0 +1,109 @@
package com.sap.tamagotchi.service;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.sap.tamagotchi.model.Care;
import com.sap.tamagotchi.model.DefunctNotification;
import com.sap.tamagotchi.model.Device;
import com.sap.tamagotchi.publisher.PublisherService;
@Service
@EnableScheduling
public class TamagotchiService {
public static final long DEVICE_EVENT_PROCESSOR_SCHEDULE = 5000;
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Map<String, Device> deviceRegistry = Collections.synchronizedMap(new HashMap<>());
private final PublisherService publisherService;
@Autowired
public TamagotchiService(PublisherService publisherService) {
this.publisherService = publisherService;
}
public Device getDevice(String deviceId) {
return deviceRegistry.get(deviceId);
}
public Collection<Device> getDevices() {
return deviceRegistry.values();
}
public Set<String> getDeviceIds() {
return deviceRegistry.keySet();
}
public Device createDevice(Device device) {
deviceRegistry.put(device.getId(), device);
return device;
}
public void takeCare(String deviceId, Care care) {
Device device = deviceRegistry.get(deviceId);
if (device == null) {
return;
}
device.changeHealthScore(care.getFeed());
device.changeHealthScore(care.getPet());
}
@Scheduled(fixedDelay = DEVICE_EVENT_PROCESSOR_SCHEDULE)
private void processDeviceEvents() {
deviceRegistry
.values()
.parallelStream()
.filter(Device::hasMessages)
.forEach(device -> {
while (device.getMessages().peek() != null) {
try {
publisherService.publish(device.getMessages().poll());
} catch (Exception ex) {
LOGGER.error("processing device events failed: {}", ex.getMessage());
}
}
});
// remove dead devices
deviceRegistry
.values()
.parallelStream()
.filter(device -> !device.isAlive())
.forEach(device -> {
sendTamagotchiDefunctNotifiction(device.getId());
deviceRegistry.remove(device.getId());
LOGGER.info("{} has died", device.getId());
});
}
private void sendTamagotchiDefunctNotifiction(String id) {
Device device = deviceRegistry.get(id);
if (device == null || device.getId() == null || device.getOwner() == null) {
return;
}
String defunctMessage = String.format("Tamagotchi %s of %s passed away", device.getId(), device.getOwner());
DefunctNotification defunctNotification = new DefunctNotification(defunctMessage);
try {
publisherService.publish(defunctNotification);
LOGGER.info("defunct notification sent for {}", device.getId());
} catch (Exception ex) {
LOGGER.error("sendTamagotchiDefunctNotifiction failed: {}", ex.getMessage());
}
}
}

View file

@ -0,0 +1,40 @@
/**
* Copyright (c) 2019, SAP SE, All rights reserved.
*/
package com.sap.tamagotchi.model;
import static java.util.Arrays.asList;
import static org.mockito.Mockito.when;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import com.sap.tamagotchi.service.TamagotchiService;;
@RunWith(SpringRunner.class)
@SpringBootTest
public class OwnerTest {
@Autowired
Owner owner;
@MockBean
TamagotchiService tamagotchiService;
Collection<Device> mockDevices;
// @Before
// public void init() {
// MockitoAnnotations.initMocks(this);
// }
@Test
public void testIt() {
when(tamagotchiService.getDevices()).thenReturn(asList(new Device("elisa@sap.com", Color.BLUE)));
owner.setData();
}
}