Javalin process-based custom router; javalin meta-data to define routers

This commit is contained in:
2025-01-30 19:13:32 -06:00
parent 6d749e9df6
commit bcca710316
5 changed files with 512 additions and 1 deletions

View File

@ -22,9 +22,11 @@
package com.kingsrook.qqq.backend.javalin;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import org.apache.logging.log4j.Level;
@ -46,7 +48,8 @@ public class QJavalinMetaData implements QSupplementalInstanceMetaData
private Integer queryWithoutLimitDefault = 1000;
private Level queryWithoutLimitLogLevel = Level.INFO;
// todo - list of objects with hosted path, file-system paths
private List<JavalinRouteProviderMetaData> routeProviders;
/***************************************************************************
@ -278,4 +281,35 @@ public class QJavalinMetaData implements QSupplementalInstanceMetaData
return (this);
}
/*******************************************************************************
** Getter for routeProviders
*******************************************************************************/
public List<JavalinRouteProviderMetaData> getRouteProviders()
{
return (this.routeProviders);
}
/*******************************************************************************
** Setter for routeProviders
*******************************************************************************/
public void setRouteProviders(List<JavalinRouteProviderMetaData> routeProviders)
{
this.routeProviders = routeProviders;
}
/*******************************************************************************
** Fluent setter for routeProviders
*******************************************************************************/
public QJavalinMetaData withRouteProviders(List<JavalinRouteProviderMetaData> routeProviders)
{
this.routeProviders = routeProviders;
return (this);
}
}

View File

@ -32,8 +32,13 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
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.QJavalinMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouter;
import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.Javalin;
@ -99,6 +104,12 @@ public class QApplicationJavalinServer
{
QInstance qInstance = application.defineValidatedQInstance();
QJavalinMetaData qJavalinMetaData = QJavalinMetaData.of(qInstance);
if(qJavalinMetaData != null)
{
addRouteProvidersFromMetaData(qJavalinMetaData);
}
service = Javalin.create(config ->
{
if(serveFrontendMaterialDashboard)
@ -205,6 +216,35 @@ public class QApplicationJavalinServer
/***************************************************************************
**
***************************************************************************/
private void addRouteProvidersFromMetaData(QJavalinMetaData qJavalinMetaData) throws QException
{
if(qJavalinMetaData == null)
{
return;
}
for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(qJavalinMetaData.getRouteProviders()))
{
if(StringUtils.hasContent(routeProviderMetaData.getProcessName()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath()))
{
withAdditionalRouteProvider(new ProcessBasedRouter(routeProviderMetaData));
}
else if(StringUtils.hasContent(routeProviderMetaData.getFileSystemPath()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath()))
{
withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter(routeProviderMetaData));
}
else
{
throw (new QException("Error processing route provider - does not have sufficient fields set."));
}
}
}
/***************************************************************************
**
***************************************************************************/

View File

@ -0,0 +1,175 @@
/*
* 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.metadata;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
**
*******************************************************************************/
public class JavalinRouteProviderMetaData implements QMetaDataObject
{
private String hostedPath;
private String fileSystemPath;
private String processName;
private List<String> methods;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public JavalinRouteProviderMetaData()
{
}
/*******************************************************************************
** Getter for hostedPath
*******************************************************************************/
public String getHostedPath()
{
return (this.hostedPath);
}
/*******************************************************************************
** Setter for hostedPath
*******************************************************************************/
public void setHostedPath(String hostedPath)
{
this.hostedPath = hostedPath;
}
/*******************************************************************************
** Fluent setter for hostedPath
*******************************************************************************/
public JavalinRouteProviderMetaData withHostedPath(String hostedPath)
{
this.hostedPath = hostedPath;
return (this);
}
/*******************************************************************************
** Getter for fileSystemPath
*******************************************************************************/
public String getFileSystemPath()
{
return (this.fileSystemPath);
}
/*******************************************************************************
** Setter for fileSystemPath
*******************************************************************************/
public void setFileSystemPath(String fileSystemPath)
{
this.fileSystemPath = fileSystemPath;
}
/*******************************************************************************
** Fluent setter for fileSystemPath
*******************************************************************************/
public JavalinRouteProviderMetaData withFileSystemPath(String fileSystemPath)
{
this.fileSystemPath = fileSystemPath;
return (this);
}
/*******************************************************************************
** Getter for processName
*******************************************************************************/
public String getProcessName()
{
return (this.processName);
}
/*******************************************************************************
** Setter for processName
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
*******************************************************************************/
public JavalinRouteProviderMetaData withProcessName(String processName)
{
this.processName = processName;
return (this);
}
/*******************************************************************************
** Getter for methods
*******************************************************************************/
public List<String> getMethods()
{
return (this.methods);
}
/*******************************************************************************
** Setter for methods
*******************************************************************************/
public void setMethods(List<String> methods)
{
this.methods = methods;
}
/*******************************************************************************
** Fluent setter for methods
*******************************************************************************/
public JavalinRouteProviderMetaData withMethods(List<String> methods)
{
this.methods = methods;
return (this);
}
}

View File

@ -0,0 +1,251 @@
/*
* 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;
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.processes.RunProcessAction;
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.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
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 io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessBasedRouter implements QJavalinRouteProviderInterface
{
private static final QLogger LOG = QLogger.getLogger(ProcessBasedRouter.class);
private final String hostedPath;
private final String processName;
private final List<String> methods;
private QInstance qInstance;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessBasedRouter(String hostedPath, String processName)
{
this(hostedPath, processName, null);
}
/***************************************************************************
**
***************************************************************************/
public ProcessBasedRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods());
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessBasedRouter(String hostedPath, String processName, List<String> methods)
{
this.hostedPath = hostedPath;
this.processName = processName;
if(CollectionUtils.nullSafeHasContents(methods))
{
this.methods = methods;
}
else
{
this.methods = List.of("GET");
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setQInstance(QInstance qInstance)
{
this.qInstance = qInstance;
}
/***************************************************************************
**
***************************************************************************/
@Override
public EndpointGroup getJavalinEndpointGroup()
{
return (() ->
{
for(String method : methods)
{
switch(method.toLowerCase())
{
case "get" -> ApiBuilder.get(hostedPath, this::handleRequest);
case "post" -> ApiBuilder.post(hostedPath, this::handleRequest);
case "put" -> ApiBuilder.put(hostedPath, this::handleRequest);
case "patch" -> ApiBuilder.patch(hostedPath, this::handleRequest);
case "delete" -> ApiBuilder.delete(hostedPath, this::handleRequest);
default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
}
}
});
}
/***************************************************************************
**
***************************************************************************/
private void handleRequest(Context context)
{
RunProcessInput input = new RunProcessInput();
input.setProcessName(processName);
try
{
QJavalinImplementation.setupSession(context, input);
}
catch(Exception e)
{
context.header("WWW-Authenticate", "Basic realm=\"Access to this QQQ site\"");
context.status(HttpStatus.UNAUTHORIZED);
return;
}
/*
boolean authorized = false;
String authorization = context.header("Authorization");
if(authorization != null && authorization.matches("^Basic .+"))
{
String base64Authorization = authorization.substring("Basic ".length());
String decoded = new String(Base64.getDecoder().decode(base64Authorization), StandardCharsets.UTF_8);
String[] parts = decoded.split(":", 2);
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
}
if(!authorized)
{
}
// todo - not always system-user session!!
QContext.init(this.qInstance, new QSystemUserSession());
*/
try
{
LOG.info("Running [" + processName + "] to serve [" + context.path() + "]...");
/////////////////////
// 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()));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
/////////////////
// status code //
/////////////////
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
if(statusCode != null)
{
context.status(statusCode);
}
/////////////////
// 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)));
}
}
// todo - make the inputStream available to the process
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
// context.resultInputStream();
///////////////////
// response body //
///////////////////
Serializable response = runProcessOutput.getValue("response");
if(response instanceof String s)
{
context.result(s);
}
else if(response instanceof byte[] ba)
{
context.result(ba);
}
else if(response instanceof InputStream is)
{
context.result(is);
}
else
{
context.result(ValueUtils.getValueAsString(response));
}
}
catch(Exception e)
{
QJavalinUtils.handleException(null, context, e);
}
finally
{
QContext.clear();
}
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.middleware.javalin.routeproviders;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import io.javalin.config.JavalinConfig;
import io.javalin.http.staticfiles.Location;
import io.javalin.http.staticfiles.StaticFileConfig;
@ -52,6 +53,16 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
/***************************************************************************
**
***************************************************************************/
public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath());
}
/***************************************************************************
**
***************************************************************************/