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

This commit is contained in:
2025-06-12 20:28:42 -05:00
parent a7b5e00e27
commit 1808cea5c0
6 changed files with 413 additions and 68 deletions

View File

@ -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);
}
}
}

View File

@ -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<String> 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);
}
}
}

View File

@ -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<String> 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);
}
}

View File

@ -41,6 +41,7 @@ public class ProcessBasedRouterPayload extends QProcessPayload
private Map<String, List<String>> queryParams;
private Map<String, List<String>> formParams;
private Map<String, String> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}