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 9c4bd1f8..9383b1d6 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 @@ -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 routeProviders; + /*************************************************************************** @@ -278,4 +281,35 @@ public class QJavalinMetaData implements QSupplementalInstanceMetaData return (this); } + + + /******************************************************************************* + ** Getter for routeProviders + *******************************************************************************/ + public List getRouteProviders() + { + return (this.routeProviders); + } + + + + /******************************************************************************* + ** Setter for routeProviders + *******************************************************************************/ + public void setRouteProviders(List routeProviders) + { + this.routeProviders = routeProviders; + } + + + + /******************************************************************************* + ** Fluent setter for routeProviders + *******************************************************************************/ + public QJavalinMetaData withRouteProviders(List routeProviders) + { + this.routeProviders = routeProviders; + return (this); + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java index 78bd118a..9ad8d1f8 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java @@ -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.")); + } + } + } + + + /*************************************************************************** ** ***************************************************************************/ 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 new file mode 100644 index 00000000..3b0e50c6 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java @@ -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 . + */ + +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 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 getMethods() + { + return (this.methods); + } + + + + /******************************************************************************* + ** Setter for methods + *******************************************************************************/ + public void setMethods(List methods) + { + this.methods = methods; + } + + + + /******************************************************************************* + ** Fluent setter for methods + *******************************************************************************/ + public JavalinRouteProviderMetaData withMethods(List methods) + { + this.methods = methods; + return (this); + } + +} 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 new file mode 100644 index 00000000..00ae3abc --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java @@ -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 . + */ + +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 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 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(); + } + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java index ae22a981..ac16e283 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java @@ -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()); + } + + + /*************************************************************************** ** ***************************************************************************/