Mqtt, Topic, TopicQuery, Series, Bool, Delta, Varying, Websocket
This commit is contained in:
parent
d73c1acbe4
commit
41a645227c
@ -1 +1,11 @@
|
||||
logging.level.de.ph87=DEBUG
|
||||
logging.level.de.ph87=INFO
|
||||
logging.level.de.ph87.data.mqtt=DEBUG
|
||||
#-
|
||||
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
#spring.jpa.hibernate.ddl-auto=create
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
#-
|
||||
spring.jackson.serialization.indent_output=true
|
||||
#-
|
||||
de.ph87.knx.mqtt.uri=tcp://10.0.0.50:1883
|
||||
|
||||
9
pom.xml
9
pom.xml
@ -38,10 +38,6 @@
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
@ -58,6 +54,11 @@
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
129
src/main/java/de/ph87/data/DemoService.java
Normal file
129
src/main/java/de/ph87/data/DemoService.java
Normal file
@ -0,0 +1,129 @@
|
||||
package de.ph87.data;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.SeriesRepository;
|
||||
import de.ph87.data.series.SeriesType;
|
||||
import de.ph87.data.topic.Topic;
|
||||
import de.ph87.data.topic.TopicRepository;
|
||||
import de.ph87.data.topic.query.TopicQuery;
|
||||
import de.ph87.data.topic.query.TopicQueryFunction;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DemoService {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
private final TopicRepository topicRepository;
|
||||
|
||||
@Transactional
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void init() {
|
||||
final Series electricityEnergyProduce = series("electricity/energy/produce", "kWh", SeriesType.DELTA, 5);
|
||||
final Series electricityPowerProduce = series("electricity/power/produce", "W", SeriesType.VARYING, 5);
|
||||
topic(
|
||||
"openDTU/pv/patrix/json2",
|
||||
new TopicQuery(electricityEnergyProduce, "$.totalKWh"),
|
||||
new TopicQuery(electricityPowerProduce, "$.totalW")
|
||||
);
|
||||
|
||||
final Series electricityEnergyPurchase = series("electricity/energy/purchase", "kWh", SeriesType.DELTA, 5);
|
||||
final Series electricityPowerPurchase = series("electricity/power/purchase", "W", SeriesType.VARYING, 5);
|
||||
final Series electricityEnergyDelivery = series("electricity/energy/delivery", "kWh", SeriesType.DELTA, 5);
|
||||
final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5);
|
||||
topic(
|
||||
"electricity/grid/json",
|
||||
new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001),
|
||||
new TopicQuery(electricityPowerPurchase, "$.powerW", TopicQueryFunction.ONLY_POSITIVE),
|
||||
new TopicQuery(electricityEnergyDelivery, "$.deliveryWh", 0.001),
|
||||
new TopicQuery(electricityPowerDelivery, "$.powerW", TopicQueryFunction.ONLY_NEGATIVE_BUT_NEGATE)
|
||||
);
|
||||
|
||||
final Series gardenPressure = series("garden/pressure", "hPa", SeriesType.VARYING, 5);
|
||||
final Series gardenTemperature = series("garden/temperature", "°C", SeriesType.VARYING, 5);
|
||||
final Series gardenHumidityAbsolute = series("garden/humidity/absolute", "mg/L", SeriesType.VARYING, 5);
|
||||
final Series gardenHumidityRelative = series("garden/humidity/relative", "%", SeriesType.VARYING, 5);
|
||||
topic("garten/sensor/pressure", new TopicQuery(gardenPressure, "$.value"));
|
||||
topic("garten/sensor/temperature", new TopicQuery(gardenTemperature, "$.value"));
|
||||
topic("garten/sensor/humidity_absolute", new TopicQuery(gardenHumidityAbsolute, "$.value"));
|
||||
topic("garten/sensor/humidity_relative", new TopicQuery(gardenHumidityRelative, "$.value"));
|
||||
|
||||
final Series bedroomPressure = series("bedroom/pressure", "hPa", SeriesType.VARYING, 5);
|
||||
final Series bedroomTemperature = series("bedroom/temperature", "°C", SeriesType.VARYING, 5);
|
||||
final Series bedroomHumidityAbsolute = series("bedroom/humidity/absolute", "mg/L", SeriesType.VARYING, 5);
|
||||
final Series bedroomHumidityRelative = series("bedroom/humidity/relative", "%", SeriesType.VARYING, 5);
|
||||
topic("schlafzimmer/sensor/pressure", new TopicQuery(bedroomPressure, "$.value"));
|
||||
topic("schlafzimmer/sensor/temperature", new TopicQuery(bedroomTemperature, "$.value"));
|
||||
topic("schlafzimmer/sensor/humidity_absolute", new TopicQuery(bedroomHumidityAbsolute, "$.value"));
|
||||
topic("schlafzimmer/sensor/humidity_relative", new TopicQuery(bedroomHumidityRelative, "$.value"));
|
||||
|
||||
final Series basementTemperature = series("basement/temperature", "°C", SeriesType.VARYING, 60);
|
||||
final Series basementHumidityAbsolute = series("basement/humidity/absolute", "mg/L", SeriesType.VARYING, 60);
|
||||
final Series basementHumidityRelative = series("basement/humidity/relative", "%", SeriesType.VARYING, 60);
|
||||
topic("aggregation/heizraum/luftfeuchte/absolut", "$.lastTime", new TopicQuery(basementTemperature, "$.lastValue"));
|
||||
topic("aggregation/heizraum/luftfeuchte/relativ", "$.lastTime", new TopicQuery(basementHumidityAbsolute, "$.lastValue"));
|
||||
topic("aggregation/heizraum/temperatur", "$.lastTime", new TopicQuery(basementHumidityRelative, "$.lastValue"));
|
||||
|
||||
final Series heatingExhaustTemperature = series("heating/exhaust/temperature", "°C", SeriesType.VARYING, 60);
|
||||
topic("aggregation/heizung/abgas/temperatur", "$.lastTime", new TopicQuery(heatingExhaustTemperature, "$.lastValue"));
|
||||
|
||||
final Series heatingCircuitReturnTemperature = series("heating/circuit/return/temperature", "°C", SeriesType.VARYING, 60);
|
||||
final Series heatingCircuitSupplyTemperature = series("heating/circuit/supply/temperature", "°C", SeriesType.VARYING, 60);
|
||||
topic("aggregation/heizung/heizkreis/ruecklauf/temperatur", "$.lastTime", new TopicQuery(heatingCircuitReturnTemperature, "$.lastValue"));
|
||||
topic("aggregation/heizung/heizkreis/vorlauf/temperatur", "$.lastTime", new TopicQuery(heatingCircuitSupplyTemperature, "$.lastValue"));
|
||||
|
||||
final Series heatingBufferInletTemperature = series("heating/buffer/inlet/temperature", "°C", SeriesType.VARYING, 60);
|
||||
final Series heatingBufferOutletTemperature = series("heating/buffer/outlet/temperature", "°C", SeriesType.VARYING, 60);
|
||||
final Series heatingBufferCirculationTemperature = series("heating/buffer/circulation/temperature", "°C", SeriesType.VARYING, 60);
|
||||
topic("aggregation/heizung/puffer/eingang/temperatur", "$.lastTime", new TopicQuery(heatingBufferInletTemperature, "$.lastValue"));
|
||||
topic("aggregation/heizung/puffer/ausgang/temperatur", "$.lastTime", new TopicQuery(heatingBufferOutletTemperature, "$.lastValue"));
|
||||
topic("aggregation/heizung/puffer/zirkulation/temperatur", "$.lastTime", new TopicQuery(heatingBufferCirculationTemperature, "$.lastValue"));
|
||||
|
||||
final Series heatingBufferInsideTemperature = series("heating/buffer/inside/temperature", "°C", SeriesType.VARYING, 60);
|
||||
topic("aggregation/heizung/puffer/speicher/temperatur", "$.lastTime", new TopicQuery(heatingBufferInsideTemperature, "$.lastValue"));
|
||||
|
||||
final Series heatingBufferSupplyTemperature = series("heating/buffer/supply/temperature", "°C", SeriesType.VARYING, 60);
|
||||
final Series heatingBufferReturnTemperature = series("heating/buffer/return/temperature", "°C", SeriesType.VARYING, 60);
|
||||
topic("aggregation/heizung/puffer/vorlauf/temperatur", "$.lastTime", new TopicQuery(heatingBufferSupplyTemperature, "$.lastValue"));
|
||||
topic("aggregation/heizung/puffer/ruecklauf/temperatur", "$.lastTime", new TopicQuery(heatingBufferReturnTemperature, "$.lastValue"));
|
||||
|
||||
final Series cisternVolume = series("cistern/volume", "L", SeriesType.VARYING, 5);
|
||||
topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value"));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int expectedEverySeconds) {
|
||||
return seriesRepository
|
||||
.findByName(name)
|
||||
.stream()
|
||||
.peek(existing -> {
|
||||
existing.setUnit(unit);
|
||||
existing.setType(type);
|
||||
existing.setExpectedEverySeconds(expectedEverySeconds);
|
||||
})
|
||||
.findFirst()
|
||||
.orElseGet(() -> seriesRepository.save(new Series(name, unit, 1, expectedEverySeconds, type)));
|
||||
}
|
||||
|
||||
private void topic(@NonNull final String name, @NonNull final TopicQuery... queries) {
|
||||
topic(name, "$.timestamp", queries);
|
||||
}
|
||||
|
||||
private void topic(@NonNull final String name, @NonNull final String timestampQuery, @NonNull final TopicQuery... queries) {
|
||||
final Topic topic = topicRepository.findByName(name).orElseGet(() -> topicRepository.save(new Topic(name)));
|
||||
topic.setTimestampQuery(timestampQuery);
|
||||
topic.getQueries().clear();
|
||||
topic.getQueries().addAll(List.of(queries));
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package de.ph87.data.user;
|
||||
package de.ph87.data;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.NonNull;
|
||||
@ -8,11 +8,11 @@ import java.util.function.Function;
|
||||
public class Helpers {
|
||||
|
||||
@Nullable
|
||||
public static <T, R> R map(@Nullable T t, @NonNull final Function<T, R> map) {
|
||||
public static <T, R> R map(@Nullable final T t, @NonNull final Function<T, R> mapper) {
|
||||
if (t == null) {
|
||||
return null;
|
||||
}
|
||||
return map.apply(t);
|
||||
return mapper.apply(t);
|
||||
}
|
||||
|
||||
}
|
||||
51
src/main/java/de/ph87/data/log/AbstractEntityLog.java
Normal file
51
src/main/java/de/ph87/data/log/AbstractEntityLog.java
Normal file
@ -0,0 +1,51 @@
|
||||
package de.ph87.data.log;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.OrderColumn;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@MappedSuperclass
|
||||
@NoArgsConstructor
|
||||
public abstract class AbstractEntityLog {
|
||||
|
||||
@NonNull
|
||||
@OrderColumn
|
||||
@ToString.Exclude
|
||||
@ElementCollection
|
||||
private List<LogMessage> log = new ArrayList<>();
|
||||
|
||||
public void error(@NonNull final Logger logger, @NonNull final String message) {
|
||||
error(logger, message, null);
|
||||
}
|
||||
|
||||
public void error(@NonNull final Logger logger, @NonNull final String message, @Nullable final Exception e) {
|
||||
if (e instanceof RuntimeException) {
|
||||
this.log.add(new LogMessage(LogSeverity.ERROR, message + "\n stacktrace:\n" + stacktraceToString(e)));
|
||||
logger.error(message, e);
|
||||
} else {
|
||||
this.log.add(new LogMessage(LogSeverity.ERROR, message));
|
||||
logger.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String stacktraceToString(@NonNull final Exception e) {
|
||||
final StringWriter sw = new StringWriter();
|
||||
e.printStackTrace(new PrintWriter(sw));
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/java/de/ph87/data/log/LogMessage.java
Normal file
38
src/main/java/de/ph87/data/log/LogMessage.java
Normal file
@ -0,0 +1,38 @@
|
||||
package de.ph87.data.log;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Embeddable
|
||||
@NoArgsConstructor
|
||||
public class LogMessage {
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private ZonedDateTime date;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private LogSeverity severity;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String message;
|
||||
|
||||
public LogMessage(@NonNull final LogSeverity severity, @NonNull final String message) {
|
||||
this.severity = severity;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
}
|
||||
5
src/main/java/de/ph87/data/log/LogSeverity.java
Normal file
5
src/main/java/de/ph87/data/log/LogSeverity.java
Normal file
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.log;
|
||||
|
||||
public enum LogSeverity {
|
||||
ERROR, WARN, INFO, DEBUG
|
||||
}
|
||||
25
src/main/java/de/ph87/data/mqtt/MqttInbound.java
Normal file
25
src/main/java/de/ph87/data/mqtt/MqttInbound.java
Normal file
@ -0,0 +1,25 @@
|
||||
package de.ph87.data.mqtt;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
public class MqttInbound {
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime date = ZonedDateTime.now();
|
||||
|
||||
@NonNull
|
||||
public final String topic;
|
||||
|
||||
@NonNull
|
||||
public final String payload;
|
||||
|
||||
public MqttInbound(@NonNull final String topic, @NonNull final String payload) {
|
||||
this.topic = topic;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
}
|
||||
89
src/main/java/de/ph87/data/mqtt/MqttService.java
Normal file
89
src/main/java/de/ph87/data/mqtt/MqttService.java
Normal file
@ -0,0 +1,89 @@
|
||||
package de.ph87.data.mqtt;
|
||||
|
||||
import de.ph87.data.topic.TopicReceiver;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MqttService {
|
||||
|
||||
private final TopicReceiver topicReceiver;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
private boolean stop = false;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
new Thread(this::stayConnected).start();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
synchronized (lock) {
|
||||
stop = true;
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void stayConnected() {
|
||||
try {
|
||||
while (!stop) {
|
||||
connectOnce();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Interrupted while waiting for connection", e);
|
||||
} finally {
|
||||
log.info("MQTT client stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
private void connectOnce() throws InterruptedException {
|
||||
MqttClient client = null;
|
||||
try {
|
||||
log.info("MQTT connecting...");
|
||||
client = new MqttClient("tcp://10.0.0.50:1883", "DataDynamic", new MemoryPersistence());
|
||||
final MqttConnectOptions options = new MqttConnectOptions();
|
||||
options.setAutomaticReconnect(false);
|
||||
options.setCleanSession(true);
|
||||
options.setConnectionTimeout(5);
|
||||
options.setKeepAliveInterval(2);
|
||||
client.connect(options);
|
||||
client.subscribe("#", (topic, message) -> topicReceiver.receive(new MqttInbound(topic, new String(message.getPayload()))));
|
||||
log.info("MQTT connected.");
|
||||
synchronized (lock) {
|
||||
while (!stop && client.isConnected()) {
|
||||
lock.wait(1000);
|
||||
}
|
||||
if (!client.isConnected()) {
|
||||
log.info("MQTT disconnected.");
|
||||
}
|
||||
}
|
||||
} catch (MqttException e) {
|
||||
log.error("MQTT connection error: {}", e.getMessage());
|
||||
synchronized (lock) {
|
||||
lock.wait(3000);
|
||||
}
|
||||
} finally {
|
||||
if (client != null && client.isConnected()) {
|
||||
log.info("MQTT disconnecting...");
|
||||
try {
|
||||
client.disconnect();
|
||||
log.info("MQTT disconnected");
|
||||
} catch (MqttException e) {
|
||||
log.error("Failed to cleanup connection: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
84
src/main/java/de/ph87/data/series/Series.java
Normal file
84
src/main/java/de/ph87/data/series/Series.java
Normal file
@ -0,0 +1,84 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import de.ph87.data.log.AbstractEntityLog;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class Series extends AbstractEntityLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String unit;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private int decimals;
|
||||
|
||||
@Column
|
||||
@Nullable
|
||||
private ZonedDateTime first = null;
|
||||
|
||||
@Column
|
||||
@Nullable
|
||||
private ZonedDateTime last = null;
|
||||
|
||||
@Nullable
|
||||
@Column(name = "`value`")
|
||||
private Double value = null;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private int expectedEverySeconds = 5;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private SeriesType type;
|
||||
|
||||
public Series(@NonNull final String name, @NonNull final String unit, final int decimals, final int expectedEverySeconds, @NonNull final SeriesType type) {
|
||||
this.name = name;
|
||||
this.unit = unit;
|
||||
this.decimals = decimals;
|
||||
this.expectedEverySeconds = expectedEverySeconds;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void update(@NonNull final ZonedDateTime timestamp, final double value) {
|
||||
if (this.last == null || this.last.isBefore(timestamp)) {
|
||||
this.last = timestamp;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
src/main/java/de/ph87/data/series/SeriesController.java
Normal file
39
src/main/java/de/ph87/data/series/SeriesController.java
Normal file
@ -0,0 +1,39 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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 java.util.List;
|
||||
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("Series")
|
||||
public class SeriesController {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
|
||||
@GetMapping("findAll")
|
||||
public List<SeriesDto> findAll() {
|
||||
return seriesRepository.findAllDto();
|
||||
}
|
||||
|
||||
@PostMapping("findByName")
|
||||
public SeriesDto findByName(@NonNull @RequestBody final String name) {
|
||||
return seriesRepository.findDtoByName(name);
|
||||
}
|
||||
|
||||
@PostMapping("points")
|
||||
public List<? extends SeriesPoint> points(@NonNull @RequestBody final SeriesPointsRequest request) {
|
||||
return seriesService.points(request);
|
||||
}
|
||||
|
||||
}
|
||||
48
src/main/java/de/ph87/data/series/SeriesDto.java
Normal file
48
src/main/java/de/ph87/data/series/SeriesDto.java
Normal file
@ -0,0 +1,48 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import de.ph87.data.websocket.IWebsocketMessage;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
public class SeriesDto implements IWebsocketMessage {
|
||||
|
||||
public final long id;
|
||||
|
||||
public final String name;
|
||||
|
||||
@NonNull
|
||||
public final String unit;
|
||||
|
||||
public final int decimals;
|
||||
|
||||
@Nullable
|
||||
public final ZonedDateTime first;
|
||||
|
||||
@Nullable
|
||||
public final ZonedDateTime last;
|
||||
|
||||
@Nullable
|
||||
public final Double value;
|
||||
|
||||
public final int expectedEverySeconds;
|
||||
|
||||
@NonNull
|
||||
public final SeriesType type;
|
||||
|
||||
public SeriesDto(@NonNull final Series series) {
|
||||
this.id = series.getId();
|
||||
this.name = series.getName();
|
||||
this.unit = series.getUnit();
|
||||
this.decimals = series.getDecimals();
|
||||
this.first = series.getFirst();
|
||||
this.last = series.getLast();
|
||||
this.value = series.getValue();
|
||||
this.expectedEverySeconds = series.getExpectedEverySeconds();
|
||||
this.type = series.getType();
|
||||
}
|
||||
|
||||
}
|
||||
13
src/main/java/de/ph87/data/series/SeriesPoint.java
Normal file
13
src/main/java/de/ph87/data/series/SeriesPoint.java
Normal file
@ -0,0 +1,13 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@JsonSerialize(using = SeriesPointSerializer.class)
|
||||
public interface SeriesPoint {
|
||||
|
||||
void toJson(final JsonGenerator jsonGenerator) throws IOException;
|
||||
|
||||
}
|
||||
18
src/main/java/de/ph87/data/series/SeriesPointSerializer.java
Normal file
18
src/main/java/de/ph87/data/series/SeriesPointSerializer.java
Normal file
@ -0,0 +1,18 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SeriesPointSerializer extends JsonSerializer<SeriesPoint> {
|
||||
|
||||
@Override
|
||||
public void serialize(final SeriesPoint seriesPoint, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException {
|
||||
jsonGenerator.writeStartArray();
|
||||
seriesPoint.toJson(jsonGenerator);
|
||||
jsonGenerator.writeEndArray();
|
||||
}
|
||||
|
||||
}
|
||||
45
src/main/java/de/ph87/data/series/SeriesPointsRequest.java
Normal file
45
src/main/java/de/ph87/data/series/SeriesPointsRequest.java
Normal file
@ -0,0 +1,45 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.ph87.data.series.data.Interval;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
public class SeriesPointsRequest {
|
||||
|
||||
public final long id;
|
||||
|
||||
@NonNull
|
||||
public final Interval interval;
|
||||
|
||||
public final long offset;
|
||||
|
||||
public final long duration;
|
||||
|
||||
@NonNull
|
||||
@JsonIgnore
|
||||
public final ZonedDateTime first;
|
||||
|
||||
@NonNull
|
||||
@JsonIgnore
|
||||
public final ZonedDateTime after;
|
||||
|
||||
public SeriesPointsRequest(
|
||||
@JsonProperty("id") final long id,
|
||||
@JsonProperty("interval") final Interval interval,
|
||||
@JsonProperty("offset") final long offset,
|
||||
@JsonProperty("duration") final long duration
|
||||
) {
|
||||
this.id = id;
|
||||
this.interval = interval;
|
||||
this.offset = offset;
|
||||
this.duration = duration;
|
||||
this.after = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * (offset - 1), interval.unit);
|
||||
this.first = this.after.minus(interval.amount * duration, interval.unit);
|
||||
}
|
||||
|
||||
}
|
||||
21
src/main/java/de/ph87/data/series/SeriesRepository.java
Normal file
21
src/main/java/de/ph87/data/series/SeriesRepository.java
Normal file
@ -0,0 +1,21 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SeriesRepository extends ListCrudRepository<Series, Long> {
|
||||
|
||||
@NonNull
|
||||
Optional<Series> findByName(@NonNull String seriesName);
|
||||
|
||||
@Query("select new de.ph87.data.series.SeriesDto(s) from Series s where s.name = :name")
|
||||
SeriesDto findDtoByName(@NonNull String name);
|
||||
|
||||
@Query("select new de.ph87.data.series.SeriesDto(t) from Series t")
|
||||
List<SeriesDto> findAllDto();
|
||||
|
||||
}
|
||||
38
src/main/java/de/ph87/data/series/SeriesService.java
Normal file
38
src/main/java/de/ph87/data/series/SeriesService.java
Normal file
@ -0,0 +1,38 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import de.ph87.data.series.data.bool.BoolService;
|
||||
import de.ph87.data.series.data.delta.DeltaService;
|
||||
import de.ph87.data.series.data.varying.VaryingService;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SeriesService {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
private final BoolService boolService;
|
||||
|
||||
private final DeltaService deltaService;
|
||||
|
||||
private final VaryingService varyingService;
|
||||
|
||||
@NonNull
|
||||
public List<? extends SeriesPoint> points(@NonNull final SeriesPointsRequest request) {
|
||||
final Series series = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return switch (series.getType()) {
|
||||
case BOOL -> boolService.points(series, request);
|
||||
case DELTA -> deltaService.points(series, request);
|
||||
case VARYING -> varyingService.points(series, request);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
5
src/main/java/de/ph87/data/series/SeriesType.java
Normal file
5
src/main/java/de/ph87/data/series/SeriesType.java
Normal file
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
public enum SeriesType {
|
||||
BOOL, DELTA, VARYING
|
||||
}
|
||||
40
src/main/java/de/ph87/data/series/data/DataId.java
Normal file
40
src/main/java/de/ph87/data/series/data/DataId.java
Normal file
@ -0,0 +1,40 @@
|
||||
package de.ph87.data.series.data;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Embeddable
|
||||
@EqualsAndHashCode
|
||||
@NoArgsConstructor
|
||||
public class DataId {
|
||||
|
||||
@NonNull
|
||||
@ManyToOne(optional = false)
|
||||
private Series series;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private ZonedDateTime date;
|
||||
|
||||
public DataId(@NonNull final Series series, @NonNull final ZonedDateTime date, @NonNull final Interval interval) {
|
||||
this.series = series;
|
||||
this.date = interval.align.apply(date);
|
||||
}
|
||||
|
||||
public DataId(@NonNull final Series series, @NonNull final ZonedDateTime date) {
|
||||
this.series = series;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/java/de/ph87/data/series/data/Interval.java
Normal file
29
src/main/java/de/ph87/data/series/data/Interval.java
Normal file
@ -0,0 +1,29 @@
|
||||
package de.ph87.data.series.data;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
public enum Interval {
|
||||
FIVE(t -> t.truncatedTo(ChronoUnit.MINUTES).minusMinutes(t.getMinute() % 5), ChronoUnit.MINUTES, 5),
|
||||
HOUR(t -> t.truncatedTo(ChronoUnit.HOURS), ChronoUnit.HOURS, 1),
|
||||
DAY(t -> t.truncatedTo(ChronoUnit.DAYS), ChronoUnit.DAYS, 1),
|
||||
WEEK(t -> t.truncatedTo(ChronoUnit.DAYS).minusDays(t.getDayOfWeek().getValue() - 1), ChronoUnit.WEEKS, 1),
|
||||
MONTH(t -> t.truncatedTo(ChronoUnit.DAYS).minusDays(t.getDayOfMonth() - 1), ChronoUnit.MONTHS, 1),
|
||||
YEAR(t -> t.truncatedTo(ChronoUnit.DAYS).minusDays(t.getDayOfYear() - 1), ChronoUnit.YEARS, 1),
|
||||
;
|
||||
|
||||
public final Function<ZonedDateTime, ZonedDateTime> align;
|
||||
|
||||
public final ChronoUnit unit;
|
||||
|
||||
public final int amount;
|
||||
|
||||
Interval(final Function<ZonedDateTime, ZonedDateTime> align, @NonNull final ChronoUnit unit, final int amount) {
|
||||
this.align = align;
|
||||
this.unit = unit;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
48
src/main/java/de/ph87/data/series/data/bool/Bool.java
Normal file
48
src/main/java/de/ph87/data/series/data/bool/Bool.java
Normal file
@ -0,0 +1,48 @@
|
||||
package de.ph87.data.series.data.bool;
|
||||
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class Bool {
|
||||
|
||||
@Id
|
||||
@NonNull
|
||||
private DataId id;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean state;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Column(nullable = false, name = "`end`")
|
||||
private ZonedDateTime end;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private boolean terminated;
|
||||
|
||||
public Bool(@NonNull final DataId id, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) {
|
||||
this.id = id;
|
||||
this.end = end;
|
||||
this.state = state;
|
||||
this.terminated = terminated;
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/java/de/ph87/data/series/data/bool/BoolDto.java
Normal file
34
src/main/java/de/ph87/data/series/data/bool/BoolDto.java
Normal file
@ -0,0 +1,34 @@
|
||||
package de.ph87.data.series.data.bool;
|
||||
|
||||
import de.ph87.data.series.SeriesDto;
|
||||
import de.ph87.data.websocket.IWebsocketMessage;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
public class BoolDto implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final SeriesDto series;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime date;
|
||||
|
||||
public final boolean state;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime end;
|
||||
|
||||
public final boolean terminated;
|
||||
|
||||
public BoolDto(@NonNull final Bool bool) {
|
||||
this.series = new SeriesDto(bool.getId().getSeries());
|
||||
this.date = bool.getId().getDate();
|
||||
this.end = bool.getEnd();
|
||||
this.state = bool.isState();
|
||||
this.terminated = bool.isTerminated();
|
||||
}
|
||||
|
||||
}
|
||||
36
src/main/java/de/ph87/data/series/data/bool/BoolPoint.java
Normal file
36
src/main/java/de/ph87/data/series/data/bool/BoolPoint.java
Normal file
@ -0,0 +1,36 @@
|
||||
package de.ph87.data.series.data.bool;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import de.ph87.data.series.SeriesPoint;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@SuppressWarnings("unused") // used by repository query
|
||||
public class BoolPoint implements SeriesPoint {
|
||||
|
||||
public final ZonedDateTime begin;
|
||||
|
||||
public final ZonedDateTime end;
|
||||
|
||||
public final boolean state;
|
||||
|
||||
public final boolean terminated;
|
||||
|
||||
public BoolPoint(@NonNull final Bool bool) {
|
||||
this.begin = bool.getId().getDate();
|
||||
this.end = bool.getEnd();
|
||||
this.state = bool.isState();
|
||||
this.terminated = bool.isTerminated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(final JsonGenerator jsonGenerator) throws IOException {
|
||||
jsonGenerator.writeNumber(begin.toEpochSecond());
|
||||
jsonGenerator.writeNumber(end.toEpochSecond());
|
||||
jsonGenerator.writeNumber(state ? 1 : 0);
|
||||
jsonGenerator.writeNumber(terminated ? 1 : 0);
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/java/de/ph87/data/series/data/bool/BoolRepo.java
Normal file
18
src/main/java/de/ph87/data/series/data/bool/BoolRepo.java
Normal file
@ -0,0 +1,18 @@
|
||||
package de.ph87.data.series.data.bool;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface BoolRepo extends CrudRepository<Bool, DataId> {
|
||||
|
||||
@NonNull
|
||||
@Query("select new de.ph87.data.series.data.bool.BoolPoint(e) from Bool e where e.id.series = :series and e.end >= :first and e.id.date < :after")
|
||||
List<BoolPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
|
||||
|
||||
}
|
||||
66
src/main/java/de/ph87/data/series/data/bool/BoolService.java
Normal file
66
src/main/java/de/ph87/data/series/data/bool/BoolService.java
Normal file
@ -0,0 +1,66 @@
|
||||
package de.ph87.data.series.data.bool;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.SeriesPointsRequest;
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BoolService {
|
||||
|
||||
private final BoolRepo boolRepo;
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Transactional
|
||||
public void write(@NonNull final Series series, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) {
|
||||
final Bool bool = updateOrCreate(series, begin, end, state, terminated);
|
||||
log.debug("Bool written: {}", bool);
|
||||
applicationEventPublisher.publishEvent(new BoolDto(bool));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Bool updateOrCreate(@NonNull final Series series, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) {
|
||||
final DataId id = new DataId(series, begin);
|
||||
return boolRepo
|
||||
.findById(id)
|
||||
.stream()
|
||||
.peek(
|
||||
existing -> {
|
||||
if (existing.isState() != state) {
|
||||
id.getSeries().error(log, "Differing states: received=(begin=%s, end=%s, state=%s, terminated=%s), existing=%s".formatted(begin, end, state, terminated, existing));
|
||||
return;
|
||||
}
|
||||
if (existing.getEnd().isAfter(end)) {
|
||||
id.getSeries().error(log, "End ran backwards: received=(begin=%s, end=%s, state=%s, terminated=%s), existing=%s".formatted(begin, end, state, terminated, existing));
|
||||
return;
|
||||
}
|
||||
if (existing.isTerminated() && (!terminated || !existing.getEnd().equals(end))) {
|
||||
id.getSeries().error(log, "Already terminated: received=(begin=%s, end=%s, state=%s, terminated=%s), existing=%s".formatted(begin, end, state, terminated, existing));
|
||||
return;
|
||||
}
|
||||
existing.setEnd(end);
|
||||
existing.setTerminated(terminated);
|
||||
})
|
||||
.findFirst()
|
||||
.orElseGet(
|
||||
() -> boolRepo.save(new Bool(id, end, state, terminated))
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<BoolPoint> points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) {
|
||||
return boolRepo.points(series, request.first, request.after);
|
||||
}
|
||||
|
||||
}
|
||||
116
src/main/java/de/ph87/data/series/data/delta/Delta.java
Normal file
116
src/main/java/de/ph87/data/series/data/delta/Delta.java
Normal file
@ -0,0 +1,116 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@MappedSuperclass
|
||||
@NoArgsConstructor
|
||||
public abstract class Delta {
|
||||
|
||||
@Id
|
||||
@NonNull
|
||||
private DataId id;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double first;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private double last;
|
||||
|
||||
protected Delta(@NonNull final DataId id, final double value) {
|
||||
this.id = id;
|
||||
this.first = value;
|
||||
this.last = value;
|
||||
}
|
||||
|
||||
public void update(final double value) {
|
||||
this.last = value;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "DeltaFive")
|
||||
public static class Five extends Delta {
|
||||
|
||||
public Five(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "DeltaHour")
|
||||
public static class Hour extends Delta {
|
||||
|
||||
public Hour(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "DeltaDay")
|
||||
public static class Day extends Delta {
|
||||
|
||||
public Day(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "DeltaWeek")
|
||||
public static class Week extends Delta {
|
||||
|
||||
public Week(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "DeltaMonth")
|
||||
public static class Month extends Delta {
|
||||
|
||||
public Month(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "DeltaYear")
|
||||
public static class Year extends Delta {
|
||||
|
||||
public Year(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
87
src/main/java/de/ph87/data/series/data/delta/DeltaDto.java
Normal file
87
src/main/java/de/ph87/data/series/data/delta/DeltaDto.java
Normal file
@ -0,0 +1,87 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import de.ph87.data.websocket.IWebsocketMessage;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
@Data
|
||||
public abstract class DeltaDto implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final DataId id;
|
||||
|
||||
public final double first;
|
||||
|
||||
@NonNull
|
||||
public final double last;
|
||||
|
||||
protected DeltaDto(@NonNull final Delta delta) {
|
||||
this.id = delta.getId();
|
||||
this.first = delta.getFirst();
|
||||
this.last = delta.getLast();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Five extends DeltaDto {
|
||||
|
||||
public Five(@NonNull final Delta.Five delta) {
|
||||
super(delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Hour extends DeltaDto {
|
||||
|
||||
public Hour(@NonNull final Delta.Hour delta) {
|
||||
super(delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Day extends DeltaDto {
|
||||
|
||||
public Day(@NonNull final Delta.Day delta) {
|
||||
super(delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Week extends DeltaDto {
|
||||
|
||||
public Week(@NonNull final Delta.Week delta) {
|
||||
super(delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Month extends DeltaDto {
|
||||
|
||||
public Month(@NonNull final Delta.Month delta) {
|
||||
super(delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Year extends DeltaDto {
|
||||
|
||||
public Year(@NonNull final Delta.Year delta) {
|
||||
super(delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
Normal file
32
src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
Normal file
@ -0,0 +1,32 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import de.ph87.data.series.SeriesPoint;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@SuppressWarnings("unused") // used by repository query
|
||||
public class DeltaPoint implements SeriesPoint {
|
||||
|
||||
public final ZonedDateTime date;
|
||||
|
||||
public final double first;
|
||||
|
||||
public final double last;
|
||||
|
||||
public DeltaPoint(@NonNull final Delta delta) {
|
||||
this.date = delta.getId().getDate();
|
||||
this.first = delta.getFirst();
|
||||
this.last = delta.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(final JsonGenerator jsonGenerator) throws IOException {
|
||||
jsonGenerator.writeNumber(date.toEpochSecond());
|
||||
jsonGenerator.writeNumber(first);
|
||||
jsonGenerator.writeNumber(last);
|
||||
}
|
||||
|
||||
}
|
||||
20
src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java
Normal file
20
src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java
Normal file
@ -0,0 +1,20 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.NoRepositoryBean;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@NoRepositoryBean
|
||||
public interface DeltaRepo<T extends Delta> extends CrudRepository<T, DataId> {
|
||||
|
||||
@NonNull
|
||||
@Query("select new de.ph87.data.series.data.delta.DeltaPoint(e) from #{#entityName} e where e.id.series = :series and e.id.date >= :first and e.id.date < :after")
|
||||
List<DeltaPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
public interface DeltaRepoDay extends DeltaRepo<Delta.Day> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
public interface DeltaRepoFive extends DeltaRepo<Delta.Five> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
public interface DeltaRepoHour extends DeltaRepo<Delta.Hour> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
public interface DeltaRepoMonth extends DeltaRepo<Delta.Month> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
public interface DeltaRepoWeek extends DeltaRepo<Delta.Week> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
public interface DeltaRepoYear extends DeltaRepo<Delta.Year> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package de.ph87.data.series.data.delta;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.SeriesPointsRequest;
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import de.ph87.data.series.data.Interval;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeltaService {
|
||||
|
||||
private final DeltaRepoFive five;
|
||||
|
||||
private final DeltaRepoHour hour;
|
||||
|
||||
private final DeltaRepoDay day;
|
||||
|
||||
private final DeltaRepoWeek week;
|
||||
|
||||
private final DeltaRepoMonth month;
|
||||
|
||||
private final DeltaRepoYear year;
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Transactional
|
||||
public void write(@NonNull final Series series, @NonNull final ZonedDateTime date, final double value) {
|
||||
write(series, five, Interval.FIVE, date, value, Delta.Five::new, DeltaDto.Five::new);
|
||||
write(series, hour, Interval.HOUR, date, value, Delta.Hour::new, DeltaDto.Hour::new);
|
||||
write(series, day, Interval.DAY, date, value, Delta.Day::new, DeltaDto.Day::new);
|
||||
write(series, week, Interval.WEEK, date, value, Delta.Week::new, DeltaDto.Week::new);
|
||||
write(series, month, Interval.MONTH, date, value, Delta.Month::new, DeltaDto.Month::new);
|
||||
write(series, year, Interval.YEAR, date, value, Delta.Year::new, DeltaDto.Year::new);
|
||||
}
|
||||
|
||||
private <DELTA extends Delta, DTO extends DeltaDto> void write(@NonNull final Series series, @NonNull final DeltaRepo<DELTA> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, DELTA> create, @NonNull final Function<DELTA, DTO> toDto) {
|
||||
final DataId id = new DataId(series, date, interval);
|
||||
final DELTA delta = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
||||
log.debug("Delta written: {}", delta);
|
||||
applicationEventPublisher.publishEvent(toDto.apply(delta));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<DeltaPoint> points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) {
|
||||
return switch (request.interval) {
|
||||
case FIVE -> five.points(series, request.first, request.after);
|
||||
case HOUR -> hour.points(series, request.first, request.after);
|
||||
case DAY -> day.points(series, request.first, request.after);
|
||||
case WEEK -> week.points(series, request.first, request.after);
|
||||
case MONTH -> month.points(series, request.first, request.after);
|
||||
case YEAR -> year.points(series, request.first, request.after);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
126
src/main/java/de/ph87/data/series/data/varying/Varying.java
Normal file
126
src/main/java/de/ph87/data/series/data/varying/Varying.java
Normal file
@ -0,0 +1,126 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@MappedSuperclass
|
||||
@NoArgsConstructor
|
||||
public abstract class Varying {
|
||||
|
||||
@Id
|
||||
@NonNull
|
||||
private DataId id;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double min;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double max;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double avg;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int count;
|
||||
|
||||
protected Varying(@NonNull final DataId id, final double value) {
|
||||
this.id = id;
|
||||
this.min = value;
|
||||
this.max = value;
|
||||
this.avg = value;
|
||||
this.count = 1;
|
||||
}
|
||||
|
||||
public void update(final double value) {
|
||||
this.min = Math.min(this.min, value);
|
||||
this.max = Math.max(this.max, value);
|
||||
this.avg = (this.avg * this.count + value) / (this.count + 1);
|
||||
this.count++;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "VaryingFive")
|
||||
public static class Five extends Varying {
|
||||
|
||||
public Five(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "VaryingHour")
|
||||
public static class Hour extends Varying {
|
||||
|
||||
public Hour(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "VaryingDay")
|
||||
public static class Day extends Varying {
|
||||
|
||||
public Day(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "VaryingWeek")
|
||||
public static class Week extends Varying {
|
||||
|
||||
public Week(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "VaryingMonth")
|
||||
public static class Month extends Varying {
|
||||
|
||||
public Month(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@Entity(name = "VaryingYear")
|
||||
public static class Year extends Varying {
|
||||
|
||||
public Year(@NonNull final DataId id, @NonNull final double value) {
|
||||
super(id, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
import de.ph87.data.series.SeriesDto;
|
||||
import de.ph87.data.websocket.IWebsocketMessage;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
public abstract class VaryingDto implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final SeriesDto seriesDto;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime date;
|
||||
|
||||
public final double min;
|
||||
|
||||
public final double max;
|
||||
|
||||
public final double avg;
|
||||
|
||||
public final int count;
|
||||
|
||||
protected VaryingDto(@NonNull final Varying varying) {
|
||||
this.seriesDto = new SeriesDto(varying.getId().getSeries());
|
||||
this.date = varying.getId().getDate();
|
||||
this.min = varying.getMin();
|
||||
this.max = varying.getMax();
|
||||
this.avg = varying.getAvg();
|
||||
this.count = varying.getCount();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Five extends VaryingDto {
|
||||
|
||||
public Five(@NonNull final Varying.Five varying) {
|
||||
super(varying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Hour extends VaryingDto {
|
||||
|
||||
public Hour(@NonNull final Varying.Hour varying) {
|
||||
super(varying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Day extends VaryingDto {
|
||||
|
||||
public Day(@NonNull final Varying.Day varying) {
|
||||
super(varying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Week extends VaryingDto {
|
||||
|
||||
public Week(@NonNull final Varying.Week varying) {
|
||||
super(varying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Month extends VaryingDto {
|
||||
|
||||
public Month(@NonNull final Varying.Month varying) {
|
||||
super(varying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Year extends VaryingDto {
|
||||
|
||||
public Year(@NonNull final Varying.Year varying) {
|
||||
super(varying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import de.ph87.data.series.SeriesPoint;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@SuppressWarnings("unused") // used by repository query
|
||||
public class VaryingPoint implements SeriesPoint {
|
||||
|
||||
public final ZonedDateTime date;
|
||||
|
||||
public final double min;
|
||||
|
||||
public final double max;
|
||||
|
||||
public final double avg;
|
||||
|
||||
public VaryingPoint(@NonNull final Varying varying) {
|
||||
this.date = varying.getId().getDate();
|
||||
this.min = varying.getMin();
|
||||
this.max = varying.getMax();
|
||||
this.avg = varying.getAvg();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(final JsonGenerator jsonGenerator) throws IOException {
|
||||
jsonGenerator.writeNumber(date.toEpochSecond());
|
||||
jsonGenerator.writeNumber(min);
|
||||
jsonGenerator.writeNumber(max);
|
||||
jsonGenerator.writeNumber(avg);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.NoRepositoryBean;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@NoRepositoryBean
|
||||
public interface VaryingRepo<T extends Varying> extends CrudRepository<T, DataId> {
|
||||
|
||||
@NonNull
|
||||
@Query("select new de.ph87.data.series.data.varying.VaryingPoint(e) from #{#entityName} e where e.id.series = :series and e.id.date >= :first and e.id.date < :after")
|
||||
List<VaryingPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
public interface VaryingRepoDay extends VaryingRepo<Varying.Day> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
public interface VaryingRepoFive extends VaryingRepo<Varying.Five> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
public interface VaryingRepoHour extends VaryingRepo<Varying.Hour> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
public interface VaryingRepoMonth extends VaryingRepo<Varying.Month> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
public interface VaryingRepoWeek extends VaryingRepo<Varying.Week> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
public interface VaryingRepoYear extends VaryingRepo<Varying.Year> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.SeriesPointsRequest;
|
||||
import de.ph87.data.series.data.DataId;
|
||||
import de.ph87.data.series.data.Interval;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VaryingService {
|
||||
|
||||
private final VaryingRepoFive five;
|
||||
|
||||
private final VaryingRepoHour hour;
|
||||
|
||||
private final VaryingRepoDay day;
|
||||
|
||||
private final VaryingRepoWeek week;
|
||||
|
||||
private final VaryingRepoMonth month;
|
||||
|
||||
private final VaryingRepoYear year;
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Transactional
|
||||
public void write(@NonNull final Series series, @NonNull final ZonedDateTime date, final double value) {
|
||||
write(series, five, Interval.FIVE, date, value, Varying.Five::new, VaryingDto.Five::new);
|
||||
write(series, hour, Interval.HOUR, date, value, Varying.Hour::new, VaryingDto.Hour::new);
|
||||
write(series, day, Interval.DAY, date, value, Varying.Day::new, VaryingDto.Day::new);
|
||||
write(series, week, Interval.WEEK, date, value, Varying.Week::new, VaryingDto.Week::new);
|
||||
write(series, month, Interval.MONTH, date, value, Varying.Month::new, VaryingDto.Month::new);
|
||||
write(series, year, Interval.YEAR, date, value, Varying.Year::new, VaryingDto.Year::new);
|
||||
}
|
||||
|
||||
private <VARYING extends Varying, DTO extends VaryingDto> void write(@NonNull final Series series, @NonNull final VaryingRepo<VARYING> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, VARYING> create, @NonNull final Function<VARYING, DTO> toDto) {
|
||||
final DataId id = new DataId(series, date, interval);
|
||||
final VARYING varying = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
||||
log.debug("Varying written: {}", varying);
|
||||
applicationEventPublisher.publishEvent(toDto.apply(varying));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<VaryingPoint> points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) {
|
||||
return switch (request.interval) {
|
||||
case FIVE -> five.points(series, request.first, request.after);
|
||||
case HOUR -> hour.points(series, request.first, request.after);
|
||||
case DAY -> day.points(series, request.first, request.after);
|
||||
case WEEK -> week.points(series, request.first, request.after);
|
||||
case MONTH -> month.points(series, request.first, request.after);
|
||||
case YEAR -> year.points(series, request.first, request.after);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
5
src/main/java/de/ph87/data/topic/TimestampType.java
Normal file
5
src/main/java/de/ph87/data/topic/TimestampType.java
Normal file
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
public enum TimestampType {
|
||||
EPOCH_MILLISECONDS, EPOCH_SECONDS
|
||||
}
|
||||
85
src/main/java/de/ph87/data/topic/Topic.java
Normal file
85
src/main/java/de/ph87/data/topic/Topic.java
Normal file
@ -0,0 +1,85 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import de.ph87.data.log.AbstractEntityLog;
|
||||
import de.ph87.data.topic.query.TopicQuery;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class Topic extends AbstractEntityLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private boolean enabled = true;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private ZonedDateTime first;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private ZonedDateTime last;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int count;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private TimestampType timestampType = TimestampType.EPOCH_SECONDS;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String timestampQuery = "";
|
||||
|
||||
@NonNull
|
||||
@ToString.Exclude
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
private List<TopicQuery> queries = new ArrayList<>();
|
||||
|
||||
public Topic(@NonNull final String name) {
|
||||
this.name = name;
|
||||
this.first = ZonedDateTime.now();
|
||||
this.last = this.first;
|
||||
this.count = 1;
|
||||
}
|
||||
|
||||
public void update() {
|
||||
this.last = ZonedDateTime.now();
|
||||
this.count++;
|
||||
}
|
||||
|
||||
}
|
||||
45
src/main/java/de/ph87/data/topic/TopicController.java
Normal file
45
src/main/java/de/ph87/data/topic/TopicController.java
Normal file
@ -0,0 +1,45 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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 java.util.List;
|
||||
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("Topic")
|
||||
public class TopicController {
|
||||
|
||||
private final TopicService topicService;
|
||||
|
||||
private final TopicRepository topicRepository;
|
||||
|
||||
@GetMapping("findAll")
|
||||
public List<TopicDto> findAll() {
|
||||
return topicRepository.findAllDto();
|
||||
}
|
||||
|
||||
@PostMapping("{id}/setEnabled")
|
||||
public TopicDto setEnabled(@PathVariable final long id, @RequestBody final boolean enabled) {
|
||||
return topicService.setEnabled(id, enabled);
|
||||
}
|
||||
|
||||
@PostMapping("{id}/setTimestampQuery")
|
||||
public TopicDto setTimestampQuery(@PathVariable final long id, @NonNull @RequestBody final String timestampQuery) {
|
||||
return topicService.setTimestampQuery(id, timestampQuery);
|
||||
}
|
||||
|
||||
@PostMapping("{id}/setTimestampType")
|
||||
public TopicDto setTimestampType(@PathVariable final long id, @NonNull @RequestBody final String timestampType) {
|
||||
return topicService.setTimestampType(id, TimestampType.valueOf(timestampType));
|
||||
}
|
||||
|
||||
}
|
||||
50
src/main/java/de/ph87/data/topic/TopicDto.java
Normal file
50
src/main/java/de/ph87/data/topic/TopicDto.java
Normal file
@ -0,0 +1,50 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import de.ph87.data.topic.query.TopicQueryDto;
|
||||
import de.ph87.data.websocket.IWebsocketMessage;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TopicDto implements IWebsocketMessage {
|
||||
|
||||
public final long id;
|
||||
|
||||
@NonNull
|
||||
public final String name;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime first;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime last;
|
||||
|
||||
public final long count;
|
||||
|
||||
public final boolean enabled;
|
||||
|
||||
@NonNull
|
||||
public final TimestampType timestampType;
|
||||
|
||||
@NonNull
|
||||
public final String timestampQuery;
|
||||
|
||||
@NonNull
|
||||
public final List<TopicQueryDto> queries;
|
||||
|
||||
public TopicDto(@NonNull final Topic topic) {
|
||||
this.id = topic.getId();
|
||||
this.name = topic.getName();
|
||||
this.first = topic.getFirst();
|
||||
this.last = topic.getLast();
|
||||
this.count = topic.getCount();
|
||||
this.enabled = topic.isEnabled();
|
||||
this.timestampType = topic.getTimestampType();
|
||||
this.timestampQuery = topic.getTimestampQuery();
|
||||
this.queries = topic.getQueries().stream().map(TopicQueryDto::new).toList();
|
||||
}
|
||||
|
||||
}
|
||||
129
src/main/java/de/ph87/data/topic/TopicReceiver.java
Normal file
129
src/main/java/de/ph87/data/topic/TopicReceiver.java
Normal file
@ -0,0 +1,129 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import de.ph87.data.mqtt.MqttInbound;
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.SeriesDto;
|
||||
import de.ph87.data.series.SeriesType;
|
||||
import de.ph87.data.series.data.bool.BoolService;
|
||||
import de.ph87.data.series.data.delta.DeltaService;
|
||||
import de.ph87.data.series.data.varying.VaryingService;
|
||||
import de.ph87.data.topic.query.TopicQuery;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TopicReceiver {
|
||||
|
||||
private final TopicRepository topicRepository;
|
||||
|
||||
private final BoolService boolService;
|
||||
|
||||
private final DeltaService deltaService;
|
||||
|
||||
private final VaryingService varyingService;
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Transactional
|
||||
public void receive(@NonNull final MqttInbound inbound) {
|
||||
final Topic topic = updateOrCreate(inbound.topic);
|
||||
if (!topic.isEnabled()) {
|
||||
log.debug("Topic is not enabled: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
if (topic.getTimestampQuery().isEmpty()) {
|
||||
log.debug("Topic timestampQuery is not set: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
if (topic.getQueries().isEmpty()) {
|
||||
log.debug("Topic queries not set: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Parsing Topic payload: topic={}", topic);
|
||||
final DocumentContext json;
|
||||
try {
|
||||
json = JsonPath.parse(inbound.payload);
|
||||
} catch (Exception e) {
|
||||
topic.error(log, "Error parsing JSON: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Executing Topic timestampQuery: topic={}", topic);
|
||||
final ZonedDateTime date;
|
||||
try {
|
||||
date = queryTimestamp(json, topic.getTimestampQuery(), topic.getTimestampType());
|
||||
} catch (Exception e) {
|
||||
topic.error(log, "Error executing Topic timestampQuery: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e);
|
||||
return;
|
||||
}
|
||||
|
||||
topic.getQueries().forEach(query -> query(topic, inbound, json, date, query));
|
||||
}
|
||||
|
||||
private void query(@NonNull final Topic topic, @NonNull final MqttInbound inbound, @NonNull final DocumentContext json, @NonNull final ZonedDateTime date, @NonNull final TopicQuery query) {
|
||||
log.debug("Executing TopicQuery: topicQuery={}", query);
|
||||
try {
|
||||
final Series series = query.getSeries();
|
||||
if (series == null) {
|
||||
log.debug("TopicQuery Series not set: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
if (query.getValueQuery().isEmpty()) {
|
||||
log.debug("TopicQuery valueQuery not set: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
if (series.getType() == SeriesType.BOOL) {
|
||||
if (query.getBeginQuery().isEmpty()) {
|
||||
log.debug("TopicQuery beginQuery not set: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
if (query.getTerminatedQuery().isEmpty()) {
|
||||
log.debug("TopicQuery terminatedQuery not set: topic={}", topic);
|
||||
return;
|
||||
}
|
||||
}
|
||||
final double value = query.getFunction().apply(json.read(query.getValueQuery(), Double.class)) * query.getFactor();
|
||||
series.update(date, value);
|
||||
applicationEventPublisher.publishEvent(new SeriesDto(series));
|
||||
|
||||
switch (series.getType()) {
|
||||
case BOOL -> {
|
||||
final ZonedDateTime begin = queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType());
|
||||
final boolean terminated = json.read(query.getTerminatedQuery(), Boolean.class);
|
||||
boolService.write(series, begin, date, value > 0, terminated);
|
||||
}
|
||||
case DELTA -> deltaService.write(series, date, value);
|
||||
case VARYING -> varyingService.write(series, date, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
topic.error(log, "Error executing TopicQuery: %s\n topic=%s\n query=%s\n inbound=%s".formatted(e.toString(), topic, query, inbound), e);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Topic updateOrCreate(@NonNull final String name) {
|
||||
return topicRepository.findByName(name).stream().peek(Topic::update).findFirst().orElseGet(() -> topicRepository.save(new Topic(name)));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static ZonedDateTime queryTimestamp(@NonNull final DocumentContext json, @NonNull final String query, @NonNull final TimestampType type) {
|
||||
return switch (type) {
|
||||
case TimestampType.EPOCH_SECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(json.read(query, Long.class)), ZoneId.systemDefault());
|
||||
case TimestampType.EPOCH_MILLISECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(json.read(query, Long.class)), ZoneId.systemDefault());
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/java/de/ph87/data/topic/TopicRepository.java
Normal file
18
src/main/java/de/ph87/data/topic/TopicRepository.java
Normal file
@ -0,0 +1,18 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TopicRepository extends ListCrudRepository<Topic, Long> {
|
||||
|
||||
@NonNull
|
||||
Optional<Topic> findByName(@NonNull String name);
|
||||
|
||||
@Query("select new de.ph87.data.topic.TopicDto(t) from Topic t")
|
||||
List<TopicDto> findAllDto();
|
||||
|
||||
}
|
||||
46
src/main/java/de/ph87/data/topic/TopicService.java
Normal file
46
src/main/java/de/ph87/data/topic/TopicService.java
Normal file
@ -0,0 +1,46 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TopicService {
|
||||
|
||||
private final TopicRepository topicRepository;
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Transactional
|
||||
public TopicDto setEnabled(final long id, final boolean enabled) {
|
||||
return set(id, t -> t.setEnabled(enabled));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TopicDto setTimestampQuery(final long id, @NonNull final String timestampQuery) {
|
||||
return set(id, t -> t.setTimestampQuery(timestampQuery));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TopicDto setTimestampType(final long id, @NonNull final TimestampType timestampType) {
|
||||
return set(id, t -> t.setTimestampType(timestampType));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TopicDto set(final long id, @NonNull final Consumer<Topic> modifier) {
|
||||
final Topic topic = topicRepository.findById(id).orElseThrow();
|
||||
modifier.accept(topic);
|
||||
log.info("Topic CHANGED: {}", topic);
|
||||
final TopicDto dto = new TopicDto(topic);
|
||||
applicationEventPublisher.publishEvent(dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
}
|
||||
66
src/main/java/de/ph87/data/topic/query/TopicQuery.java
Normal file
66
src/main/java/de/ph87/data/topic/query/TopicQuery.java
Normal file
@ -0,0 +1,66 @@
|
||||
package de.ph87.data.topic.query;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Embeddable
|
||||
@NoArgsConstructor
|
||||
public class TopicQuery {
|
||||
|
||||
@Nullable
|
||||
@ManyToOne
|
||||
private Series series;
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String valueQuery = "";
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String beginQuery = "";
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String terminatedQuery = "";
|
||||
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private TopicQueryFunction function = TopicQueryFunction.NONE;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double factor;
|
||||
|
||||
public TopicQuery(@Nullable final Series series, @NonNull final String valueQuery) {
|
||||
this(series, valueQuery, "", "", TopicQueryFunction.NONE, 1);
|
||||
}
|
||||
|
||||
public TopicQuery(@Nullable final Series series, @NonNull final String valueQuery, final double factor) {
|
||||
this(series, valueQuery, "", "", TopicQueryFunction.NONE, factor);
|
||||
}
|
||||
|
||||
public TopicQuery(@Nullable final Series series, @NonNull final String valueQuery, @NonNull final TopicQueryFunction function) {
|
||||
this(series, valueQuery, "", "", function, 1);
|
||||
}
|
||||
|
||||
public TopicQuery(@Nullable final Series series, @NonNull final String valueQuery, @NonNull final String beginQuery, @NonNull final String terminatedQuery, @NonNull final TopicQueryFunction function, final double factor) {
|
||||
this.series = series;
|
||||
this.valueQuery = valueQuery;
|
||||
this.beginQuery = beginQuery;
|
||||
this.terminatedQuery = terminatedQuery;
|
||||
this.function = function;
|
||||
this.factor = factor;
|
||||
}
|
||||
|
||||
}
|
||||
41
src/main/java/de/ph87/data/topic/query/TopicQueryDto.java
Normal file
41
src/main/java/de/ph87/data/topic/query/TopicQueryDto.java
Normal file
@ -0,0 +1,41 @@
|
||||
package de.ph87.data.topic.query;
|
||||
|
||||
import de.ph87.data.series.SeriesDto;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import static de.ph87.data.Helpers.map;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class TopicQueryDto {
|
||||
|
||||
@Nullable
|
||||
public final SeriesDto series;
|
||||
|
||||
@NonNull
|
||||
public final String valueQuery;
|
||||
|
||||
@NonNull
|
||||
public final String beginQuery;
|
||||
|
||||
@NonNull
|
||||
public final String terminatedQuery;
|
||||
|
||||
@NonNull
|
||||
public final TopicQueryFunction function;
|
||||
|
||||
public final double factor;
|
||||
|
||||
public TopicQueryDto(@NonNull final TopicQuery topicQuery) {
|
||||
this.series = map(topicQuery.getSeries(), SeriesDto::new);
|
||||
this.valueQuery = topicQuery.getValueQuery();
|
||||
this.beginQuery = topicQuery.getBeginQuery();
|
||||
this.terminatedQuery = topicQuery.getTerminatedQuery();
|
||||
this.function = topicQuery.getFunction();
|
||||
this.factor = topicQuery.getFactor();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package de.ph87.data.topic.query;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public enum TopicQueryFunction {
|
||||
NONE(v -> v),
|
||||
ONLY_POSITIVE(v -> v > 0 ? v : 0),
|
||||
ONLY_NEGATIVE_BUT_NEGATE(v -> v < 0 ? -v : 0),
|
||||
;
|
||||
|
||||
private final Function<Double, Double> function;
|
||||
|
||||
TopicQueryFunction(@NonNull Function<Double, Double> function) {
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
public double apply(final double value) {
|
||||
return function.apply(value);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConsumerWithException<T, E extends Exception> {
|
||||
|
||||
void accept(final T t) throws E;
|
||||
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
public class Principal {
|
||||
|
||||
public final long id;
|
||||
|
||||
@NonNull
|
||||
public final String token;
|
||||
|
||||
@NonNull
|
||||
public final UserDto user;
|
||||
|
||||
public Principal(@NonNull final String token, @NonNull final User user) {
|
||||
this.id = user.getId();
|
||||
this.token = token;
|
||||
this.user = new UserDto(user);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PrincipalArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
public static final String AUTH_TOKEN_COOKIE_NAME = "PATRIX-DATA-MULTI-AUTH-TOKEN";
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(final MethodParameter parameter) {
|
||||
return parameter.getParameterType() == Principal.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
|
||||
final boolean required = !parameter.hasParameterAnnotation(PrincipalNotRequired.class);
|
||||
final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
if (request == null) {
|
||||
throw new RuntimeException("HttpServletRequest is required");
|
||||
}
|
||||
log.debug("Principal: path={}", request.getRequestURL().toString());
|
||||
|
||||
final Principal principal;
|
||||
if (request.getCookies() == null) {
|
||||
log.debug("Principal: No cookies received.");
|
||||
principal = null;
|
||||
} else {
|
||||
final String token = Arrays.stream(request.getCookies()).filter(c -> AUTH_TOKEN_COOKIE_NAME.equals(c.getName())).findFirst().map(Cookie::getValue).orElse("");
|
||||
if (token.isEmpty()) {
|
||||
log.debug("Principal: Token not set.");
|
||||
principal = null;
|
||||
} else {
|
||||
principal = userService.findPrincipalByToken(token).orElse(null);
|
||||
}
|
||||
}
|
||||
if (principal == null) {
|
||||
if (required) {
|
||||
log.warn("Principal: Not set but REQUIRED");
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||
} else {
|
||||
log.debug("Principal: Not set but NOT required");
|
||||
}
|
||||
}
|
||||
return principal;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PrincipalNotRequired {
|
||||
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Table(name = "`user`")
|
||||
@ToString(onlyExplicitlyIncluded = true)
|
||||
@NoArgsConstructor
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@ToString.Include
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Version
|
||||
@ToString.Include
|
||||
private long version;
|
||||
|
||||
@NonNull
|
||||
@ToString.Include
|
||||
@Column(nullable = false, unique = true)
|
||||
private String uuid = UUID.randomUUID().toString();
|
||||
|
||||
@NonNull
|
||||
@ToString.Include
|
||||
@Column(nullable = false, unique = true)
|
||||
private String username;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@NonNull
|
||||
@ElementCollection
|
||||
private Map<String, ZonedDateTime> tokens = new HashMap<>();
|
||||
|
||||
public User(final @NonNull UserCreate create, final @NonNull String password) {
|
||||
this.username = create.getUsername();
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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 org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static de.ph87.data.user.PrincipalArgumentResolver.AUTH_TOKEN_COOKIE_NAME;
|
||||
import static de.ph87.data.user.Helpers.map;
|
||||
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("User")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@NonNull
|
||||
@PostMapping("create")
|
||||
public UserDto create(@RequestBody final UserCreate create, final HttpServletResponse response) {
|
||||
try {
|
||||
final Principal principal = userService.create(create);
|
||||
response.addCookie(new Cookie(AUTH_TOKEN_COOKIE_NAME, principal.token));
|
||||
return principal.user;
|
||||
} catch (UserDuplicateError | UserInsecurePassword e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@GetMapping("whoAmI")
|
||||
public UserDto whoAmI(@PrincipalNotRequired @Nullable final Principal principal) {
|
||||
return map(principal, Principal::getUser);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@PostMapping("login")
|
||||
public UserDto login(@PrincipalNotRequired @Nullable final Principal oldPrincipal, @RequestBody final UserLogin login, final HttpServletResponse response) {
|
||||
if (oldPrincipal != null) {
|
||||
userService.logout(oldPrincipal);
|
||||
}
|
||||
try {
|
||||
final Principal principal = userService.login(login);
|
||||
response.addCookie(new Cookie(AUTH_TOKEN_COOKIE_NAME, principal.token));
|
||||
return principal.user;
|
||||
} catch (UserNotFound | UserWrongPassword e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("logout")
|
||||
public void logout(final Principal principal) {
|
||||
userService.logout(principal);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@PostMapping("setPassword")
|
||||
public UserDto setPassword(@NonNull final Principal principal, @RequestBody final String password) {
|
||||
try {
|
||||
return userService.setPassword(principal, password);
|
||||
} catch (UserInsecurePassword e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public class UserCreate {
|
||||
|
||||
@NonNull
|
||||
private String username;
|
||||
|
||||
@NonNull
|
||||
private String password;
|
||||
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
public class UserDto {
|
||||
|
||||
@NonNull
|
||||
public final String uuid;
|
||||
|
||||
@NonNull
|
||||
public final String username;
|
||||
|
||||
public UserDto(@NonNull final User user) {
|
||||
this.uuid = user.getUuid();
|
||||
this.username = user.getUsername();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused") // used in tests
|
||||
private UserDto(
|
||||
@JsonProperty("uuid") @NonNull final String uuid,
|
||||
@JsonProperty("username") @NonNull final String username
|
||||
) {
|
||||
this.uuid = uuid;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
public class UserDuplicateError extends Exception {
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
public class UserInsecurePassword extends Exception {
|
||||
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
public class UserLogin {
|
||||
|
||||
@NonNull
|
||||
public final String username;
|
||||
|
||||
@NonNull
|
||||
public final String password;
|
||||
|
||||
public UserLogin(@NonNull final String username, @NonNull final String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
public class UserNotFound extends Exception {
|
||||
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends ListCrudRepository<User, Long> {
|
||||
|
||||
Optional<User> findByUsername(@NonNull String username);
|
||||
|
||||
@Query("select u from User u join u.tokens t where key(t) = :token")
|
||||
Optional<User> findByToken(@NonNull String token);
|
||||
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
|
||||
private static final Duration TOKEN_TIMEOUT = Duration.ofDays(3);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@NonNull
|
||||
@Transactional
|
||||
public Principal create(final @NonNull UserCreate create) throws UserDuplicateError, UserInsecurePassword {
|
||||
final Optional<User> existing = userRepository.findByUsername(create.getUsername());
|
||||
if (existing.isPresent()) {
|
||||
log.warn("create: Duplicate username: existing={}", existing.get());
|
||||
throw new UserDuplicateError();
|
||||
}
|
||||
final User user = userRepository.save(new User(create, encodePassword(create.getPassword())));
|
||||
log.info("create: User created: user={}", user);
|
||||
return createToken(user);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Transactional
|
||||
public Principal login(@NonNull final UserLogin login) throws UserNotFound, UserWrongPassword {
|
||||
final User user = userRepository.findByUsername(login.username).orElse(null);
|
||||
if (user == null) {
|
||||
passwordEncoder.matches(login.password, ""); // make a dummy check to fake runtime for bruteforce attacks
|
||||
log.warn("login: User not found: username=\"{}\"", login.username);
|
||||
throw new UserNotFound();
|
||||
}
|
||||
if (!passwordEncoder.matches(login.password, user.getPassword())) {
|
||||
log.warn("login: Wrong password: user={}", user);
|
||||
throw new UserWrongPassword();
|
||||
}
|
||||
log.info("login: User logged in: user={}", user);
|
||||
return createToken(user);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Principal createToken(@NonNull final User user) {
|
||||
final String token = UUID.randomUUID().toString();
|
||||
user.getTokens().put(token, ZonedDateTime.now());
|
||||
return new Principal(token, user);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Transactional
|
||||
public UserDto setPassword(@NonNull final Principal principal, @NonNull final String password) throws UserInsecurePassword {
|
||||
return set(principal, u -> u.setPassword(encodePassword(password)));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private <E extends Exception> UserDto set(@NonNull final Principal principal, @NonNull final ConsumerWithException<User, E> modifier) throws E {
|
||||
final User user = userRepository.findById(principal.id).orElseThrow();
|
||||
modifier.accept(user);
|
||||
return new UserDto(user);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String encodePassword(@NonNull final String password) throws UserInsecurePassword {
|
||||
if (password.length() < 12) {
|
||||
throw new UserInsecurePassword();
|
||||
}
|
||||
return passwordEncoder.encode(password);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Transactional
|
||||
public Optional<Principal> findPrincipalByToken(@NonNull final String token) {
|
||||
final Optional<User> userOptional = userRepository.findByToken(token);
|
||||
if (userOptional.isEmpty()) {
|
||||
log.debug("findByToken: No user found by given token!");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final User user = userOptional.get();
|
||||
final ZonedDateTime last = user.getTokens().get(token);
|
||||
if (last == null) {
|
||||
throw new RuntimeException("Fetched User by token, but token isn't present in users tokens!");
|
||||
}
|
||||
|
||||
final ZonedDateTime earliest = ZonedDateTime.now().minus(TOKEN_TIMEOUT);
|
||||
if (last.isBefore(earliest)) {
|
||||
user.getTokens().remove(token);
|
||||
log.debug("findByToken: Token expired: user={}", user);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
log.info("findByToken: Renewed: user={}", user);
|
||||
user.getTokens().put(token, ZonedDateTime.now());
|
||||
return Optional.of(new Principal(token, user));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void logout(@NonNull final Principal principal) {
|
||||
set(principal, user -> {
|
||||
user.getTokens().remove(principal.token);
|
||||
log.info("logout: user={}", user);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
public class UserWrongPassword extends Exception {
|
||||
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final PrincipalArgumentResolver principalArgumentResolver;
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
resolvers.add(principalArgumentResolver);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
30
src/main/java/de/ph87/data/websocket/IWebsocketMessage.java
Normal file
30
src/main/java/de/ph87/data/websocket/IWebsocketMessage.java
Normal file
@ -0,0 +1,30 @@
|
||||
package de.ph87.data.websocket;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.NonNull;
|
||||
|
||||
public interface IWebsocketMessage {
|
||||
|
||||
String DTO_SUFFIX = "Dto";
|
||||
|
||||
@NonNull
|
||||
@JsonIgnore
|
||||
default String getWebsocketTopic() {
|
||||
return getNameWithoutDtoSuffix();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
default String getNameWithoutDtoSuffix() {
|
||||
final String name = getClass().getSimpleName();
|
||||
if (name.endsWith(DTO_SUFFIX)) {
|
||||
return name.substring(0, name.length() - DTO_SUFFIX.length());
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused") // json
|
||||
default String get_type_() {
|
||||
return getNameWithoutDtoSuffix();
|
||||
}
|
||||
|
||||
}
|
||||
40
src/main/java/de/ph87/data/websocket/WebSocketConfig.java
Normal file
40
src/main/java/de/ph87/data/websocket/WebSocketConfig.java
Normal file
@ -0,0 +1,40 @@
|
||||
package de.ph87.data.websocket;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@CrossOrigin
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
public static final String DESTINATION = "";
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/websocket").setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker(DESTINATION).setHeartbeatValue(new long[]{2000, 2000}).setTaskScheduler(heartBeatScheduler());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler heartBeatScheduler() {
|
||||
final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setThreadNamePrefix("wss-heartbeat-");
|
||||
scheduler.setPoolSize(1);
|
||||
scheduler.setRemoveOnCancelPolicy(true);
|
||||
scheduler.setAwaitTerminationSeconds(5);
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
}
|
||||
23
src/main/java/de/ph87/data/websocket/WebSocketService.java
Normal file
23
src/main/java/de/ph87/data/websocket/WebSocketService.java
Normal file
@ -0,0 +1,23 @@
|
||||
package de.ph87.data.websocket;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketService {
|
||||
|
||||
private final SimpMessageSendingOperations simpMessageSendingOperations;
|
||||
|
||||
@TransactionalEventListener(IWebsocketMessage.class)
|
||||
public void send(@NonNull final IWebsocketMessage message) {
|
||||
log.debug("Websocket: topic={}, payload={}", message.getWebsocketTopic(), message);
|
||||
simpMessageSendingOperations.convertAndSend(message.getWebsocketTopic(), message);
|
||||
}
|
||||
|
||||
}
|
||||
6
src/main/resources/application.properties
Normal file
6
src/main/resources/application.properties
Normal file
@ -0,0 +1,6 @@
|
||||
logging.level.root=WARN
|
||||
logging.level.de.ph87=INFO
|
||||
logging.level.calimero=OFF
|
||||
#-
|
||||
spring.main.banner-mode=off
|
||||
spring.jpa.open-in-view=false
|
||||
@ -1,99 +0,0 @@
|
||||
package de.ph87.data.user;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import lombok.NonNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
public class UserControllerTest {
|
||||
|
||||
private static final String USERNAME = "test-username";
|
||||
|
||||
private static final String PASSWORD = "test-password";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
void crud() throws Exception {
|
||||
Cookie[] cookies = new Cookie[0];
|
||||
anonymous(cookies, "/User/whoAmI");
|
||||
cookies = create(cookies);
|
||||
online(cookies, USERNAME);
|
||||
anonymous(cookies, "/User/logout");
|
||||
anonymous(cookies, "/User/whoAmI");
|
||||
cookies = login(cookies, USERNAME, PASSWORD);
|
||||
online(cookies, USERNAME);
|
||||
}
|
||||
|
||||
private void anonymous(final Cookie[] cookies, final String path) throws Exception {
|
||||
final MockHttpServletRequestBuilder request = get(path);
|
||||
if (cookies.length > 0) {
|
||||
request.cookie(cookies);
|
||||
}
|
||||
final MockHttpServletResponse response = mockMvc.perform(request).andExpect(status().isOk()).andReturn().getResponse();
|
||||
assertEquals("", response.getContentAsString());
|
||||
response.getCookies();
|
||||
}
|
||||
|
||||
private Cookie[] create(final Cookie[] cookies) throws Exception {
|
||||
final MockHttpServletRequestBuilder create = post("/User/create");
|
||||
if (cookies.length > 0) {
|
||||
create.cookie(cookies);
|
||||
}
|
||||
create.contentType(MediaType.APPLICATION_JSON);
|
||||
create.content(objectMapper.writeValueAsString(new UserCreate(USERNAME, PASSWORD)));
|
||||
return assertUser(create, USERNAME);
|
||||
}
|
||||
|
||||
private Cookie[] login(final Cookie[] cookies, final String username, final String password) throws Exception {
|
||||
final MockHttpServletRequestBuilder request = post("/User/login");
|
||||
if (cookies.length > 0) {
|
||||
request.cookie(cookies);
|
||||
}
|
||||
request.contentType(MediaType.APPLICATION_JSON);
|
||||
request.content(objectMapper.writeValueAsString(new UserLogin(username, password)));
|
||||
return assertUser(request, username);
|
||||
}
|
||||
|
||||
private void online(final Cookie[] cookies, final String username) throws Exception {
|
||||
final MockHttpServletRequestBuilder request = get("/User/whoAmI");
|
||||
if (cookies.length > 0) {
|
||||
request.cookie(cookies);
|
||||
}
|
||||
assertUser(request, username);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Cookie[] assertUser(final MockHttpServletRequestBuilder request, @NonNull final String username) throws Exception {
|
||||
final MockHttpServletResponse response = mockMvc.perform(request).andExpect(status().isOk()).andReturn().getResponse();
|
||||
final String payload = response.getContentAsString();
|
||||
assertNotNull(payload);
|
||||
assertNotEquals("", payload);
|
||||
final UserDto userDto = objectMapper.readValue(payload, UserDto.class);
|
||||
assertEquals(36, userDto.uuid.length());
|
||||
assertEquals(username, userDto.username);
|
||||
return response.getCookies();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user