From 405dae71a46438aa5c0a35879996b3fc62e93eaa Mon Sep 17 00:00:00 2001 From: Alejandro Revilla Date: Sun, 27 Jan 2019 23:49:39 -0300 Subject: [PATCH] add route support at the RestServer level --- doc/src/asciidoc/module_qrest.adoc | 37 +++- modules/qrest/src/dist/deploy/50_qrest.xml | 2 + .../main/java/org/jpos/qrest/RestServer.java | 54 ++++- .../main/java/org/jpos/qrest/RestSession.java | 2 +- .../src/main/java/org/jpos/qrest/Route.java | 1 - .../org/jpos/qrest/participant/Q2Info.java | 39 ++-- .../org/jpos/qrest/participant/Q2Info.new | 200 ++++++++++++++++++ .../test/java/org/jpos/qrest/RestTest.java | 12 ++ 8 files changed, 325 insertions(+), 22 deletions(-) create mode 100644 modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.new diff --git a/doc/src/asciidoc/module_qrest.adoc b/doc/src/asciidoc/module_qrest.adoc index 566fcabc07..306bccccb5 100644 --- a/doc/src/asciidoc/module_qrest.adoc +++ b/doc/src/asciidoc/module_qrest.adoc @@ -48,7 +48,7 @@ we've created a little **QRest** module that can be configured like this: ---------------------------------------------------------------------------------------- <1> Listening port -<2> Transaction manager queue name +<2> Transaction manager queue name (if no specific routes are present) <3> `true` to enable TLS <4> Set to `false` in order to allow self-signed certificates <5> `true` requires client-side certificates @@ -61,6 +61,25 @@ Once the server receives an HTTP request, it creates a `org.jpos.transaction.Con (under the Constant name `REQUEST` defined in the `org.jpos.qrest.Constants` enum), and to the session in the `SESSION` constant (so that a `SendResponse` participant can reply) and send it off to the TransactionManager for processing. +If no specific `` entries are present in the QRest configuration, incoming messages are sent to the `queue` +specified in the `queue` property, but you can override those with a route like this: + +[source,xml] +---------------------------------------------------------------------------------------- + + + + ... + ... + <1> + <2> + +---------------------------------------------------------------------------------------- +<1> All `GET` methods starting with `/v2` will get queued to `TXNMGR.2` instead of the + standard `TXNMGR` queue. +<2> Likewise, `POST` starting with `/v2` will get queued to the `TXNMGR.2` too. + + The TransactionManager is configured like this: [source,xml] @@ -81,7 +100,6 @@ The TransactionManager is configured like this: - .. .. @@ -97,6 +115,21 @@ The TransactionManager is configured like this: ---------------------------------------------------------------------------------------- <1> This route is special, see below, route processing gets delegated to the Q2Info class +In situations where multiple routes are defined at the QRest server configuration, +classes like `Q2Info` that internally process routes may need to know about the +prefix in use. This can be configured using the `prefix` property, i.e.: + + +[source,xml] +---------------------------------------------------------------------------------------- + + + <1> + + +---------------------------------------------------------------------------------------- +<1> `prefix` property should match the route's prefix + [TIP] ===== This old link:http://jpos.org/blog/2013/10/eating-our-own-dogfood/[Blog Post] explained how diff --git a/modules/qrest/src/dist/deploy/50_qrest.xml b/modules/qrest/src/dist/deploy/50_qrest.xml index 20877dbed1..be82e8ac61 100644 --- a/modules/qrest/src/dist/deploy/50_qrest.xml +++ b/modules/qrest/src/dist/deploy/50_qrest.xml @@ -28,4 +28,6 @@ --> + + diff --git a/modules/qrest/src/main/java/org/jpos/qrest/RestServer.java b/modules/qrest/src/main/java/org/jpos/qrest/RestServer.java index 2f14c9b677..80d8939eaf 100644 --- a/modules/qrest/src/main/java/org/jpos/qrest/RestServer.java +++ b/modules/qrest/src/main/java/org/jpos/qrest/RestServer.java @@ -22,20 +22,25 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.BindException; +import java.net.URI; import java.security.*; -import java.util.Arrays; +import java.util.*; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleStateHandler; +import org.jdom2.Element; import org.jpos.core.Configuration; import org.jpos.core.ConfigurationException; +import org.jpos.core.XmlConfigurable; import org.jpos.iso.ISOUtil; import org.jpos.q2.QBeanSupport; import org.jpos.space.Space; @@ -47,7 +52,11 @@ import javax.net.ssl.*; -public class RestServer extends QBeanSupport implements Runnable { +import static org.jpos.qrest.Constants.PATHPARAMS; +import static org.jpos.qrest.Constants.QUERYPARAMS; +import static org.jpos.qrest.Constants.REQUEST; + +public class RestServer extends QBeanSupport implements Runnable, XmlConfigurable { private ServerBootstrap serverBootstrap; private ChannelFuture cf; private EventLoopGroup bossGroup; @@ -64,6 +73,9 @@ public class RestServer extends QBeanSupport implements Runnable { private boolean clientAuthNeeded=false; private String[] enabledCipherSuites; private String[] enabledProtocols; + private String queue; + private Map>> routes = new HashMap<>(); + @Override protected void initService() throws GeneralSecurityException, IOException { @@ -144,8 +156,9 @@ public void run() { } } - public void queue (Context ctx) { - sp.out(cfg.get("queue"), ctx, 60000L); + @SuppressWarnings("unchecked") + public void queue (FullHttpRequest request, Context ctx) { + sp.out(getQueue(request), ctx, 60000L); } @Override @@ -159,6 +172,25 @@ public void setConfiguration (Configuration cfg) throws ConfigurationException { serverAuthNeeded = cfg.getBoolean("server-auth", false); enabledCipherSuites = cfg.getAll("enabled-cipher"); enabledProtocols = cfg.getAll("enable-protocol"); + queue = cfg.get("queue"); + } + + @Override + public void setConfiguration(Element e) throws ConfigurationException { + try { + for (Element r : e.getChildren("route")) { + routes.computeIfAbsent( + r.getAttributeValue("method"), + k -> new ArrayList<>()).add( + new Route<>( + r.getAttributeValue("path"), + r.getAttributeValue("method"), + (t, s) -> r.getAttributeValue("queue")) + ); + } + } catch (Throwable t) { + throw new ConfigurationException(t); + } } private SSLEngine getSSLEngine(SSLContext sslContext) throws IOException { @@ -234,4 +266,18 @@ protected String getPassword() { protected String getKeyPassword() { return System.getProperty("jpos.ssl.keypass", null); } + + private String getQueue(FullHttpRequest request) { + List> routesByMethod = routes.get(request.method().name()); + QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); + + if (routesByMethod != null) { + Optional> route = routesByMethod.stream().filter(r -> r.matches(decoder.uri())).findFirst(); + String path = URI.create(decoder.uri()).getPath(); + if (route.isPresent()) { + return route.get().apply(route.get(), path); + } + } + return cfg.get("queue"); + } } diff --git a/modules/qrest/src/main/java/org/jpos/qrest/RestSession.java b/modules/qrest/src/main/java/org/jpos/qrest/RestSession.java index 9c8734f124..36bc3171f4 100644 --- a/modules/qrest/src/main/java/org/jpos/qrest/RestSession.java +++ b/modules/qrest/src/main/java/org/jpos/qrest/RestSession.java @@ -48,7 +48,7 @@ public void channelRead(ChannelHandlerContext ch, Object msg) throws Exception { ctx.put(Constants.REQUEST, request); if (contentKey != null) ctx.put(contentKey, request.content().toString(CharsetUtil.UTF_8)); - server.queue(ctx); + server.queue(request, ctx); } else { super.channelRead(ch, msg); } diff --git a/modules/qrest/src/main/java/org/jpos/qrest/Route.java b/modules/qrest/src/main/java/org/jpos/qrest/Route.java index 98e0080890..072ac9e022 100644 --- a/modules/qrest/src/main/java/org/jpos/qrest/Route.java +++ b/modules/qrest/src/main/java/org/jpos/qrest/Route.java @@ -40,7 +40,6 @@ public Route(String path, String method, BiFunction, String,T> supplier Objects.requireNonNull(path); Objects.requireNonNull(method); Objects.requireNonNull(supplier); - this.path = path; this.pathPattern = buildPathPattern(path); this.method = method; diff --git a/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.java b/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.java index 14d1c6dcd8..50535e9ae6 100644 --- a/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.java +++ b/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.java @@ -25,6 +25,9 @@ import java.util.stream.Collectors; import io.netty.handler.codec.http.*; +import org.jpos.core.Configurable; +import org.jpos.core.Configuration; +import org.jpos.core.ConfigurationException; import org.jpos.q2.Q2; import org.jpos.q2.iso.QMUX; import org.jpos.qrest.Response; @@ -32,24 +35,24 @@ import org.jpos.transaction.Context; import org.jpos.transaction.TransactionManager; import org.jpos.transaction.TransactionParticipant; +import org.jpos.util.Caller; import org.jpos.util.NameRegistrar; import static org.jpos.qrest.Constants.REQUEST; import static org.jpos.qrest.Constants.RESPONSE; -public class Q2Info implements TransactionParticipant { +public class Q2Info implements TransactionParticipant, Configurable { private TransactionManager txnmgr; private Q2 q2; private List>> routes = new ArrayList<>(); + private String prefix; - public Q2Info() { - initInternalRoutes(); - } + public Q2Info() { } @Override public int prepare(long id, Serializable context) { Context ctx = (Context) context; - FullHttpRequest request = (FullHttpRequest) ctx.get(REQUEST); + FullHttpRequest request = ctx.get(REQUEST); QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); Map response = null; Optional>> route = routes.stream().filter(rr -> rr.matches(decoder.uri())).findFirst(); @@ -86,6 +89,12 @@ public void setTransactionManager (TransactionManager txnmgr) { this.q2 = txnmgr.getServer(); } + @Override + public void setConfiguration(Configuration cfg) { + this.prefix = cfg.get("prefix"); + initInternalRoutes(); + } + private Map muxes () { List muxes = NameRegistrar.getAsMap().entrySet() .stream().filter(e -> e.getValue() instanceof QMUX && e.getKey().startsWith("mux.")) @@ -155,15 +164,16 @@ private Map txnmgrInfo(TransactionManager txnmgr) { private void initInternalRoutes() { - routes.add(new Route<>("/q2/version**", "GET", (t,s) -> mapOf ("version", q2Version()))); - routes.add(new Route<>("/q2/applicationVersion**", "GET", (t,s) -> mapOf("applicationVersion", Q2.getAppVersionString()))); - routes.add(new Route<>("/q2/instanceId**", "GET", (t,s) -> mapOf("instanceId", q2.getInstanceId()))); - routes.add(new Route<>("/q2/uptime**", "GET", (t,s) -> mapOf("uptime", q2.getUptime()))); - routes.add(new Route<>("/q2/started**", "GET", (t,s) -> mapOf("started", new Date(System.currentTimeMillis() - q2.getUptime())))); - routes.add(new Route<>("/q2/diskspace**", "GET", (t,s) -> diskspace())); - routes.add(new Route<>("/q2/mux/{muxname}**", "GET", (t,s) -> muxInfo(t,s))); - routes.add(new Route<>("/q2/mux**", "GET", (t,s) -> muxes())); - routes.add(new Route<>("/q2/txnmgr**", "GET", (t,s) -> txnmgr())); + routes.add(new Route<>(prefix + "/q2/version**", "GET", (t,s) -> mapOf ("version", q2Version()))); + routes.add(new Route<>(prefix + "/q2/applicationVersion**", "GET", (t,s) -> mapOf("applicationVersion", Q2.getAppVersionString()))); + routes.add(new Route<>(prefix + "/q2/instanceId**", "GET", (t,s) -> mapOf("instanceId", q2.getInstanceId()))); + routes.add(new Route<>(prefix + "/q2/uptime**", "GET", (t,s) -> mapOf("uptime", q2.getUptime()))); + routes.add(new Route<>(prefix + "/q2/started**", "GET", (t,s) -> mapOf("started", new Date(System.currentTimeMillis() - q2.getUptime())))); + routes.add(new Route<>(prefix + "/q2/diskspace**", "GET", (t,s) -> diskspace())); + routes.add(new Route<>(prefix + "/q2/mux/{muxname}**", "GET", (t,s) -> muxInfo(t,s))); + routes.add(new Route<>(prefix + "/q2/mux**", "GET", (t,s) -> muxes())); + routes.add(new Route<>(prefix + "/q2/txnmgr/name", "GET", (t,s) -> mapOf("name", txnmgr.getName()))); + routes.add(new Route<>(prefix + "/q2/txnmgr**", "GET", (t,s) -> txnmgr())); } private Map newMap () { @@ -186,4 +196,5 @@ private Map diskspace() { m.put("usable", f.getUsableSpace()); return mapOf("diskspace", m); } + } diff --git a/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.new b/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.new new file mode 100644 index 0000000000..c415d1db7e --- /dev/null +++ b/modules/qrest/src/main/java/org/jpos/qrest/participant/Q2Info.new @@ -0,0 +1,200 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2019 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.qrest.participant; + +import java.io.File; +import java.io.Serializable; +import java.net.URI; +import java.util.*; +import java.util.stream.Collectors; + +import io.netty.handler.codec.http.*; +import org.jpos.core.Configurable; +import org.jpos.core.Configuration; +import org.jpos.core.ConfigurationException; +import org.jpos.q2.Q2; +import org.jpos.q2.iso.QMUX; +import org.jpos.qrest.Response; +import org.jpos.qrest.Route; +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionManager; +import org.jpos.transaction.TransactionParticipant; +import org.jpos.util.NameRegistrar; + +import static org.jpos.qrest.Constants.REQUEST; +import static org.jpos.qrest.Constants.RESPONSE; + +public class Q2Info implements TransactionParticipant, Configurable { + private TransactionManager txnmgr; + private Q2 q2; + private List>> routes = new ArrayList<>(); + private String prefix; + + public Q2Info() { + initInternalRoutes(); + } + + @Override + public int prepare(long id, Serializable context) { + Context ctx = (Context) context; + FullHttpRequest request = ctx.get(REQUEST); + QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); + Map response = null; + Optional>> route = routes.stream().filter(rr -> rr.matches(decoder.uri())).findFirst(); + String path = URI.create(decoder.uri()).getPath(); + + HttpResponseStatus status = HttpResponseStatus.NOT_FOUND; + if (route.isPresent()) { + if (route.get().isValid(path)) { + response = new LinkedHashMap<>(); + response.putAll(route.get().apply(route.get(), path)); + status = HttpResponseStatus.OK; + } + } else { + response = new LinkedHashMap<>(); + for (Route> r : routes) { + if (!r.hasPathParams()) + response.putAll(r.apply(r, path)); + } + status = HttpResponseStatus.OK; + } + if (response != null && response.containsKey("error")) { + status = HttpResponseStatus.NOT_ACCEPTABLE; + } + ctx.put(RESPONSE, + response != null ? new Response(status, response) : + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status) + ); + return PREPARED | NO_JOIN | READONLY; + } + + @SuppressWarnings("unused") + public void setTransactionManager (TransactionManager txnmgr) { + this.txnmgr = txnmgr; + this.q2 = txnmgr.getServer(); + } + + @Override + public void setConfiguration(Configuration cfg) { + this.prefix = cfg.get("prefix"); + } + + private Map muxes () { + List muxes = NameRegistrar.getAsMap().entrySet() + .stream().filter(e -> e.getValue() instanceof QMUX && e.getKey().startsWith("mux.")) + .map(e -> muxInfo((QMUX) e.getValue())) + .collect(Collectors.toList()); + + Map wrapper = new HashMap<>(); + wrapper.put("mux", muxes); + return wrapper; + } + + private Map muxInfo(QMUX mux) { + Map m = new LinkedHashMap<>(); + m.put ("name", mux.getName()); + m.put ("type", mux.getClass().getSimpleName()); + m.put ("connected", mux.isConnected()); + m.put ("rx", mux.getRXCounter()); + m.put ("tx", mux.getTXCounter()); + m.put ("txExpired", mux.getTXExpired()); + m.put ("txPending", mux.getTXPending()); + m.put ("rxExpired", mux.getRXExpired()); + m.put ("rxPending", mux.getRXPending()); + m.put ("rxUnhandled", mux.getRXUnhandled()); + m.put ("rxForwarded", mux.getRXForwarded()); + m.put ("metrics", mux.getMetrics().metrics()); + long last = mux.getLastTxnTimestampInMillis(); + if (last > 0) { + m.put("last", new Date(last)); + m.put("idle", mux.getIdleTimeInMillis()); + } + return m; + } + + private Map muxInfo (Route r, String path) { + try { + return muxInfo((QMUX) QMUX.getMUX((String) r.parameters(path).get("muxname"))); + } catch (NameRegistrar.NotFoundException e) { + return mapOf("error", e.getMessage() + " not found"); + } + } + + private Map txnmgr () { + List txnmgr = NameRegistrar.getAsMap().entrySet() + .stream().filter(e -> e.getValue() instanceof TransactionManager) + .map(e -> txnmgrInfo((TransactionManager) e.getValue())) + .collect(Collectors.toList()); + + Map wrapper = new HashMap<>(); + wrapper.put("txnmgr", txnmgr); + return wrapper; + } + + private Map txnmgrInfo(TransactionManager txnmgr) { + Map m = new LinkedHashMap<>(); + m.put ("name", txnmgr.getName()); + m.put ("type", txnmgr.getClass().getSimpleName()); + m.put ("tail", txnmgr.getTail()); + m.put ("head", txnmgr.getHead()); + m.put ("inTransit", txnmgr.getInTransit()); + m.put ("TPSAvg", txnmgr.getTPSAvg()); + m.put ("TPSPeak", txnmgr.getTPSPeak()); + m.put ("TPSPeakWhen", txnmgr.getTPSPeakWhen()); + m.put ("TPSElapsed", txnmgr.getTPSElapsed()); + m.put ("metrics", txnmgr.getMetrics().metrics()); + return m; + } + + + private void initInternalRoutes() { + routes.add(new Route<>(prefix + "/q2/version**", "GET", (t,s) -> mapOf ("version", q2Version()))); + routes.add(new Route<>(prefix + "/q2/applicationVersion**", "GET", (t,s) -> mapOf("applicationVersion", Q2.getAppVersionString()))); + routes.add(new Route<>(prefix + "/q2/instanceId**", "GET", (t,s) -> mapOf("instanceId", q2.getInstanceId()))); + routes.add(new Route<>(prefix + "/q2/uptime**", "GET", (t,s) -> mapOf("uptime", q2.getUptime()))); + routes.add(new Route<>(prefix + "/q2/started**", "GET", (t,s) -> mapOf("started", new Date(System.currentTimeMillis() - q2.getUptime())))); + routes.add(new Route<>(prefix + "/q2/diskspace**", "GET", (t,s) -> diskspace())); + routes.add(new Route<>(prefix + "/q2/mux/{muxname}**", "GET", (t,s) -> muxInfo(t,s))); + routes.add(new Route<>(prefix + "/q2/mux**", "GET", (t,s) -> muxes())); + routes.add(new Route<>(prefix + "/q2/txnmgr/name", "GET", (t,s) -> mapOf("name", txnmgr.getName()))); + routes.add(new Route<>(prefix + "/q2/txnmgr**", "GET", (t,s) -> txnmgr())); + } + + private Map newMap () { + return new LinkedHashMap<>(); + } + private Map mapOf (String k, Object v) { + Map m = new LinkedHashMap<>(); + m.put(k, v); + return m; + } + private String q2Version() { + return String.format("jPOS %s %s/%s (%s)", + Q2.getVersion(), Q2.getBranch(), Q2.getRevision(), Q2.getBuildTimestamp() + ); + } + private Map diskspace() { + File f = new File("."); + Map m = newMap(); + m.put("free", f.getFreeSpace()); + m.put("usable", f.getUsableSpace()); + return mapOf("diskspace", m); + } + +} diff --git a/modules/qrest/src/test/java/org/jpos/qrest/RestTest.java b/modules/qrest/src/test/java/org/jpos/qrest/RestTest.java index c551bd272c..81da53fecb 100644 --- a/modules/qrest/src/test/java/org/jpos/qrest/RestTest.java +++ b/modules/qrest/src/test/java/org/jpos/qrest/RestTest.java @@ -88,4 +88,16 @@ public void testPost() { ) )); } + + @Test + public void testMultiplesTMs() { + given().log().all() + .get("/q2/txnmgr/name").then().statusCode(200).assertThat() + .body("name", equalTo("txnmgr") + ); + given().log().all() + .get("/v2/q2/txnmgr/name").then().statusCode(200).assertThat() + .body("name", equalTo("txnmgr2") + ); + } }