HTTP
Nasdanika Core HTTP module provides classes and interfaces for serving HTTP requests with Reactor Netty HTTP Server routes.
Reflective routing
ReflectiveHttpServerRouteBuilder uses Route and RouteBuilder annotations on methods, fields and types to build server routes.
package org.nasdanika.launcher.demo.http;
import org.nasdanika.http.ReflectiveHttpServerRouteBuilder.Route;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;
@Route("/test/")
public class DemoReflectiveHttpRoutes {
/**
* In this handler method GET is derived from the get prefix
* and path is derived from Hello
*/
@Route
public Publisher<Void> getHello(
HttpServerRequest request,
HttpServerResponse response) {
return response.sendString(Mono.just("getHello()"));
}
/**
* In this handler the HTTP method is GET because the
* first segment "hola" doesn't match any HTTP method.
* The path is "hola/soy/dora"
*/
@Route
public Publisher<Void> holaSoyDora(
HttpServerRequest request,
HttpServerResponse response) {
return response.sendString(Mono.just("holaSoyDora()"));
}
/**
* In this handler the path is explicitly specified
* by the Route annotation
*/
@Route("do-something")
public Publisher<Void> doSomething(
HttpServerRequest request,
HttpServerResponse response) {
return response.sendString(Mono.just("do someting"));
}
@RouteBuilder("field-route-builder")
public HttpServerRouteBuilder routeBuilder = routes -> {
routes.get("/hello", (request, response) -> response.sendString(Mono.just("Hello from field route builder!")));
};
@RouteBuilder("getter-route-builder")
public HttpServerRouteBuilder getRouteBuilder() {
return routes -> {
routes.get("/hello", (request, response) -> response.sendString(Mono.just("Hello from getter route builder!")));
};
};
@RouteBuilder("route-builder-method")
public void buildRoutes(HttpServerRoutes routes) {
routes.get("/hello", (request, response) -> response.sendString(Mono.just("Hello from route builder method!")));
};
@Route
public JSONObject getApiSearch(
HttpServerRequest request,
HttpServerResponse response) {
JSONObject result = new JSONObject();
result.put("result", "Hello World!");
return result;
}
@Route("do-something-else")
public Mono<String> doSomethingElse(
HttpServerRequest request,
HttpServerResponse response) {
return Mono.just("do someting else");
}
}
The code snippet above shows three GET handler methods, a route builder field and two flavors of builder methods. All of their paths are prefixed with /test/ from the class level annotation.
The getApiSearch() method returns JSONObject. The returned value is converted to String and is sent as a response with application/json content type header.
Conversion works as explained below. For Mono and Flux Mono|Flux<String> is assumed by default and request.sendString() is used. The mono result is mapped to String using ReflectiveHttpServerRouteBuilder.toString(Object).
Set binary attribute of @Route annotation to true for binary content so request.sendByteArray() is used. In this case mapping to byte[] is performed by ReflectiveHttpServerRouteBuilder.toByteArray(Object).
Both ReflectiveHttpServerRouteBuilder.toString(Object) and ReflectiveHttpServerRouteBuilder.toByteArray(Object) use DefaultConverter.INSTANCE obtained from getConverter() method. These methods can be overridden to add support for additional conversions. E.g. URI to String or byte[] using ResourceSet URI Converter obtained as capability to, say, serve resources from Maven jars or GitLab using respective URIHandlers - see Maven/Gitlab models documentation for more details.
Publisherwhich is notMonoorFluxis cast toPublisher<Void>and returned.Stringis not convertedJSONObjectis converted to string and alsoapplication/jsoncontent type header is addedJSONArrayis converted to string and alsoapplication/jsoncontent type header is addedbyte[]is not convertedInputStreamis converted tobyte[]- Other types are converted to a byte array, using
ReflectiveHttpServerRouteBuilder.toByteArray(Object), ifbinaryis true and to a string usingReflectiveHttpServerRouteBuilder.toString(Object)otherwise.
The code snippet below shows how to use the above handlers. Note that the handler target is registered with /reflective prefix. As such, the full path for, say, getHello() method, is /reflective/test/hello.
ReflectiveHttpServerRouteBuilder builder = new ReflectiveHttpServerRouteBuilder();
builder.addTargets("/reflective", new DemoReflectiveHttpRoutes());
DisposableServer server = HttpServer
.create()
.port(8080)
.route(builder::buildRoutes)
.bindNow();
try (Terminal terminal = TerminalBuilder.builder().system(true).build()) {
LineReader lineReader = LineReaderBuilder
.builder()
.terminal(terminal)
.build();
String prompt = "http-server>";
while (true) {
String line = null;
line = lineReader.readLine(prompt);
System.out.println("Got: " + line);
if ("exit".equals(line)) {
break;
}
}
}
server.dispose();
server.onDispose().block();
Factories
Route building can be organized into hierarchy using methods and fields annotated with Reflector.Factory or Reflector.Factories annotations. Such methods can also be annotated with Route - in this case route value is used as a prefix. Below snippets show two levels of factory hierarchy:
import org.nasdanika.common.Reflector;
import org.nasdanika.http.ReflectiveHttpServerRouteBuilder.Route;
/**
* Demo of a hierarchical routing with factory
*/
@Route("/super-factory")
public class ReflectorSuperFactory {
@Route("/super-demo")
@Reflector.Factory
public ReflectorFactory getRoutes() {
return new ReflectorFactory();
}
}
import org.nasdanika.common.Reflector;
import org.nasdanika.http.ReflectiveHttpServerRouteBuilder.Route;
/**
* Demo of a hierarchical routing with factory
*/
@Route("/factory")
public class ReflectorFactory {
@Route("/demo")
@Reflector.Factory
public DemoReflectiveHttpRoutes getRoutes() {
return new DemoReflectiveHttpRoutes();
}
}
With factories you can assemble web applications from parameterizable building blocks.
Serving
Java
ReflectiveHttpServerRouteBuilder builder = new ReflectiveHttpServerRouteBuilder();
builder.addTargets("/reflective", new DemoReflectiveHttpRoutes());
DisposableServer server = HttpServer
.create()
.port(8080)
.route(builder::buildRoutes)
.bindNow();
try (Terminal terminal = TerminalBuilder.builder().system(true).build()) {
LineReader lineReader = LineReaderBuilder
.builder()
.terminal(terminal)
.build();
String prompt = "http-server>";
while (true) {
String line = null;
line = lineReader.readLine(prompt);
System.out.println("Got: " + line);
if ("exit".equals(line)) {
break;
}
}
}
server.dispose();
server.onDispose().block();
http-server CLI command
You can build a custom CLI and serve routes with http-server command by creating a capability factory and registering it in module-info.java.
Capability factory
public class DemoReflectiveHttpRoutesFactory extends ServiceCapabilityFactory<Void, HttpServerRouteBuilder> {
@Override
public boolean isFor(Class<?> type, Object requirement) {
return HttpServerRouteBuilder.class == type && requirement == null;
}
@Override
protected CompletionStage<Iterable<CapabilityProvider<HttpServerRouteBuilder>>> createService(
Class<HttpServerRouteBuilder> serviceType,
Void serviceRequirement,
Loader loader,
ProgressMonitor progressMonitor) {
ReflectiveHttpServerRouteBuilder builder = new ReflectiveHttpServerRouteBuilder();
builder.addTargets("/reflective", new DemoReflectiveHttpRoutes());
return wrap(builder);
}
}
module-info.java
...
provides CapabilityFactory with DemoDiagramRoutesBuilderFactory;
...
Telemetry
You may pass TelemetryFilter to ReflectiveHttpServerRouteBuilder. In this case route methods which return String, JSONArray, JSONObject, byte[] or InputStream will be filtered to collect telemetry and propagate telemetry context along the reactive chain.
Graph/Diagram processors
HttpServerRouteBuilder has a static buildRoutes(Collection<ProcessorInfo<P>> processorInfos, String routeProperty, HttpServerRoutes routes) method for building routes from graph/diagram processors implementing BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> or implementing/adaptable to HttpServerRouteBuilder. You can find examples in Nasdanika Demo CLI.
Processors
Groovy
Below is a Groovy processor for single route:
1 import reactor.core.publisher.Mono
2 import org.nasdanika.drawio.Node
3 import org.nasdanika.graph.processor.ProcessorElement
4
5 new java.util.function.BiFunction() {
6
7 @ProcessorElement
8 public Node element;
9
10 def apply(request, response) {
11 response.sendString(Mono.just(element.getLabel()))
12 }
13
14 }
The diagram element is injected into the element field at line 8 because the fields is annotated with @ProcessorElement annotation (line 7). apply() method at line 10 processes HTTP requests by sending the element label.
Java
Route (BiFunction)
Below is a Java processor serving a single route:
1 package org.nasdanika.launcher.demo.drawio;
2
3 import java.util.concurrent.CompletionStage;
4 import java.util.function.BiConsumer;
5 import java.util.function.BiFunction;
6 import java.util.function.Consumer;
7
8 import org.nasdanika.capability.CapabilityFactory.Loader;
9 import org.nasdanika.common.Invocable;
10 import org.nasdanika.common.ProgressMonitor;
11 import org.nasdanika.drawio.Node;
12 import org.nasdanika.graph.Element;
13 import org.nasdanika.graph.processor.ConnectionProcessorConfig;
14 import org.nasdanika.graph.processor.NodeProcessorConfig;
15 import org.nasdanika.graph.processor.ProcessorConfig;
16 import org.nasdanika.graph.processor.ProcessorElement;
17 import org.nasdanika.graph.processor.ProcessorInfo;
18 import org.reactivestreams.Publisher;
19
20 import reactor.core.publisher.Mono;
21 import reactor.netty.http.server.HttpServerRequest;
22 import reactor.netty.http.server.HttpServerResponse;
23
24 /**
25 * Diagram element processor which processes HTTP requests
26 */
27 public class SystemHttpHandler implements BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> {
28
29 private String amount;
30
31 @ProcessorElement
32 public void setElement(Node element) {
33 this.amount = element.getProperty("amount");
34 }
35
36 /**
37 * This is the constructor signature for graph processor classes which are to be instantiated by URIInvocableCapabilityFactory (org.nasdanika.capability.factories.URIInvocableCapabilityFactory).
38 * Config may be of specific types {@link ProcessorConfig} - {@link NodeProcessorConfig} or {@link ConnectionProcessorConfig}.
39 * @param loader
40 * @param loaderProgressMonitor
41 * @param data
42 * @param fragment
43 * @param config
44 * @param infoProvider
45 * @param endpointWiringStageConsumer
46 * @param wiringProgressMonitor
47 */
48 public SystemHttpHandler(
49 Loader loader,
50 ProgressMonitor loaderProgressMonitor,
51 Object data,
52 String fragment,
53 ProcessorConfig config,
54 BiConsumer<Element, BiConsumer<ProcessorInfo<Invocable>, ProgressMonitor>> infoProvider,
55 Consumer<CompletionStage<?>> endpointWiringStageConsumer,
56 ProgressMonitor wiringProgressMonitor) {
57
58 System.out.println("I got constructed " + this);
59 }
60
61 @Override
62 public Publisher<Void> apply(HttpServerRequest request, HttpServerResponse response) {
63 return response.sendString(Mono.just("Account: " + request.param("account") + ", Amount: " + amount));
64 }
65
66 }
apply() method at line 62 processes HTTP GET requests.
Route Builder (HttpServerRouteBuilder)
The below processor implements HttpServerRouteBuilder:
package org.nasdanika.launcher.demo.drawio;
import java.util.concurrent.CompletionStage;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.nasdanika.capability.CapabilityFactory.Loader;
import org.nasdanika.common.Invocable;
import org.nasdanika.common.ProgressMonitor;
import org.nasdanika.drawio.Node;
import org.nasdanika.graph.Element;
import org.nasdanika.graph.processor.ConnectionProcessorConfig;
import org.nasdanika.graph.processor.NodeProcessorConfig;
import org.nasdanika.graph.processor.ProcessorConfig;
import org.nasdanika.graph.processor.ProcessorElement;
import org.nasdanika.graph.processor.ProcessorInfo;
import org.nasdanika.http.HttpServerRouteBuilder;
import reactor.core.publisher.Mono;
import reactor.netty.http.server.HttpServerRoutes;
/**
* Diagram element processor which builds HTTP routes
*/
public class RouteBuilderProcessor implements HttpServerRouteBuilder {
private String amount;
@ProcessorElement
public void setElement(Node element) {
this.amount = element.getProperty("amount");
}
/**
* This is the constructor signature for graph processor classes which are to be instantiated by URIInvocableCapabilityFactory (org.nasdanika.capability.factories.URIInvocableCapabilityFactory).
* Config may be of specific types {@link ProcessorConfig} - {@link NodeProcessorConfig} or {@link ConnectionProcessorConfig}.
* @param loader
* @param loaderProgressMonitor
* @param data
* @param fragment
* @param config
* @param infoProvider
* @param endpointWiringStageConsumer
* @param wiringProgressMonitor
*/
public RouteBuilderProcessor(
Loader loader,
ProgressMonitor loaderProgressMonitor,
Object data,
String fragment,
ProcessorConfig config,
BiConsumer<Element, BiConsumer<ProcessorInfo<Invocable>, ProgressMonitor>> infoProvider,
Consumer<CompletionStage<?>> endpointWiringStageConsumer,
ProgressMonitor wiringProgressMonitor) {
System.out.println("I got constructed " + this);
}
@Override
public void buildRoutes(HttpServerRoutes routes) {
routes.get("/balance", (request, response) -> response.sendString(Mono.just("Account: " + request.param("account") + ", Amount: " + amount)));
}
}
Serving
drawio http-server
One way to have diagrams to process HTTP requests is to use drawio http-server command pipeline. Below is a sample command line:
drawio test-data/drawio-http/diagram.drawio http-server --http-port=8080 processor route
http-server
Another option is to use the http-server command in a custom CLI assembly. In this case diagrams shall be provided as HttpServerRouteBuilder capabilities. This is done by creating a subclass of DiagramRoutesBuilderFactory:
package org.nasdanika.launcher.demo.drawio;
import org.eclipse.emf.common.util.URI;
import org.nasdanika.common.Util;
import org.nasdanika.http.DiagramRoutesBuilderFactory;
public class DemoDiagramRoutesBuilderFactory extends DiagramRoutesBuilderFactory {
public DemoDiagramRoutesBuilderFactory() {
super(
URI
.createURI("system.drawio")
.resolve(Util.createClassURI(DemoDiagramRoutesBuilderFactory.class)),
"processor",
"route");
}
}
and adding it to module-info.java provides.
Java
The below snippet shows how to serve diagram processors routes with Java:
Document document = Document.load(
URI.createFileURI(new File("test-data/drawio-http/diagram.drawio").getCanonicalPath()),
null,
null);
ElementProcessorFactory<Object> elementProcessorFactory = new ElementProcessorFactory<Object>(
document,
new CapabilityLoader(),
"processor");
ProgressMonitor progressMonitor = new PrintStreamProgressMonitor();
Map<Element, ProcessorInfo<Object>> processors = elementProcessorFactory.createProcessors(
null,
null,
progressMonitor);
DisposableServer server = HttpServer
.create()
.port(8080)
.route(routes -> HttpServerRouteBuilder.buildRoutes(processors.values(), "route", routes))
.bindNow();
try (Terminal terminal = TerminalBuilder.builder().system(true).build()) {
LineReader lineReader = LineReaderBuilder
.builder()
.terminal(terminal)
.build();
String prompt = "http-server>";
while (true) {
String line = null;
line = lineReader.readLine(prompt);
System.out.println("Got: " + line);
if ("exit".equals(line)) {
break;
}
}
}
server.dispose();
server.onDispose().block();
SerpapiConnector
SerpapiConnector class uses SerpApi Google search to find pages. Then it retrieves them, extracts main content (content of the main element, configurable) and the converts the main content to Markdown. Search result includes SerpApi data, page content, main content and markdown main content. The primary purpose of this class it to provide grounding for LLMs and AI agents with information that matters (main content) and in a format which is semantically structured.
Example:
String apiKey = System.getenv("SERPER_KEY");
String query = "What is a kernel function in microsoft semantic kernel";
SerpapiConnector serpApiConnector = new SerpapiConnector(apiKey, "learn.microsoft.com/en-us/semantic-kernel");
Flux<SearchResult> results = serpApiConnector.search(query, 10, 0);
List<SearchResult> resultList = results.collectList().block();
for (SearchResult result: resultList) {
System.out.println("===");
System.out.println(result.title());
System.out.println();
System.out.println(result.markdownMainContent());
}
Nasdanika