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>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-core</artifactId>
|
<artifactId>spring-security-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- <dependency>-->
|
|
||||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
|
||||||
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
|
|
||||||
<!-- </dependency>-->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
@ -58,6 +54,11 @@
|
|||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.paho</groupId>
|
||||||
|
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||||
|
<version>1.2.5</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</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 jakarta.annotation.Nullable;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
@ -8,11 +8,11 @@ import java.util.function.Function;
|
|||||||
public class Helpers {
|
public class Helpers {
|
||||||
|
|
||||||
@Nullable
|
@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) {
|
if (t == null) {
|
||||||
return 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