Skip to content

Commit

Permalink
Moved HTTP operations into a separate class (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
climategadgets committed Aug 27, 2024
1 parent 9788700 commit fb27874
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 257 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package net.sf.dz3r.view.webui.v2;

import com.homeclimatecontrol.hcc.Version;
import com.homeclimatecontrol.hcc.meta.EndpointMeta;
import net.sf.dz3r.common.DurationFormatter;
import net.sf.dz3r.model.UnitDirector;
import net.sf.dz3r.runtime.GitProperties;
import net.sf.dz3r.signal.Signal;
import net.sf.dz3r.signal.hvac.ZoneStatus;
import net.sf.dz3r.view.UnitObserver;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.http.server.HttpServer;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.AbstractMap;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class HttpEndpoint {

private final Logger logger = LogManager.getLogger();
private static final DurationFormatter uptimeFormatter = new DurationFormatter();

private final String interfaces;
private final int port;
private final EndpointMeta endpointMeta;
private final Map<UnitDirector, UnitObserver> unit2observer;

public HttpEndpoint(String interfaces, int port, EndpointMeta endpointMeta, Map<UnitDirector, UnitObserver> unit2observer) {

this.interfaces = interfaces;
this.port = port;
this.endpointMeta = endpointMeta;
this.unit2observer = unit2observer;
}

void run(Instant startedAt) {

try {

var httpHandler = RouterFunctions.toHttpHandler(new RoutingConfiguration().monoRouterFunction(this));
var adapter = new ReactorHttpHandlerAdapter(httpHandler);

var server = HttpServer.create().host(interfaces).port(port);
var disposableServer = server.handle(adapter).bind().block();

logger.info("started in {}ms", Duration.between(startedAt, Instant.now()).toMillis());


disposableServer.onDispose().block(); // NOSONAR Acknowledged, ignored

logger.info("done");

} finally {
ThreadContext.pop();
}
}

/**
* Advertise the capabilities ({@code HTTP GET /} request).
*
* @param ignoredRq ignored.
*
* @return Whole system representation ({@link EndpointMeta} as JSON).
*/
public Mono<ServerResponse> getMeta(ServerRequest ignoredRq) {
logger.info("GET ? " + Version.PROTOCOL_VERSION);

return ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(endpointMeta), EndpointMeta.class);
}

/**
* Response handler for the sensor set request.
*
* @param ignoredRq ignored.
*
* @return Set of sensor representations.
*/
public Mono<ServerResponse> getSensors(ServerRequest ignoredRq) {
logger.info("GET /sensors");

return ok()
.cacheControl(CacheControl.noCache())
.contentType(MediaType.APPLICATION_JSON)
.body(Flux.fromIterable(unit2observer.values())
.flatMap(UnitObserver::getSensors),
Signal.class);
}

/**
* Response handler for individual sensor request.
*
* @param rq Request object.
*
* @return Individual sensor representation.
*/
public Mono<ServerResponse> getSensor(ServerRequest rq) {

String address = rq.pathVariable("sensor");
logger.info("GET /sensor/{}", address);

return ok()
.cacheControl(CacheControl.noCache())
.contentType(MediaType.APPLICATION_JSON)
.body(
Flux.fromIterable(unit2observer.values())
.flatMap(o -> o.getSensor(address)).next(),
Signal.class);
}

/**
* Response handler for the unit set request.
*
* @param ignoredRq ignored.
*
* @return Set of unit representations.
*/
public Mono<ServerResponse> getUnits(ServerRequest ignoredRq) {
logger.info("GET /units");

var units = Flux.fromIterable(unit2observer.entrySet())
.map(kv -> new AbstractMap.SimpleEntry<>(
kv.getKey().getAddress(),
kv.getValue().getUnitStatus()))
.collectMap(Map.Entry::getKey, Map.Entry::getValue);

return ok()
.cacheControl(CacheControl.noCache())
.contentType(MediaType.APPLICATION_JSON)
.body(units, Map.class);
}

/**
* Response handler for individual unit request.
*
* @param rq Request object.
*
* @return Individual unit representation.
*/
public Mono<ServerResponse> getUnit(ServerRequest rq) {

String name = rq.pathVariable("unit");
logger.info("GET /unit/{}", name);

// Returning empty JSON is simpler on both receiving and sending side than a 404
return ok()
.cacheControl(CacheControl.noCache())
.contentType(MediaType.APPLICATION_JSON)
.body(Flux.fromIterable(unit2observer.entrySet())
.filter(kv -> kv.getKey().getAddress().equals(name))
.map(kv -> kv.getValue().getUnitStatus()),
Object.class);
}

/**
* Response handler for setting individual unit state.
*
* @param rq Request object.
*
* @return Command response.
*/
public Mono<ServerResponse> setUnit(ServerRequest rq) {
String name = rq.pathVariable("unit");
logger.info("POST /unit/{}", name);
return ServerResponse.unprocessableEntity().bodyValue("Stay tuned, coming soon");
}

/**
* Response handler for the zone set request.
*
* @param ignoredRq ignored.
*
* @return Set of zone representations.
*/
public Mono<ServerResponse> getZones(ServerRequest ignoredRq) {
logger.info("GET /zones");

return ok()
.cacheControl(CacheControl.noCache())
.contentType(MediaType.APPLICATION_JSON)
.body(Flux.fromIterable(unit2observer.values())
.flatMap(UnitObserver::getZones),
ZoneStatus.class);
}

/**
* Response handler for individual zone request.
*
* @param rq Request object.
*
* @return Individual zone representation.
*/
public Mono<ServerResponse> getZone(ServerRequest rq) {

String zone = rq.pathVariable("zone");
logger.info("GET /zone/{}", zone);

return ok()
.cacheControl(CacheControl.noCache())
.contentType(MediaType.APPLICATION_JSON)
.body(
Flux.fromIterable(unit2observer.values())
.flatMap(o -> o.getZone(zone)).next(),
ZoneStatus.class);
}

/**
* Response handler for setting individual zone state.
*
* @param rq Request object.
*
* @return Command response.
*/
public Mono<ServerResponse> setZone(ServerRequest rq) {
String zone = rq.pathVariable("zone");
logger.info("POST /zone/{}", zone);
return ServerResponse.unprocessableEntity().bodyValue("Stay tuned, coming soon");
}

/**
* Get uptime.
*
* @param ignoredRq ignored.
*
* @return System uptime in both computer and human readable form.
*/
public Mono<ServerResponse> getUptime(ServerRequest ignoredRq) {
logger.info("GET /uptime");

var mx = ManagementFactory.getRuntimeMXBean();
var startMillis = mx.getStartTime();
var uptimeMillis = mx.getUptime();
var start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS").format(new Date(startMillis)) + " " + ZoneId.systemDefault();
var uptime = uptimeFormatter.format(uptimeMillis);

// Let's make the JSON order predictable
var result = new LinkedHashMap<>();

result.put("start", start);
result.put("uptime", uptime);
result.put("start.millis", startMillis);
result.put("uptime.millis", uptimeMillis);

return ok()
.cacheControl(CacheControl.noStore())
.contentType(MediaType.APPLICATION_JSON)
.body(Flux.fromIterable(result.entrySet()), Object.class);
}

/**
* Get the version.
*
* @param ignoredRq ignored.
*
* @return Git revision properties.
*/
public Mono<ServerResponse> getVersion(ServerRequest ignoredRq) {
logger.info("GET /version");

try {
return ok().contentType(MediaType.APPLICATION_JSON).body(Flux.fromIterable(GitProperties.get().entrySet()), Object.class);
} catch (IOException ex) {
throw new IllegalStateException("This shouldn't have happened", ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,26 @@ public class RoutingConfiguration {
private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

@Bean
public RouterFunction<ServerResponse> monoRouterFunction(WebUI webUI) {
public RouterFunction<ServerResponse> monoRouterFunction(HttpEndpoint endpoint) {
return route(

// Accessors

GET("/").and(ACCEPT_JSON), webUI::getMeta).andRoute(
GET(META_PATH).and(ACCEPT_JSON), webUI::getMeta).andRoute(
GET("/sensors").and(ACCEPT_JSON), webUI::getSensors).andRoute(
GET("/sensor/{sensor}").and(ACCEPT_JSON), webUI::getSensor).andRoute(
GET("/units").and(ACCEPT_JSON), webUI::getUnits).andRoute(
GET("/unit/{unit}").and(ACCEPT_JSON), webUI::getUnit).andRoute(
GET("/zones").and(ACCEPT_JSON), webUI::getZones).andRoute(
GET("/zone/{zone}").and(ACCEPT_JSON), webUI::getZone).andRoute(
GET("/").and(ACCEPT_JSON), endpoint::getMeta).andRoute(
GET(META_PATH).and(ACCEPT_JSON), endpoint::getMeta).andRoute(
GET("/sensors").and(ACCEPT_JSON), endpoint::getSensors).andRoute(
GET("/sensor/{sensor}").and(ACCEPT_JSON), endpoint::getSensor).andRoute(
GET("/units").and(ACCEPT_JSON), endpoint::getUnits).andRoute(
GET("/unit/{unit}").and(ACCEPT_JSON), endpoint::getUnit).andRoute(
GET("/zones").and(ACCEPT_JSON), endpoint::getZones).andRoute(
GET("/zone/{zone}").and(ACCEPT_JSON), endpoint::getZone).andRoute(

GET("/uptime").and(ACCEPT_JSON), webUI::getUptime).andRoute(
GET("/version").and(ACCEPT_JSON), webUI::getVersion).andRoute(
GET("/uptime").and(ACCEPT_JSON), endpoint::getUptime).andRoute(
GET("/version").and(ACCEPT_JSON), endpoint::getVersion).andRoute(

// Mutators

POST("/zone{zone}").and(ACCEPT_JSON), webUI::setZone).andRoute(
POST("/unit/{unit}").and(ACCEPT_JSON), webUI::setUnit);
POST("/zone{zone}").and(ACCEPT_JSON), endpoint::setZone).andRoute(
POST("/unit/{unit}").and(ACCEPT_JSON), endpoint::setUnit);
}
}
Loading

0 comments on commit fb27874

Please sign in to comment.