├── .env.example ├── .github ├── dependabot.yml └── workflows │ └── docker-upload.yml ├── .gitignore ├── Dockerfile ├── README.md ├── default-debezium.json ├── pom.xml └── src └── main ├── java └── com │ └── example │ └── datastore │ ├── DataStoreApplication.java │ ├── config │ ├── KeyHelper.java │ ├── RedisConfig.java │ └── RedisSchema.java │ ├── model │ ├── Data.java │ ├── MeasurementType.java │ ├── Summary.java │ ├── SummaryType.java │ └── exception │ │ └── SensorNotFoundException.java │ ├── repository │ ├── SummaryRepository.java │ └── SummaryRepositoryImpl.java │ ├── service │ ├── CDCEventConsumer.java │ ├── DebeziumEventConsumerImpl.java │ ├── SummaryService.java │ └── SummaryServiceImpl.java │ └── web │ ├── controller │ ├── AnalyticsController.java │ └── ControllerAdvice.java │ ├── dto │ └── SummaryDto.java │ └── mapper │ ├── Mappable.java │ └── SummaryMapper.java └── resources └── application.yaml /.env.example: -------------------------------------------------------------------------------- 1 | REDIS_HOST=localhost 2 | REDIS_PORT=6379 3 | KAFKA_BOOTSTRAP_SERVERS=localhost:9092 4 | 5 | KAFKA_BROKER_ID=1 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/docker-upload.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Login to Docker Hub 14 | uses: docker/login-action@v2 15 | with: 16 | username: ${{ secrets.DOCKERHUB_USERNAME }} 17 | password: ${{ secrets.DOCKERHUB_TOKEN }} 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v2 20 | - name: Build and Push to Docker Hub 21 | uses: mr-smithers-excellent/docker-build-push@v5 22 | with: 23 | image: ${{ secrets.DOCKERHUB_USERNAME }}/data-store-microservice 24 | tags: 0.0.$GITHUB_RUN_NUMBER, latest 25 | dockerfile: Dockerfile 26 | registry: docker.io 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.5-openjdk-17 AS build 2 | COPY pom.xml . 3 | RUN mvn dependency:go-offline 4 | COPY /src /src 5 | RUN mvn clean package -DskipTests 6 | 7 | FROM openjdk:17-jdk-slim 8 | COPY --from=build /target/*.jar application.jar 9 | EXPOSE 8083 10 | ENTRYPOINT ["java", "-jar", "application.jar"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data store microservice 2 | 3 | This is data store microservice 4 | for [YouTube course](https://www.youtube.com/playlist?list=PL3Ur78l82EFBhKojbSO26BVqQ7n4AthHC). 5 | 6 | This application receives data 7 | from [Data analyser service](https://github.com/IlyaLisov/data-analyser-microservice) 8 | with Apache Kafka and Debezium. 9 | 10 | ### Usage 11 | 12 | To start an application you need to pass variables to `.env` file. 13 | 14 | You can use example `.env.example` file with some predefined environments. 15 | 16 | You can find Docker compose file 17 | in [Data analyser service](https://github.com/IlyaLisov/data-analyser-microservice) `docker/docker-compose.yaml`. 18 | 19 | Application is running on port `8083`. 20 | 21 | All insignificant features (checkstyle, build check, dto validation) are not 22 | presented. 23 | 24 | Just after startup application will try to connect to Apache Kafka and begin to 25 | listen topics from `data` topic created by Debezium. 26 | -------------------------------------------------------------------------------- /default-debezium.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "struct", 4 | "fields": [ 5 | { 6 | "type": "int64", 7 | "optional": false, 8 | "default": 0, 9 | "field": "id" 10 | }, 11 | { 12 | "type": "int64", 13 | "optional": false, 14 | "field": "sensor_id" 15 | }, 16 | { 17 | "type": "int64", 18 | "optional": false, 19 | "name": "io.debezium.time.MicroTimestamp", 20 | "version": 1, 21 | "field": "timestamp" 22 | }, 23 | { 24 | "type": "double", 25 | "optional": false, 26 | "field": "measurement" 27 | }, 28 | { 29 | "type": "string", 30 | "optional": false, 31 | "field": "type" 32 | } 33 | ], 34 | "optional": false, 35 | "name": "pg-replica.public.data.Value" 36 | }, 37 | "payload": { 38 | "id": 66, 39 | "sensor_id": 1, 40 | "timestamp": 1694607005000000, 41 | "measurement": 12.5, 42 | "type": "TEMPERATURE" 43 | } 44 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | com.example 9 | data-store-microservice 10 | 0.0.1-SNAPSHOT 11 | data-store-microservice 12 | data-store-microservice 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 3.2.5 18 | 19 | 20 | 21 | 22 | 17 23 | 1.18.32 24 | 1.5.5.Final 25 | 5.1.2 26 | 3.1.4 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | true 39 | ${lombok.version} 40 | 41 | 42 | 43 | redis.clients 44 | jedis 45 | ${jedis.version} 46 | 47 | 48 | 49 | org.mapstruct 50 | mapstruct 51 | ${mapstruct.version} 52 | 53 | 54 | 55 | org.mapstruct 56 | mapstruct-processor 57 | ${mapstruct.version} 58 | 59 | 60 | 61 | org.springframework.kafka 62 | spring-kafka 63 | ${spring-kafka.version} 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-maven-plugin 72 | 73 | 74 | 75 | org.projectlombok 76 | lombok 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/DataStoreApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DataStoreApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DataStoreApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/config/KeyHelper.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.config; 2 | 3 | import java.util.Objects; 4 | 5 | public class KeyHelper { 6 | 7 | final private static String defaultPrefix = "app"; 8 | 9 | private static String prefix = null; 10 | 11 | public static void setPrefix(String keyPrefix) { 12 | prefix = keyPrefix; 13 | } 14 | 15 | public static String getKey(String key) { 16 | return getPrefix() + ":" + key; 17 | } 18 | 19 | public static String getPrefix() { 20 | return Objects.requireNonNullElse(prefix, defaultPrefix); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import redis.clients.jedis.JedisPool; 7 | import redis.clients.jedis.JedisPoolConfig; 8 | 9 | @Configuration 10 | public class RedisConfig { 11 | 12 | @Value("${spring.data.redis.host}") 13 | private String host; 14 | 15 | @Value("${spring.data.redis.port}") 16 | private int port; 17 | 18 | @Bean 19 | public JedisPool jedisPool() { 20 | JedisPoolConfig config = new JedisPoolConfig(); 21 | config.setJmxEnabled(false); 22 | return new JedisPool(config, host, port); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/config/RedisSchema.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.config; 2 | 3 | import com.example.datastore.model.MeasurementType; 4 | 5 | public class RedisSchema { 6 | 7 | //set 8 | public static String sensorKeys() { 9 | return KeyHelper.getKey("sensors"); 10 | } 11 | 12 | //hash with summary types 13 | public static String summaryKey( 14 | long sensorId, 15 | MeasurementType measurementType 16 | ) { 17 | return KeyHelper.getKey("sensors:" + sensorId + ":" + measurementType.name().toLowerCase()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/model/Data.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.model; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | @NoArgsConstructor 11 | @Getter 12 | @Setter 13 | @ToString 14 | public class Data { 15 | 16 | private Long id; 17 | private Long sensorId; 18 | private LocalDateTime timestamp; 19 | private double measurement; 20 | private MeasurementType measurementType; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/model/MeasurementType.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.model; 2 | 3 | public enum MeasurementType { 4 | 5 | TEMPERATURE, 6 | VOLTAGE, 7 | POWER 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/model/Summary.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.model; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | @Getter 14 | @Setter 15 | @ToString 16 | public class Summary { 17 | 18 | private long sensorId; 19 | private Map> values; 20 | 21 | @NoArgsConstructor 22 | @Getter 23 | @Setter 24 | @ToString 25 | public static class SummaryEntry { 26 | 27 | private SummaryType type; 28 | private double value; 29 | private long counter; 30 | 31 | } 32 | 33 | public Summary() { 34 | this.values = new HashMap<>(); 35 | } 36 | 37 | public void addValue(MeasurementType type, SummaryEntry value) { 38 | if (values.containsKey(type)) { 39 | List entries = new ArrayList<>(values.get(type)); 40 | entries.add(value); 41 | values.put(type, entries); 42 | } else { 43 | values.put(type, List.of(value)); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/model/SummaryType.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.model; 2 | 3 | public enum SummaryType { 4 | 5 | MIN, 6 | MAX, 7 | AVG, 8 | SUM 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/model/exception/SensorNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.model.exception; 2 | 3 | public class SensorNotFoundException extends RuntimeException { 4 | 5 | public SensorNotFoundException() { 6 | super(); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/repository/SummaryRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.repository; 2 | 3 | import com.example.datastore.model.Data; 4 | import com.example.datastore.model.MeasurementType; 5 | import com.example.datastore.model.Summary; 6 | import com.example.datastore.model.SummaryType; 7 | 8 | import java.util.Optional; 9 | import java.util.Set; 10 | 11 | public interface SummaryRepository { 12 | 13 | Optional findBySensorId( 14 | long sensorId, 15 | Set measurementTypes, 16 | Set summaryTypes 17 | ); 18 | 19 | void handle( 20 | Data data 21 | ); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/repository/SummaryRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.repository; 2 | 3 | import com.example.datastore.config.RedisSchema; 4 | import com.example.datastore.model.Data; 5 | import com.example.datastore.model.MeasurementType; 6 | import com.example.datastore.model.Summary; 7 | import com.example.datastore.model.SummaryType; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Repository; 10 | import redis.clients.jedis.Jedis; 11 | import redis.clients.jedis.JedisPool; 12 | 13 | import java.util.Optional; 14 | import java.util.Set; 15 | 16 | @Repository 17 | @RequiredArgsConstructor 18 | public class SummaryRepositoryImpl implements SummaryRepository { 19 | 20 | private final JedisPool jedisPool; 21 | 22 | @Override 23 | public Optional findBySensorId( 24 | long sensorId, 25 | Set measurementTypes, 26 | Set summaryTypes 27 | ) { 28 | try (Jedis jedis = jedisPool.getResource()) { 29 | if (!jedis.sismember( 30 | RedisSchema.sensorKeys(), 31 | String.valueOf(sensorId) 32 | )) { 33 | return Optional.empty(); 34 | } 35 | if (measurementTypes.isEmpty() && !summaryTypes.isEmpty()) { 36 | return getSummary( 37 | sensorId, 38 | Set.of(MeasurementType.values()), 39 | summaryTypes, 40 | jedis 41 | ); 42 | } else if (!measurementTypes.isEmpty() && summaryTypes.isEmpty()) { 43 | return getSummary( 44 | sensorId, 45 | measurementTypes, 46 | Set.of(SummaryType.values()), 47 | jedis 48 | ); 49 | } else { 50 | return getSummary( 51 | sensorId, 52 | measurementTypes, 53 | summaryTypes, 54 | jedis 55 | ); 56 | } 57 | } 58 | } 59 | 60 | private Optional getSummary( 61 | long sensorId, 62 | Set measurementTypes, 63 | Set summaryTypes, 64 | Jedis jedis 65 | ) { 66 | Summary summary = new Summary(); 67 | summary.setSensorId(sensorId); 68 | for (MeasurementType mType : measurementTypes) { 69 | for (SummaryType sType : summaryTypes) { 70 | Summary.SummaryEntry entry = new Summary.SummaryEntry(); 71 | entry.setType(sType); 72 | String value = jedis.hget( 73 | RedisSchema.summaryKey(sensorId, mType), 74 | sType.name().toLowerCase() 75 | ); 76 | if (value != null) { 77 | entry.setValue(Double.parseDouble(value)); 78 | } 79 | String counter = jedis.hget( 80 | RedisSchema.summaryKey(sensorId, mType), 81 | "counter" 82 | ); 83 | if (counter != null) { 84 | entry.setCounter(Long.parseLong(counter)); 85 | } 86 | summary.addValue(mType, entry); 87 | } 88 | } 89 | return Optional.of(summary); 90 | } 91 | 92 | @Override 93 | public void handle( 94 | Data data 95 | ) { 96 | try (Jedis jedis = jedisPool.getResource()) { 97 | if (!jedis.sismember( 98 | RedisSchema.sensorKeys(), 99 | String.valueOf(data.getSensorId()) 100 | )) { 101 | jedis.sadd( 102 | RedisSchema.sensorKeys(), 103 | String.valueOf(data.getSensorId()) 104 | ); 105 | } 106 | updateMinValue(data, jedis); 107 | updateMaxValue(data, jedis); 108 | updateSumAndAvgValue(data, jedis); 109 | } 110 | } 111 | 112 | private void updateMinValue( 113 | Data data, 114 | Jedis jedis 115 | ) { 116 | String key = RedisSchema.summaryKey( 117 | data.getSensorId(), 118 | data.getMeasurementType() 119 | ); 120 | String value = jedis.hget( 121 | key, 122 | SummaryType.MIN.name().toLowerCase() 123 | ); 124 | if (value == null || data.getMeasurement() < Double.parseDouble(value)) { 125 | jedis.hset( 126 | key, 127 | SummaryType.MIN.name().toLowerCase(), 128 | String.valueOf(data.getMeasurement()) 129 | ); 130 | } 131 | } 132 | 133 | private void updateMaxValue( 134 | Data data, 135 | Jedis jedis 136 | ) { 137 | String key = RedisSchema.summaryKey( 138 | data.getSensorId(), 139 | data.getMeasurementType() 140 | ); 141 | String value = jedis.hget( 142 | key, 143 | SummaryType.MAX.name().toLowerCase() 144 | ); 145 | if (value == null || data.getMeasurement() > Double.parseDouble(value)) { 146 | jedis.hset( 147 | key, 148 | SummaryType.MAX.name().toLowerCase(), 149 | String.valueOf(data.getMeasurement()) 150 | ); 151 | } 152 | } 153 | 154 | private void updateSumAndAvgValue( 155 | Data data, 156 | Jedis jedis 157 | ) { 158 | updateSumValue(data, jedis); 159 | String key = RedisSchema.summaryKey( 160 | data.getSensorId(), 161 | data.getMeasurementType() 162 | ); 163 | String counter = jedis.hget( 164 | key, 165 | "counter" 166 | ); 167 | if (counter == null) { 168 | counter = String.valueOf( 169 | jedis.hset( 170 | key, 171 | "counter", 172 | String.valueOf(1) 173 | ) 174 | ); 175 | } else { 176 | counter = String.valueOf( 177 | jedis.hincrBy( 178 | key, 179 | "counter", 180 | 1 181 | ) 182 | ); 183 | } 184 | String sum = jedis.hget( 185 | key, 186 | SummaryType.SUM.name().toLowerCase() 187 | ); 188 | jedis.hset( 189 | key, 190 | SummaryType.AVG.name().toLowerCase(), 191 | String.valueOf( 192 | Double.parseDouble(sum) / Double.parseDouble(counter) 193 | ) 194 | ); 195 | } 196 | 197 | private void updateSumValue( 198 | Data data, 199 | Jedis jedis 200 | ) { 201 | String key = RedisSchema.summaryKey( 202 | data.getSensorId(), 203 | data.getMeasurementType() 204 | ); 205 | String value = jedis.hget( 206 | key, 207 | SummaryType.SUM.name().toLowerCase() 208 | ); 209 | if (value == null) { 210 | jedis.hset( 211 | key, 212 | SummaryType.SUM.name().toLowerCase(), 213 | String.valueOf(data.getMeasurement()) 214 | ); 215 | } else { 216 | jedis.hincrByFloat( 217 | key, 218 | SummaryType.SUM.name().toLowerCase(), 219 | data.getMeasurement() 220 | ); 221 | } 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/service/CDCEventConsumer.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.service; 2 | 3 | public interface CDCEventConsumer { 4 | 5 | void handle(String message); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/service/DebeziumEventConsumerImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.service; 2 | 3 | import com.example.datastore.model.Data; 4 | import com.example.datastore.model.MeasurementType; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonParser; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.kafka.annotation.KafkaListener; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.time.Instant; 12 | import java.time.LocalDateTime; 13 | import java.util.TimeZone; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | public class DebeziumEventConsumerImpl implements CDCEventConsumer { 18 | 19 | private final SummaryService summaryService; 20 | 21 | @KafkaListener(topics = "data") 22 | public void handle(String message) { 23 | try { 24 | JsonObject payload = JsonParser.parseString(message) 25 | .getAsJsonObject() 26 | .get("payload") 27 | .getAsJsonObject(); 28 | Data data = new Data(); 29 | data.setId( 30 | payload.get("id") 31 | .getAsLong() 32 | ); 33 | data.setSensorId( 34 | payload.get("sensor_id") 35 | .getAsLong() 36 | ); 37 | data.setMeasurement( 38 | payload.get("measurement") 39 | .getAsDouble() 40 | ); 41 | data.setMeasurementType( 42 | MeasurementType.valueOf( 43 | payload.get("type") 44 | .getAsString() 45 | ) 46 | ); 47 | data.setTimestamp( 48 | LocalDateTime.ofInstant( 49 | Instant.ofEpochMilli( 50 | payload.get("timestamp") 51 | .getAsLong() / 1000 52 | ), 53 | TimeZone.getDefault() 54 | .toZoneId() 55 | ) 56 | ); 57 | summaryService.handle(data); 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/service/SummaryService.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.service; 2 | 3 | import com.example.datastore.model.Data; 4 | import com.example.datastore.model.MeasurementType; 5 | import com.example.datastore.model.Summary; 6 | import com.example.datastore.model.SummaryType; 7 | 8 | import java.util.Set; 9 | 10 | public interface SummaryService { 11 | 12 | Summary get( 13 | Long sensorId, 14 | Set measurementTypes, 15 | Set summaryTypes 16 | ); 17 | 18 | void handle( 19 | Data data 20 | ); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/service/SummaryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.service; 2 | 3 | import com.example.datastore.model.Data; 4 | import com.example.datastore.model.MeasurementType; 5 | import com.example.datastore.model.Summary; 6 | import com.example.datastore.model.SummaryType; 7 | import com.example.datastore.model.exception.SensorNotFoundException; 8 | import com.example.datastore.repository.SummaryRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Set; 13 | 14 | @Service 15 | @RequiredArgsConstructor 16 | public class SummaryServiceImpl implements SummaryService { 17 | 18 | private final SummaryRepository summaryRepository; 19 | 20 | @Override 21 | public Summary get( 22 | Long sensorId, 23 | Set measurementTypes, 24 | Set summaryTypes 25 | ) { 26 | return summaryRepository.findBySensorId( 27 | sensorId, 28 | measurementTypes == null ? Set.of(MeasurementType.values()) : measurementTypes, 29 | summaryTypes == null ? Set.of(SummaryType.values()) : summaryTypes 30 | ) 31 | .orElseThrow(SensorNotFoundException::new); 32 | } 33 | 34 | @Override 35 | public void handle( 36 | Data data 37 | ) { 38 | summaryRepository.handle(data); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/web/controller/AnalyticsController.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.web.controller; 2 | 3 | import com.example.datastore.model.MeasurementType; 4 | import com.example.datastore.model.Summary; 5 | import com.example.datastore.model.SummaryType; 6 | import com.example.datastore.service.SummaryService; 7 | import com.example.datastore.web.dto.SummaryDto; 8 | import com.example.datastore.web.mapper.SummaryMapper; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import java.util.Set; 17 | 18 | @RestController 19 | @RequestMapping("/api/v1/analytics") 20 | @RequiredArgsConstructor 21 | public class AnalyticsController { 22 | 23 | private final SummaryService summaryService; 24 | 25 | private final SummaryMapper summaryMapper; 26 | 27 | @GetMapping("/summary/{sensorId}") 28 | public SummaryDto getSummary( 29 | @PathVariable long sensorId, 30 | @RequestParam(value = "mt", required = false) 31 | Set measurementTypes, 32 | @RequestParam(value = "st", required = false) 33 | Set summaryTypes 34 | ) { 35 | Summary summary = summaryService.get( 36 | sensorId, 37 | measurementTypes, 38 | summaryTypes 39 | ); 40 | return summaryMapper.toDto(summary); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/web/controller/ControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.web.controller; 2 | 3 | import com.example.datastore.model.exception.SensorNotFoundException; 4 | import org.springframework.web.bind.annotation.ExceptionHandler; 5 | import org.springframework.web.bind.annotation.RestControllerAdvice; 6 | 7 | @RestControllerAdvice 8 | public class ControllerAdvice { 9 | 10 | @ExceptionHandler(SensorNotFoundException.class) 11 | public String sensorNotFound(SensorNotFoundException e) { 12 | return "Sensor not found."; 13 | } 14 | 15 | @ExceptionHandler 16 | public String server(Exception e) { 17 | e.printStackTrace(); 18 | return "Something happened."; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/web/dto/SummaryDto.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.web.dto; 2 | 3 | import com.example.datastore.model.MeasurementType; 4 | import com.example.datastore.model.Summary; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | @NoArgsConstructor 14 | @Getter 15 | @Setter 16 | @ToString 17 | public class SummaryDto { 18 | 19 | private long sensorId; 20 | private Map> values; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/web/mapper/Mappable.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.web.mapper; 2 | 3 | import java.util.List; 4 | 5 | public interface Mappable { 6 | 7 | E toEntity(D d); 8 | 9 | List toEntity(List d); 10 | 11 | D toDto(E e); 12 | 13 | List toDto(List e); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/datastore/web/mapper/SummaryMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.datastore.web.mapper; 2 | 3 | import com.example.datastore.model.Summary; 4 | import com.example.datastore.web.dto.SummaryDto; 5 | import org.mapstruct.Mapper; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface SummaryMapper extends Mappable { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | config: 3 | import: optional:file:.env[.properties] 4 | data: 5 | redis: 6 | host: ${REDIS_HOST} 7 | port: ${REDIS_PORT} 8 | kafka: 9 | consumer: 10 | bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS} 11 | group-id: ${KAFKA_BROKER_ID} 12 | auto-offset-reset: earliest 13 | key-deserializer: org.apache.kafka.common.serialization.StringDeserializer 14 | value-deserializer: org.apache.kafka.common.serialization.StringDeserializer 15 | server: 16 | port: 8083 17 | --------------------------------------------------------------------------------