feat: make gb32960 archive history query production ready
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package com.lingniu.ingest.vehiclestate;
|
||||
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class RedisVehicleStateRepository implements VehicleStateRepository {
|
||||
|
||||
private final StringRedisTemplate redis;
|
||||
|
||||
public RedisVehicleStateRepository(StringRedisTemplate redis) {
|
||||
if (redis == null) {
|
||||
throw new IllegalArgumentException("redis must not be null");
|
||||
}
|
||||
this.redis = redis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putState(String vin, String json) {
|
||||
put(key("vehicle:state:", vin), json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putLocation(String vin, String json) {
|
||||
put(key("vehicle:location:", vin), json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putSafety(String vin, String json) {
|
||||
put(key("vehicle:safety:", vin), json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putLastEvent(String vin, String json) {
|
||||
put(key("vehicle:event:last:", vin), json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getState(String vin) {
|
||||
return get(key("vehicle:state:", vin));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getLocation(String vin) {
|
||||
return get(key("vehicle:location:", vin));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getSafety(String vin) {
|
||||
return get(key("vehicle:safety:", vin));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getLastEvent(String vin) {
|
||||
return get(key("vehicle:event:last:", vin));
|
||||
}
|
||||
|
||||
private void put(String key, String json) {
|
||||
redis.opsForValue().set(key, json == null ? "{}" : json);
|
||||
}
|
||||
|
||||
private Optional<String> get(String key) {
|
||||
return Optional.ofNullable(redis.opsForValue().get(key));
|
||||
}
|
||||
|
||||
private static String key(String prefix, String vin) {
|
||||
if (vin == null || vin.isBlank()) {
|
||||
throw new IllegalArgumentException("vin must not be blank");
|
||||
}
|
||||
return prefix + vin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.lingniu.ingest.vehiclestate;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-state", name = "enabled", havingValue = "true")
|
||||
@ConditionalOnBean(VehicleStateRepository.class)
|
||||
@RequestMapping(path = "/api/vehicle-state", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public final class VehicleStateController {
|
||||
|
||||
private final VehicleStateRepository repository;
|
||||
|
||||
public VehicleStateController(VehicleStateRepository repository) {
|
||||
if (repository == null) {
|
||||
throw new IllegalArgumentException("repository must not be null");
|
||||
}
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@GetMapping("/{vin}")
|
||||
public ResponseEntity<String> state(@PathVariable String vin) {
|
||||
return json(repository.getState(vin));
|
||||
}
|
||||
|
||||
@GetMapping("/{vin}/location")
|
||||
public ResponseEntity<String> location(@PathVariable String vin) {
|
||||
return json(repository.getLocation(vin));
|
||||
}
|
||||
|
||||
@GetMapping("/{vin}/safety")
|
||||
public ResponseEntity<String> safety(@PathVariable String vin) {
|
||||
return json(repository.getSafety(vin));
|
||||
}
|
||||
|
||||
@GetMapping("/{vin}/last-event")
|
||||
public ResponseEntity<String> lastEvent(@PathVariable String vin) {
|
||||
return json(repository.getLastEvent(vin));
|
||||
}
|
||||
|
||||
private static ResponseEntity<String> json(Optional<String> value) {
|
||||
return value.map(json -> ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(json))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.lingniu.ingest.vehiclestate;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeIngestResult;
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeIngestor;
|
||||
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||
|
||||
public final class VehicleStateEnvelopeIngestor implements EnvelopeIngestor {
|
||||
|
||||
private final VehicleStateUpdater updater;
|
||||
|
||||
public VehicleStateEnvelopeIngestor(VehicleStateUpdater updater) {
|
||||
if (updater == null) {
|
||||
throw new IllegalArgumentException("updater must not be null");
|
||||
}
|
||||
this.updater = updater;
|
||||
}
|
||||
|
||||
public void ingest(byte[] kafkaValue) {
|
||||
updater.update(parse(kafkaValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnvelopeIngestResult tryIngest(byte[] kafkaValue) {
|
||||
VehicleEnvelope envelope = null;
|
||||
try {
|
||||
envelope = parse(kafkaValue);
|
||||
updater.update(envelope);
|
||||
return EnvelopeIngestResult.processed(envelope.getEventId(), envelope.getVin());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return envelope == null
|
||||
? EnvelopeIngestResult.invalid(ex.getMessage())
|
||||
: EnvelopeIngestResult.skipped(envelope.getEventId(), envelope.getVin(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static VehicleEnvelope parse(byte[] kafkaValue) {
|
||||
if (kafkaValue == null || kafkaValue.length == 0) {
|
||||
throw new IllegalArgumentException("VehicleEnvelope bytes must not be empty");
|
||||
}
|
||||
try {
|
||||
return VehicleEnvelope.parseFrom(kafkaValue);
|
||||
} catch (InvalidProtocolBufferException ex) {
|
||||
throw new IllegalArgumentException("VehicleEnvelope bytes are invalid", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.lingniu.ingest.vehiclestate;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface VehicleStateRepository {
|
||||
|
||||
void putState(String vin, String json);
|
||||
|
||||
void putLocation(String vin, String json);
|
||||
|
||||
void putSafety(String vin, String json);
|
||||
|
||||
void putLastEvent(String vin, String json);
|
||||
|
||||
Optional<String> getState(String vin);
|
||||
|
||||
Optional<String> getLocation(String vin);
|
||||
|
||||
Optional<String> getSafety(String vin);
|
||||
|
||||
Optional<String> getLastEvent(String vin);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.lingniu.ingest.vehiclestate;
|
||||
|
||||
import com.lingniu.ingest.sink.mq.proto.TelemetryField;
|
||||
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class VehicleStateUpdater {
|
||||
|
||||
private final VehicleStateRepository repository;
|
||||
|
||||
public VehicleStateUpdater(VehicleStateRepository repository) {
|
||||
if (repository == null) {
|
||||
throw new IllegalArgumentException("repository must not be null");
|
||||
}
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void update(VehicleEnvelope envelope) {
|
||||
if (envelope == null) {
|
||||
throw new IllegalArgumentException("envelope must not be null");
|
||||
}
|
||||
if (!envelope.hasTelemetrySnapshot()) {
|
||||
throw new IllegalArgumentException("envelope telemetry_snapshot is required");
|
||||
}
|
||||
Map<String, String> fields = fields(envelope);
|
||||
String vin = envelope.getVin();
|
||||
|
||||
repository.putState(vin, json(base(envelope, fields)));
|
||||
repository.putLastEvent(vin, json(lastEvent(envelope)));
|
||||
|
||||
if (fields.containsKey("longitude") && fields.containsKey("latitude")) {
|
||||
repository.putLocation(vin, json(location(envelope, fields)));
|
||||
}
|
||||
if (hasSafetyFields(fields)) {
|
||||
repository.putSafety(vin, json(safety(envelope, fields)));
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> fields(VehicleEnvelope envelope) {
|
||||
Map<String, String> fields = new LinkedHashMap<>();
|
||||
for (TelemetryField field : envelope.getTelemetrySnapshot().getFieldsList()) {
|
||||
fields.put(field.getKey(), field.getValue());
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static Map<String, Object> base(VehicleEnvelope envelope, Map<String, String> fields) {
|
||||
Map<String, Object> map = eventIdentity(envelope);
|
||||
map.put("eventType", envelope.getTelemetrySnapshot().getEventType());
|
||||
map.put("fields", fields);
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Map<String, Object> location(VehicleEnvelope envelope, Map<String, String> fields) {
|
||||
Map<String, Object> map = eventIdentity(envelope);
|
||||
putIfPresent(map, fields, "longitude");
|
||||
putIfPresent(map, fields, "latitude");
|
||||
putIfPresent(map, fields, "altitude_m");
|
||||
putIfPresent(map, fields, "direction_deg");
|
||||
putIfPresent(map, fields, "speed_kmh");
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Map<String, Object> safety(VehicleEnvelope envelope, Map<String, String> fields) {
|
||||
Map<String, Object> map = eventIdentity(envelope);
|
||||
putIfPresent(map, fields, "safety_category");
|
||||
putIfPresent(map, fields, "hydrogen_leak_detected");
|
||||
putIfPresent(map, fields, "hydrogen_leak_level");
|
||||
putIfPresent(map, fields, "hydrogen_leak_action_required");
|
||||
putIfPresent(map, fields, "alarm_level");
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Map<String, Object> lastEvent(VehicleEnvelope envelope) {
|
||||
return eventIdentity(envelope);
|
||||
}
|
||||
|
||||
private static Map<String, Object> eventIdentity(VehicleEnvelope envelope) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("eventId", envelope.getEventId());
|
||||
map.put("traceId", envelope.getTraceId());
|
||||
map.put("vin", envelope.getVin());
|
||||
map.put("source", envelope.getSource());
|
||||
map.put("eventTimeMs", Long.toString(envelope.getEventTimeMs()));
|
||||
map.put("ingestTimeMs", Long.toString(envelope.getIngestTimeMs()));
|
||||
return map;
|
||||
}
|
||||
|
||||
private static boolean hasSafetyFields(Map<String, String> fields) {
|
||||
return fields.containsKey("safety_category")
|
||||
|| fields.containsKey("hydrogen_leak_detected")
|
||||
|| fields.containsKey("hydrogen_leak_level")
|
||||
|| fields.containsKey("hydrogen_leak_action_required")
|
||||
|| fields.containsKey("alarm_level");
|
||||
}
|
||||
|
||||
private static void putIfPresent(Map<String, Object> map, Map<String, String> fields, String key) {
|
||||
if (fields.containsKey(key)) {
|
||||
map.put(key, fields.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
private static String json(Map<String, ?> map) {
|
||||
StringBuilder out = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, ?> entry : map.entrySet()) {
|
||||
if (!first) {
|
||||
out.append(',');
|
||||
}
|
||||
first = false;
|
||||
out.append('"').append(escape(entry.getKey())).append('"').append(':');
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map<?, ?> nested) {
|
||||
out.append(jsonObject(nested));
|
||||
} else {
|
||||
out.append('"').append(escape(String.valueOf(value))).append('"');
|
||||
}
|
||||
}
|
||||
return out.append('}').toString();
|
||||
}
|
||||
|
||||
private static String jsonObject(Map<?, ?> map) {
|
||||
StringBuilder out = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (!first) {
|
||||
out.append(',');
|
||||
}
|
||||
first = false;
|
||||
out.append('"').append(escape(String.valueOf(entry.getKey()))).append('"')
|
||||
.append(':')
|
||||
.append('"').append(escape(String.valueOf(entry.getValue()))).append('"');
|
||||
}
|
||||
return out.append('}').toString();
|
||||
}
|
||||
|
||||
private static String escape(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.lingniu.ingest.vehiclestate.config;
|
||||
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeConsumerProcessor;
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeDeadLetterSink;
|
||||
import com.lingniu.ingest.vehiclestate.RedisVehicleStateRepository;
|
||||
import com.lingniu.ingest.vehiclestate.VehicleStateController;
|
||||
import com.lingniu.ingest.vehiclestate.VehicleStateEnvelopeIngestor;
|
||||
import com.lingniu.ingest.vehiclestate.VehicleStateRepository;
|
||||
import com.lingniu.ingest.vehiclestate.VehicleStateUpdater;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-state", name = "enabled", havingValue = "true")
|
||||
public class VehicleStateAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(StringRedisTemplate.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStateRepository vehicleStateRepository(StringRedisTemplate redis) {
|
||||
return new RedisVehicleStateRepository(redis);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(VehicleStateRepository.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStateUpdater vehicleStateUpdater(VehicleStateRepository repository) {
|
||||
return new VehicleStateUpdater(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(VehicleStateUpdater.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStateEnvelopeIngestor vehicleStateEnvelopeIngestor(VehicleStateUpdater updater) {
|
||||
return new VehicleStateEnvelopeIngestor(updater);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean({VehicleStateEnvelopeIngestor.class, EnvelopeDeadLetterSink.class})
|
||||
@ConditionalOnMissingBean(name = "vehicleStateEnvelopeConsumerProcessor")
|
||||
public EnvelopeConsumerProcessor vehicleStateEnvelopeConsumerProcessor(VehicleStateEnvelopeIngestor ingestor,
|
||||
EnvelopeDeadLetterSink deadLetterSink) {
|
||||
return new EnvelopeConsumerProcessor("vehicle-state", ingestor, deadLetterSink);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(VehicleStateRepository.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStateController vehicleStateController(VehicleStateRepository repository) {
|
||||
return new VehicleStateController(repository);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.lingniu.ingest.vehiclestate.config.VehicleStateAutoConfiguration
|
||||
Reference in New Issue
Block a user