From 1808cea5c0a6675db3aa76646ea5829d92101910 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 12 Jun 2025 20:28:42 -0500 Subject: [PATCH] Update processBasedRouters to use different handlers for processing the javalin context - with a new default implementation that makes available the request body as a string --- .../qqq/backend/javalin/QJavalinMetaData.java | 14 ++ .../JavalinRouteProviderMetaData.java | 93 ++++++++++ .../routeproviders/ProcessBasedRouter.java | 134 ++++++++------- .../ProcessBasedRouterPayload.java | 32 ++++ .../DefaultRouteProviderContextHandler.java | 159 ++++++++++++++++++ .../RouteProviderContextHandlerInterface.java | 49 ++++++ 6 files changed, 413 insertions(+), 68 deletions(-) create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/DefaultRouteProviderContextHandler.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/RouteProviderContextHandlerInterface.java diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java index 3c1ea8df..eda20c47 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java @@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.javalin; import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; import org.apache.logging.log4j.Level; @@ -329,4 +331,16 @@ public class QJavalinMetaData implements QSupplementalInstanceMetaData } + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void validate(QInstance qInstance, QInstanceValidator validator) + { + for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(routeProviders)) + { + routeProviderMetaData.validate(qInstance, validator); + } + } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java index 82c09173..88f2b640 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java @@ -23,8 +23,13 @@ package com.kingsrook.qqq.middleware.javalin.metadata; import java.util.List; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface; +import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.RouteProviderContextHandlerInterface; /******************************************************************************* @@ -32,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; *******************************************************************************/ public class JavalinRouteProviderMetaData implements QMetaDataObject { + private String name; private String hostedPath; private String fileSystemPath; @@ -40,6 +46,7 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject private List methods; private QCodeReference routeAuthenticator; + private QCodeReference contextHandler; @@ -206,4 +213,90 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject return (this); } + + + /******************************************************************************* + ** Getter for contextHandler + *******************************************************************************/ + public QCodeReference getContextHandler() + { + return (this.contextHandler); + } + + + + /******************************************************************************* + ** Setter for contextHandler + *******************************************************************************/ + public void setContextHandler(QCodeReference contextHandler) + { + this.contextHandler = contextHandler; + } + + + + /******************************************************************************* + ** Fluent setter for contextHandler + *******************************************************************************/ + public JavalinRouteProviderMetaData withContextHandler(QCodeReference contextHandler) + { + this.contextHandler = contextHandler; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public JavalinRouteProviderMetaData withName(String name) + { + this.name = name; + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void validate(QInstance qInstance, QInstanceValidator validator) + { + String prefix = "In javalinRouteProvider '" + name + "', "; + if(StringUtils.hasContent(processName)) + { + validator.assertCondition(qInstance.getProcesses().containsKey(processName), prefix + "unrecognized process name: " + processName + " in a javalinRouteProvider"); + } + + if(routeAuthenticator != null) + { + validator.validateSimpleCodeReference(prefix + "routeAuthenticator ", routeAuthenticator, RouteAuthenticatorInterface.class); + } + + if(contextHandler != null) + { + validator.validateSimpleCodeReference(prefix + "contextHandler ", contextHandler, RouteProviderContextHandlerInterface.class); + } + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java index 02dad90b..6ffbdbba 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java @@ -22,34 +22,27 @@ package com.kingsrook.qqq.middleware.javalin.routeproviders; -import java.io.InputStream; -import java.io.Serializable; -import java.util.HashMap; import java.util.List; -import java.util.Map; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; -import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; -import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import com.kingsrook.qqq.backend.javalin.QJavalinUtils; import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface; import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface; +import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.DefaultRouteProviderContextHandler; +import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.RouteProviderContextHandlerInterface; import io.javalin.apibuilder.ApiBuilder; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; -import io.javalin.http.HttpStatus; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -65,6 +58,7 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface private final List methods; private QCodeReference routeAuthenticator; + private QCodeReference contextHandler; private QInstance qInstance; @@ -88,6 +82,7 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface { this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods()); setRouteAuthenticator(routeProvider.getRouteAuthenticator()); + setContextHandler(routeProvider.getContextHandler()); } @@ -188,72 +183,27 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface { LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path())); + ////////////////////////////////////////////////////////////////////////////////////// + // handle request (either using route's specific context handler, or a default one) // + ////////////////////////////////////////////////////////////////////////////////////// + RouteProviderContextHandlerInterface contextHandler = createContextHandler(); + contextHandler.handleRequest(context, input); + + // todo - make the inputStream available to the process to stream results? + // maybe via the callback object??? input.setCallback(new QProcessCallback() {}); + // context.resultInputStream(); + ///////////////////// // run the process // ///////////////////// input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); - input.addValue("path", context.path()); - input.addValue("method", context.method()); - input.addValue("pathParams", new HashMap<>(context.pathParamMap())); - input.addValue("queryParams", new HashMap<>(context.queryParamMap())); - input.addValue("formParams", new HashMap<>(context.formParamMap())); - input.addValue("cookies", new HashMap<>(context.cookieMap())); - input.addValue("requestHeaders", new HashMap<>(context.headerMap())); - RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); - ///////////////// - // headers map // - ///////////////// - Serializable headers = runProcessOutput.getValue("responseHeaders"); - if(headers instanceof Map headersMap) + ///////////////////// + // handle response // + ///////////////////// + if(contextHandler.handleResponse(context, runProcessOutput)) { - for(Object key : headersMap.keySet()) - { - context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key))); - } - } - - // todo - make the inputStream available to the process - // maybe via the callback object??? input.setCallback(new QProcessCallback() {}); - // context.resultInputStream(); - - ////////////// - // response // - ////////////// - Integer statusCode = runProcessOutput.getValueInteger("statusCode"); - String redirectURL = runProcessOutput.getValueString("redirectURL"); - String responseString = runProcessOutput.getValueString("responseString"); - byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes"); - StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput"); - - if(StringUtils.hasContent(redirectURL)) - { - context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode)); - return; - } - - if(statusCode != null) - { - context.status(statusCode); - } - - if(StringUtils.hasContent(responseString)) - { - context.result(responseString); - return; - } - - if(responseBytes != null && responseBytes.length > 0) - { - context.result(responseBytes); - return; - } - - if(responseStorageInput != null) - { - InputStream inputStream = new StorageAction().getInputStream(responseStorageInput); - context.result(inputStream); return; } @@ -271,6 +221,23 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface + /*************************************************************************** + ** + ***************************************************************************/ + private RouteProviderContextHandlerInterface createContextHandler() + { + if(contextHandler != null) + { + return QCodeLoader.getAdHoc(RouteProviderContextHandlerInterface.class, this.contextHandler); + } + else + { + return (new DefaultRouteProviderContextHandler()); + } + } + + + /******************************************************************************* ** Getter for routeAuthenticator *******************************************************************************/ @@ -300,4 +267,35 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface return (this); } + + /******************************************************************************* + ** Getter for contextHandler + *******************************************************************************/ + public QCodeReference getContextHandler() + { + return (this.contextHandler); + } + + + + /******************************************************************************* + ** Setter for contextHandler + *******************************************************************************/ + public void setContextHandler(QCodeReference contextHandler) + { + this.contextHandler = contextHandler; + } + + + + /******************************************************************************* + ** Fluent setter for contextHandler + *******************************************************************************/ + public ProcessBasedRouter withContextHandler(QCodeReference contextHandler) + { + this.contextHandler = contextHandler; + return (this); + } + + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java index 8d754469..d0ae1fd7 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java @@ -41,6 +41,7 @@ public class ProcessBasedRouterPayload extends QProcessPayload private Map> queryParams; private Map> formParams; private Map cookies; + private String bodyString; private Integer statusCode; private String redirectURL; @@ -410,4 +411,35 @@ public class ProcessBasedRouterPayload extends QProcessPayload return (this); } + + /******************************************************************************* + ** Getter for bodyString + *******************************************************************************/ + public String getBodyString() + { + return (this.bodyString); + } + + + + /******************************************************************************* + ** Setter for bodyString + *******************************************************************************/ + public void setBodyString(String bodyString) + { + this.bodyString = bodyString; + } + + + + /******************************************************************************* + ** Fluent setter for bodyString + *******************************************************************************/ + public ProcessBasedRouterPayload withBodyString(String bodyString) + { + this.bodyString = bodyString; + return (this); + } + + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/DefaultRouteProviderContextHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/DefaultRouteProviderContextHandler.java new file mode 100644 index 00000000..efe4facd --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/DefaultRouteProviderContextHandler.java @@ -0,0 +1,159 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * 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 com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers; + + +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import io.javalin.http.Context; +import io.javalin.http.HttpStatus; + + +/******************************************************************************* + ** default implementation of this interface. reads the request body as a string + *******************************************************************************/ +public class DefaultRouteProviderContextHandler implements RouteProviderContextHandlerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleRequest(Context context, RunProcessInput input) + { + input.addValue("path", context.path()); + input.addValue("method", context.method()); + input.addValue("pathParams", new HashMap<>(context.pathParamMap())); + input.addValue("queryParams", new HashMap<>(context.queryParamMap())); + input.addValue("cookies", new HashMap<>(context.cookieMap())); + input.addValue("requestHeaders", new HashMap<>(context.headerMap())); + + handleRequestBody(context, input); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void handleRequestBody(Context context, RunProcessInput input) + { + input.addValue("formParams", new HashMap<>(context.formParamMap())); + input.addValue("bodyString", context.body()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean handleResponse(Context context, RunProcessOutput runProcessOutput) throws QException + { + handleResponseHeaders(context, runProcessOutput); + + ////////////// + // response // + ////////////// + Integer statusCode = runProcessOutput.getValueInteger("statusCode"); + String redirectURL = runProcessOutput.getValueString("redirectURL"); + + if(StringUtils.hasContent(redirectURL)) + { + context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode)); + return true; + } + + if(statusCode != null) + { + context.status(statusCode); + } + + if(handleResponseBody(context, runProcessOutput)) + { + return true; + } + + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void handleResponseHeaders(Context context, RunProcessOutput runProcessOutput) + { + ///////////////// + // headers map // + ///////////////// + Serializable headers = runProcessOutput.getValue("responseHeaders"); + if(headers instanceof Map headersMap) + { + for(Object key : headersMap.keySet()) + { + context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key))); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected boolean handleResponseBody(Context context, RunProcessOutput runProcessOutput) throws QException + { + String responseString = runProcessOutput.getValueString("responseString"); + byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes"); + StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput"); + if(StringUtils.hasContent(responseString)) + { + context.result(responseString); + return true; + } + + if(responseBytes != null && responseBytes.length > 0) + { + context.result(responseBytes); + return true; + } + + if(responseStorageInput != null) + { + InputStream inputStream = new StorageAction().getInputStream(responseStorageInput); + context.result(inputStream); + return true; + } + return false; + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/RouteProviderContextHandlerInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/RouteProviderContextHandlerInterface.java new file mode 100644 index 00000000..aaa9d490 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/contexthandlers/RouteProviderContextHandlerInterface.java @@ -0,0 +1,49 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * 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 com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import io.javalin.http.Context; + + +/******************************************************************************* + ** interface for how to handle the javalin context for a process based route provider. + ** e.g., taking things like query params and the request body into the process input + ** and similarly for the http response from the process output.. + *******************************************************************************/ +public interface RouteProviderContextHandlerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + void handleRequest(Context context, RunProcessInput runProcessInput); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean handleResponse(Context context, RunProcessOutput runProcessOutput) throws QException; + +}