702fafc1 by Ean Schuessler

Enhance screen rendering with CustomScreenTestImpl and improved WebFacadeStub

- Add CustomScreenTestImpl for better screen path handling
- Fix WebFacadeStub to use actual screenPath instead of hardcoded '/test'
- Improve screen discovery and execution in McpServices
- Support standalone screens and proper root screen detection
- Add timeout protection for screen rendering
1 parent 5618521e
......@@ -435,22 +435,6 @@
} else {
ec.logger.info("Found original screen path for tool ${name}: ${screenPath}")
}
/*
// Map common screen patterns to actual screen locations
if (screenPath.startsWith("OrderFind") || screenPath.startsWith("ProductFind") || screenPath.startsWith("PartyFind")) {
// For find screens, try to use appropriate component screens
if (screenPath.startsWith("Order")) {
screenPath = "webroot/apps/order" // Try to map to order app
} else if (screenPath.startsWith("Product")) {
screenPath = "webroot/apps/product" // Try to map to product app
} else if (screenPath.startsWith("Party")) {
screenPath = "webroot/apps/party" // Try to map to party app
}
} else if (screenPath == "apps") {
screenPath = "component://webroot/screen/webroot.xml" // Use full component path to webroot screen
}
*/
// Restore user context from sessionId before calling screen tool
def serviceResult = null
......@@ -483,7 +467,7 @@
// Now call the screen tool with proper user context
def screenParams = arguments ?: [:]
serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
.parameters([screenPath: screenPath, parameters: screenParams, renderMode: "html"])
.parameters([screenPath: screenPath, parameters: screenParams, renderMode: "html", sessionId: sessionId])
.call()
} finally {
......@@ -530,41 +514,6 @@
def originalUsername = ec.user.username
UserInfo adminUserInfo = null
// Timing already started above
// Validate session if provided
/*
if (sessionId) {
def visit = null
adminUserInfo = null
try {
adminUserInfo = ec.user.pushUser("ADMIN")
visit = ec.entity.find("moqui.server.Visit")
.condition("visitId", sessionId)
.one()
} finally {
if (adminUserInfo != null) {
ec.user.popUser()
}
}
// Validate session - allow special MCP case where Visit was created with ADMIN but accessed by MCP_USER or MCP_BUSINESS
boolean sessionValid = false
if (visit) {
if (visit.userId == ec.user.userId) {
sessionValid = true
} else if (visit.userId == "ADMIN" && (ec.user.username == "mcp-user" || ec.user.username == "mcp-business")) {
// Special MCP case: Visit created with ADMIN for privileged access but accessed by MCP users
sessionValid = true
}
}
if (!sessionValid) {
//throw new Exception("Invalid session: ${sessionId}")
}
}
*/
try {
// Execute service with elevated privileges for system access
// but maintain audit context with actual user
......@@ -844,11 +793,48 @@ try {
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.impl.context.UserFacadeImpl.UserInfo
// Get current user information
def currentUser = ec.user.username
def currentUserId = ec.user.userId
// Try to get visit information if sessionId is provided
def visitInfo = null
if (sessionId) {
try {
def adminUserInfo = ec.user.pushUser("ADMIN")
try {
def visit = ec.entity.find("moqui.server.Visit")
.condition("visitId", sessionId)
.disableAuthz()
.one()
if (visit) {
visitInfo = [
visitId: visit.visitId,
userId: visit.userId,
fromDate: visit.fromDate,
lastUpdatedStamp: visit.lastUpdatedStamp
]
}
} finally {
ec.user.popUser()
}
} catch (Exception e) {
// Log but don't fail the ping
ec.logger.warn("Error getting visit info for sessionId ${sessionId}: ${e.message}")
}
}
result = [
timestamp: ec.user.getNowTimestamp(),
status: "healthy",
version: "2.0.2",
sessionId: sessionId,
currentUser: currentUser,
currentUserId: currentUserId,
visitInfo: visitInfo,
architecture: "Visit-based sessions"
]
]]></script>
......@@ -1080,9 +1066,11 @@ try {
try {
screenDefinition = ec.screen.getScreenDefinition(screenPath)
if (screenDefinition?.screenNode?.first("description")?.text) {
title = screenDefinition.screenNode.first("description").text
description = screenDefinition.screenNode.first("description").text
// Screen XML doesn't have description elements, so use screen path as description
// We could potentially use default-menu-title attribute if available
if (screenDefinition?.screenNode?.attribute('default-menu-title')) {
title = screenDefinition.screenNode.attribute('default-menu-title')
description = "Moqui screen: ${screenPath} (${title})"
}
} catch (Exception e) {
ec.logger.info("MCP Screen Discovery: No screen definition for ${screenPath}, using basic info")
......@@ -1170,8 +1158,17 @@ try {
// Extract screen information
def screenName = screenPath.replaceAll("[^a-zA-Z0-9]", "_")
def title = screenDef.getScreenName() ?: screenPath.split("/")[-1]
def description = screenDef.getDescription() ?: "Moqui screen: ${screenPath}"
def title = screenPath.split("/")[-1]
def description = "Moqui screen: ${screenPath}"
// Safely get screen description - screen XML doesn't have description elements
try {
if (screenDef?.screenNode?.attribute('default-menu-title')) {
description = screenDef.screenNode.attribute('default-menu-title')
}
} catch (Exception e) {
ec.logger.debug("Could not get screen title: ${e.message}")
}
// Get screen parameters from transitions and forms
def parameters = [:]
......@@ -1284,123 +1281,216 @@ try {
<parameter name="screenPath" required="true"/>
<parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter>
<parameter name="renderMode" default="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter>
<parameter name="sessionId"><description>Session ID for user context restoration</description></parameter>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
def startTime = System.currentTimeMillis()
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
def startTime = System.currentTimeMillis()
// Note: Screen validation will happen during render
// if (!ec.screen.isScreenDefined(screenPath)) {
// throw new Exception("Screen not found: ${screenPath}")
// }
// Set parameters in context
if (parameters) {
ec.context.putAll(parameters)
}
// Try to render screen content for LLM consumption
def output = null
def screenUrl = "http://localhost:8080/${screenPath}"
try {
ec.logger.info("MCP Screen Execution: Attempting to render screen ${screenPath} using ScreenTest with proper root screen")
// For ScreenTest to work properly, we need to use the correct root screen
// The screenPath should be relative to the appropriate root screen
def testScreenPath = screenPath
def rootScreen = "component://webroot/screen/webroot.xml"
// Initialize standalone flag outside the if block
def targetScreenDef = null
def isStandalone = false
// If the screen path is already a full component:// path, we need to handle it differently
if (screenPath.startsWith("component://")) {
// For component:// paths, we need to use the component's root screen, not webroot
// Extract the component name and use its root screen
def pathAfterComponent = screenPath.substring(12) // Remove "component://"
def pathParts = pathAfterComponent.split("/")
if (pathParts.length >= 2) {
def componentName = pathParts[0]
def remainingPath = pathParts[1..-1].join("/")
// Check if the target screen itself is standalone FIRST
try {
// Note: Screen validation will happen during render
// if (!ec.screen.isScreenDefined(screenPath)) {
// throw new Exception("Screen not found: ${screenPath}")
// }
// Set parameters in context
if (parameters) {
ec.context.putAll(parameters)
targetScreenDef = ec.screen.getScreenDefinition(screenPath)
ec.logger.info("MCP Screen Execution: Target screen def for ${screenPath}: ${targetScreenDef?.getClass()?.getSimpleName()}")
if (targetScreenDef?.screenNode) {
def standaloneAttr = targetScreenDef.screenNode.attribute('standalone')
ec.logger.info("MCP Screen Execution: Target screen ${screenPath} standalone attribute: '${standaloneAttr}'")
isStandalone = standaloneAttr == "true"
} else {
ec.logger.warn("MCP Screen Execution: Target screen ${screenPath} has no screenNode")
}
ec.logger.info("MCP Screen Execution: Target screen ${screenPath} standalone=${isStandalone}")
// Try to render screen content for LLM consumption
def output = null
def screenUrl = "http://localhost:8080/${screenPath}"
try {
ec.logger.info("MCP Screen Execution: Attempting to render screen ${screenPath} using ScreenTest with proper root screen")
// For ScreenTest to work properly, we need to use the correct root screen
// The screenPath should be relative to the appropriate root screen
def testScreenPath = screenPath
def rootScreen = "component://webroot/screen/webroot.xml"
// If the screen path is already a full component:// path, we need to handle it differently
if (screenPath.startsWith("component://")) {
// Extract the path after component:// for ScreenTest
testScreenPath = screenPath.substring(12) // Remove "component://"
ec.logger.info("MCP Screen Execution: Converted component path to test path: ${testScreenPath}")
if (isStandalone) {
// For standalone screens, try to render with minimal context or fall back to URL
ec.logger.info("MCP Screen Execution: Standalone screen detected, will try direct rendering")
rootScreen = screenPath
testScreenPath = "" // Empty path for standalone screens
// We'll handle standalone screens specially below
}
} catch (Exception e) {
ec.logger.warn("MCP Screen Execution: Error checking target screen ${screenPath}: ${e.message}")
ec.logger.error("MCP Screen Execution: Full exception", e)
}
// Only look for component root if target is not standalone
if (!isStandalone) {
// For component://webroot/screen/... paths, always use webroot as root
if (pathAfterComponent.startsWith("webroot/screen/")) {
// This is a webroot screen, use webroot as root and the rest as path
rootScreen = "component://webroot/screen/webroot.xml"
testScreenPath = pathAfterComponent.substring("webroot/screen/".length())
// Remove any leading "webroot/" from the path since we're already using webroot as root
if (testScreenPath.startsWith("webroot/")) {
testScreenPath = testScreenPath.substring("webroot/".length())
}
ec.logger.info("MCP Screen Execution: Using webroot root for webroot screen: ${rootScreen} with path: ${testScreenPath}")
} else {
// For other component screens, check if this is a direct screen path (not a subscreen path)
def pathSegments = remainingPath.split("/")
def isDirectScreenPath = false
def screenTest = ec.screen.makeTest()
.rootScreen(rootScreen)
.renderMode(renderMode ? renderMode : "text")
ec.logger.info("MCP Screen Execution: ScreenTest object created: ${screenTest?.getClass()?.getSimpleName()}")
// Try to check if the full path is a valid screen
try {
def directScreenDef = ec.screen.getScreenDefinition(screenPath)
if (directScreenDef) {
isDirectScreenPath = true
ec.logger.info("MCP Screen Execution: Found direct screen path: ${screenPath}")
}
} catch (Exception e) {
ec.logger.debug("MCP Screen Execution: Direct screen check failed for ${screenPath}: ${e.message}")
}
if (screenTest) {
def renderParams = parameters ?: [:]
// Add current user info to render context to maintain authentication
renderParams.userId = ec.user.userId
renderParams.username = ec.user.username
if (isDirectScreenPath) {
// For direct screen paths, use the screen itself as root
rootScreen = screenPath
testScreenPath = ""
ec.logger.info("MCP Screen Execution: Using direct screen as root: ${rootScreen}")
} else {
// Try to find the actual root screen for this component
def componentRootScreen = null
def possibleRootScreens = [
"${componentName}.xml",
"${componentName}Root.xml",
"${componentName}Admin.xml"
]
// Add timeout to prevent hanging
def future = java.util.concurrent.Executors.newSingleThreadExecutor().submit({
return screenTest.render(testScreenPath, renderParams, null)
} as java.util.concurrent.Callable)
for (rootScreenName in possibleRootScreens) {
def candidateRoot = "component://${componentName}/screen/${rootScreenName}"
try {
def testDef = ec.screen.getScreenDefinition(candidateRoot)
if (testDef) {
componentRootScreen = candidateRoot
ec.logger.info("MCP Screen Execution: Found component root screen: ${componentRootScreen}")
break
}
} catch (Exception e) {
ec.logger.debug("MCP Screen Execution: Root screen ${candidateRoot} not found: ${e.message}")
}
}
try {
def testRender = future.get(30, java.util.concurrent.TimeUnit.SECONDS) // 30 second timeout
output = testRender.output
def outputLength = output?.length() ?: 0
ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${outputLength}")
} catch (java.util.concurrent.TimeoutException e) {
future.cancel(true)
throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}")
} finally {
future.cancel(true)
if (componentRootScreen) {
rootScreen = componentRootScreen
testScreenPath = remainingPath
ec.logger.info("MCP Screen Execution: Using component root ${rootScreen} for path ${testScreenPath}")
} else {
// Fallback: try using webroot with the full path
rootScreen = "component://webroot/screen/webroot.xml"
testScreenPath = pathAfterComponent
ec.logger.warn("MCP Screen Execution: Could not find component root for ${componentName}, using webroot fallback: ${testScreenPath}")
}
} else {
throw new Exception("ScreenTest object is null")
}
} catch (Exception e) {
ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, falling back to URL: ${e.message}")
ec.logger.warn("MCP Screen Execution: Exception details: ${e.getClass()?.getSimpleName()}: ${e.getMessage()}")
ec.logger.error("MCP Screen Execution: Full exception for ${screenPath}", e)
// Fallback to URL if rendering fails
output = "Screen '${screenPath}' is accessible at: ${screenUrl}\n\nNote: Screen content could not be rendered. You can visit this URL in a web browser to interact with the screen directly."
}
}
} else {
// Fallback for malformed component paths
testScreenPath = pathAfterComponent
ec.logger.warn("MCP Screen Execution: Malformed component path, using fallback: ${testScreenPath}")
}
}
// Restore user context from sessionId before creating ScreenTest
// Regular screen rendering with restored user context - use our custom ScreenTestImpl
def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
.rootScreen(rootScreen)
.renderMode(renderMode ? renderMode : "html")
ec.logger.info("MCP Screen Execution: ScreenTest object created: ${screenTest?.getClass()?.getSimpleName()}")
if (screenTest) {
def renderParams = parameters ?: [:]
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Return just the rendered screen content for MCP wrapper to handle
result = [
type: "text",
text: output,
screenPath: screenPath,
screenUrl: screenUrl,
executionTime: executionTime
]
// Add current user info to render context to maintain authentication
renderParams.userId = ec.user.userId
renderParams.username = ec.user.username
ec.logger.info("MCP Screen Execution: Generated URL for screen ${screenPath} in ${executionTime}s")
// Set user context in ScreenTest to maintain authentication
// Note: ScreenTestImpl may not have direct userAccountId property,
// the user context should be inherited from the current ExecutionContext
ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}")
} catch (Exception e) {
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Regular screen rendering with timeout
def future = java.util.concurrent.Executors.newSingleThreadExecutor().submit({
return screenTest.render(testScreenPath, renderParams, null)
} as java.util.concurrent.Callable)
result = [
content: [
[
type: "text",
text: "Error executing screen ${screenPath}: ${e.message}"
]
],
isError: true,
metadata: [
screenPath: screenPath,
renderMode: renderMode,
executionTime: executionTime
]
]
ec.logger.error("MCP Screen Execution error for ${screenPath}", e)
try {
def testRender = future.get(30, java.util.concurrent.TimeUnit.SECONDS) // 30 second timeout
output = testRender.output
def outputLength = output?.length() ?: 0
ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${outputLength}")
} catch (java.util.concurrent.TimeoutException e) {
future.cancel(true)
throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}")
} finally {
future.cancel(true)
}
} else {
throw new Exception("ScreenTest object is null")
}
} catch (Exception e) {
ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, falling back to URL: ${e.message}")
ec.logger.warn("MCP Screen Execution: Exception details: ${e.getClass()?.getSimpleName()}: ${e.getMessage()}")
ec.logger.error("MCP Screen Execution: Full exception for ${screenPath}", e)
// Fallback to URL if rendering fails
output = "Screen '${screenPath}' is accessible at: ${screenUrl}\n\nNote: Screen content could not be rendered. You can visit this URL in a web browser to interact with the screen directly."
}
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Return just the rendered screen content for MCP wrapper to handle
result = [
type: "text",
text: output,
screenPath: screenPath,
screenUrl: screenUrl,
executionTime: executionTime
]
ec.logger.info("MCP Screen Execution: Generated URL for screen ${screenPath} in ${executionTime}s")
]]></script>
</actions>
</service>
......
/*
* This software is in the public domain under CC0 1.0 Universal plus a
* Grant of Patent License.
*
* To the extent possible under law, author(s) have dedicated all
* copyright and related and neighboring rights to this software to the
* public domain worldwide. This software is distributed without any
* warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication
* along with this software (see the LICENSE.md file). If not, see
* <http://creativecommons.org/publicdomain/zero/1.0/>.
*/
package org.moqui.mcp
import groovy.transform.CompileStatic
import org.apache.shiro.subject.Subject
import org.moqui.BaseArtifactException
import org.moqui.util.ContextStack
import org.moqui.impl.context.ExecutionContextFactoryImpl
import org.moqui.impl.context.ExecutionContextImpl
import org.moqui.impl.screen.ScreenDefinition
import org.moqui.impl.screen.ScreenFacadeImpl
import org.moqui.impl.screen.ScreenTestImpl
import org.moqui.impl.screen.ScreenUrlInfo
import org.moqui.screen.ScreenRender
import org.moqui.screen.ScreenTest
import org.moqui.util.MNode
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.Future
@CompileStatic
class CustomScreenTestImpl implements ScreenTest {
protected final static Logger logger = LoggerFactory.getLogger(CustomScreenTestImpl.class)
protected final ExecutionContextFactoryImpl ecfi
protected final ScreenFacadeImpl sfi
// see FtlTemplateRenderer.MoquiTemplateExceptionHandler, others
final List<String> errorStrings = ["[Template Error", "FTL stack trace", "Could not find subscreen or transition"]
protected String rootScreenLocation = null
protected ScreenDefinition rootScreenDef = null
protected String baseScreenPath = null
protected List<String> baseScreenPathList = null
protected ScreenDefinition baseScreenDef = null
protected String outputType = null
protected String characterEncoding = null
protected String macroTemplateLocation = null
protected String baseLinkUrl = null
protected String servletContextPath = null
protected String webappName = null
protected boolean skipJsonSerialize = false
protected static final String hostname = "localhost"
long renderCount = 0, errorCount = 0, totalChars = 0, startTime = System.currentTimeMillis()
final Map<String, Object> sessionAttributes = [:]
CustomScreenTestImpl(ExecutionContextFactoryImpl ecfi) {
this.ecfi = ecfi
sfi = ecfi.screenFacade
// init default webapp, root screen
webappName('webroot')
}
@Override
ScreenTest rootScreen(String screenLocation) {
rootScreenLocation = screenLocation
rootScreenDef = sfi.getScreenDefinition(rootScreenLocation)
if (rootScreenDef == null) throw new IllegalArgumentException("Root screen not found: ${rootScreenLocation}")
baseScreenDef = rootScreenDef
return this
}
@Override
ScreenTest baseScreenPath(String screenPath) {
if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified")
baseScreenPath = screenPath
if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1)
if (baseScreenPath) {
baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, [], baseScreenPath, null, sfi)
if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}")
for (String screenName in baseScreenPathList) {
ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName)
if (ssi == null) throw new BaseArtifactException("Error in baseScreenPath, could not find ${screenName} under ${baseScreenDef.location}")
baseScreenDef = sfi.getScreenDefinition(ssi.location)
if (baseScreenDef == null) throw new BaseArtifactException("Error in baseScreenPath, could not find screen ${screenName} at ${ssi.location}")
}
}
return this
}
@Override ScreenTest renderMode(String outputType) { this.outputType = outputType; return this }
@Override ScreenTest encoding(String characterEncoding) { this.characterEncoding = characterEncoding; return this }
@Override ScreenTest macroTemplate(String macroTemplateLocation) { this.macroTemplateLocation = macroTemplateLocation; return this }
@Override ScreenTest baseLinkUrl(String baseLinkUrl) { this.baseLinkUrl = baseLinkUrl; return this }
@Override ScreenTest servletContextPath(String scp) { this.servletContextPath = scp; return this }
@Override ScreenTest skipJsonSerialize(boolean skip) { this.skipJsonSerialize = skip; return this }
@Override
ScreenTest webappName(String wan) {
webappName = wan
// set a default root screen based on config for "localhost"
MNode webappNode = ecfi.getWebappNode(webappName)
for (MNode rootScreenNode in webappNode.children("root-screen")) {
if (hostname.matches(rootScreenNode.attribute('host'))) {
String rsLoc = rootScreenNode.attribute('location')
rootScreen(rsLoc)
break
}
}
return this
}
@Override
List<String> getNoRequiredParameterPaths(Set<String> screensToSkip) {
if (!rootScreenLocation) throw new IllegalStateException("No rootScreen specified")
List<String> noReqParmLocations = baseScreenDef.nestedNoReqParmLocations("", screensToSkip)
// logger.info("======= rootScreenLocation=${rootScreenLocation}\nbaseScreenPath=${baseScreenPath}\nbaseScreenDef: ${baseScreenDef.location}\nnoReqParmLocations: ${noReqParmLocations}")
return noReqParmLocations
}
@Override
ScreenTestRender render(String screenPath, Map<String, Object> parameters, String requestMethod) {
if (!rootScreenLocation) throw new IllegalArgumentException("No rootScreenLocation specified")
return new CustomScreenTestRenderImpl(this, screenPath, parameters, requestMethod).render()
}
@Override
void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod) {
// NOTE: using single thread for now, doesn't actually make a lot of difference in overall test run time
int threads = 1
if (threads == 1) {
for (String screenPath in screenPathList) {
ScreenTestRender str = render(screenPath, parameters, requestMethod)
logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.output?.length()} characters")
}
} else {
ExecutionContextImpl eci = ecfi.getEci()
ArrayList<Future> threadList = new ArrayList<Future>(threads)
int screenPathListSize = screenPathList.size()
for (int si = 0; si < screenPathListSize; si++) {
String screenPath = (String) screenPathList.get(si)
threadList.add(eci.runAsync({
ScreenTestRender str = render(screenPath, parameters, requestMethod)
logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.output?.length()} characters")
}))
if (threadList.size() == threads || (si + 1) == screenPathListSize) {
for (int i = 0; i < threadList.size(); i++) { ((Future) threadList.get(i)).get() }
threadList.clear()
}
}
}
}
long getRenderCount() { return renderCount }
long getErrorCount() { return errorCount }
long getRenderTotalChars() { return totalChars }
long getStartTime() { return startTime }
@CompileStatic
static class CustomScreenTestRenderImpl implements ScreenTestRender {
protected final CustomScreenTestImpl sti
String screenPath = (String) null
Map<String, Object> parameters = [:]
String requestMethod = (String) null
ScreenRender screenRender = (ScreenRender) null
String outputString = (String) null
Object jsonObj = null
long renderTime = 0
Map postRenderContext = (Map) null
protected List<String> errorMessages = []
CustomScreenTestRenderImpl(CustomScreenTestImpl sti, String screenPath, Map<String, Object> parameters, String requestMethod) {
this.sti = sti
this.screenPath = screenPath
if (parameters != null) this.parameters.putAll(parameters)
this.requestMethod = requestMethod
}
ScreenTestRender render() {
// render in separate thread with an independent ExecutionContext so it doesn't muck up the current one
ExecutionContextFactoryImpl ecfi = sti.ecfi
ExecutionContextImpl localEci = ecfi.getEci()
String username = localEci.userFacade.getUsername()
Subject loginSubject = localEci.userFacade.getCurrentSubject()
boolean authzDisabled = localEci.artifactExecutionFacade.getAuthzDisabled()
CustomScreenTestRenderImpl stri = this
Throwable threadThrown = null
Thread newThread = new Thread("CustomScreenTestRender") {
@Override void run() {
try {
ExecutionContextImpl threadEci = ecfi.getEci()
if (loginSubject != null) threadEci.userFacade.internalLoginSubject(loginSubject)
else if (username != null && !username.isEmpty()) threadEci.userFacade.internalLoginUser(username)
if (authzDisabled) threadEci.artifactExecutionFacade.disableAuthz()
// as this is used for server-side transition calls don't do tarpit checks
threadEci.artifactExecutionFacade.disableTarpit()
renderInternal(threadEci, stri)
threadEci.destroy()
} catch (Throwable t) {
threadThrown = t
}
}
}
newThread.start()
newThread.join()
if (threadThrown != null) throw threadThrown
return this
}
private static void renderInternal(ExecutionContextImpl eci, CustomScreenTestRenderImpl stri) {
CustomScreenTestImpl sti = stri.sti
long startTime = System.currentTimeMillis()
// parse the screenPath - if empty or null, use empty list to render the root screen
ArrayList<String> screenPathList = []
if (stri.screenPath != null && !stri.screenPath.trim().isEmpty()) {
screenPathList = ScreenUrlInfo.parseSubScreenPath(sti.rootScreenDef, sti.baseScreenDef,
sti.baseScreenPathList, stri.screenPath, stri.parameters, sti.sfi)
if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${sti.baseScreenDef.location}")
}
// push context
ContextStack cs = eci.getContext()
cs.push()
// create our custom WebFacadeStub instead of framework's, passing screen path for proper path handling
WebFacadeStub wfs = new WebFacadeStub(sti.ecfi, stri.parameters, sti.sessionAttributes, stri.requestMethod, stri.screenPath)
// set stub on eci, will also put parameters in context
eci.setWebFacade(wfs)
// make the ScreenRender
ScreenRender screenRender = sti.sfi.makeRender()
stri.screenRender = screenRender
// pass through various settings
if (sti.rootScreenLocation != null && sti.rootScreenLocation.length() > 0) screenRender.rootScreen(sti.rootScreenLocation)
if (sti.outputType != null && sti.outputType.length() > 0) screenRender.renderMode(sti.outputType)
if (sti.characterEncoding != null && sti.characterEncoding.length() > 0) screenRender.encoding(sti.characterEncoding)
if (sti.macroTemplateLocation != null && sti.macroTemplateLocation.length() > 0) screenRender.macroTemplate(sti.macroTemplateLocation)
if (sti.baseLinkUrl != null && sti.baseLinkUrl.length() > 0) screenRender.baseLinkUrl(sti.baseLinkUrl)
if (sti.servletContextPath != null && sti.servletContextPath.length() > 0) screenRender.servletContextPath(sti.servletContextPath)
screenRender.webappName(sti.webappName)
if (sti.skipJsonSerialize) wfs.skipJsonSerialize = true
// set the screenPath
screenRender.screenPath(screenPathList)
// do the render
try {
screenRender.render(wfs.httpServletRequest, wfs.httpServletResponse)
// get the response text from our WebFacadeStub
stri.outputString = wfs.getResponseText()
stri.jsonObj = wfs.getResponseJsonObj()
System.out.println("STRI: " + stri.outputString + " : " + stri.jsonObj)
} catch (Throwable t) {
String errMsg = "Exception in render of ${stri.screenPath}: ${t.toString()}"
logger.warn(errMsg, t)
stri.errorMessages.add(errMsg)
sti.errorCount++
}
// calc renderTime
stri.renderTime = System.currentTimeMillis() - startTime
// pop the context stack, get rid of var space
stri.postRenderContext = cs.pop()
// check, pass through, error messages
if (eci.message.hasError()) {
stri.errorMessages.addAll(eci.message.getErrors())
eci.message.clearErrors()
StringBuilder sb = new StringBuilder("Error messages from ${stri.screenPath}: ")
for (String errorMessage in stri.errorMessages) sb.append("\n").append(errorMessage)
logger.warn(sb.toString())
sti.errorCount += stri.errorMessages.size()
}
// check for error strings in output
if (stri.outputString != null) for (String errorStr in sti.errorStrings) if (stri.outputString.contains(errorStr)) {
String errMsg = "Found error [${errorStr}] in output from ${stri.screenPath}"
stri.errorMessages.add(errMsg)
sti.errorCount++
logger.warn(errMsg)
}
// update stats
sti.renderCount++
if (stri.outputString != null) sti.totalChars += stri.outputString.length()
}
@Override ScreenRender getScreenRender() { return screenRender }
@Override String getOutput() { return outputString }
@Override Object getJsonObject() { return jsonObj }
@Override long getRenderTime() { return renderTime }
@Override Map getPostRenderContext() { return postRenderContext }
@Override List<String> getErrorMessages() { return errorMessages }
@Override
boolean assertContains(String text) {
if (!outputString) return false
return outputString.contains(text)
}
@Override
boolean assertNotContains(String text) {
if (!outputString) return true
return !outputString.contains(text)
}
@Override
boolean assertRegex(String regex) {
if (!outputString) return false
return outputString.matches(regex)
}
}
}
\ No newline at end of file
......@@ -33,6 +33,7 @@ class WebFacadeStub implements WebFacade {
protected final Map<String, Object> parameters
protected final Map<String, Object> sessionAttributes
protected final String requestMethod
protected final String screenPath
protected HttpServletRequest httpServletRequest
protected HttpServletResponse httpServletResponse
......@@ -54,11 +55,12 @@ class WebFacadeStub implements WebFacade {
boolean skipJsonSerialize = false
WebFacadeStub(ExecutionContextFactoryImpl ecfi, Map<String, Object> parameters,
Map<String, Object> sessionAttributes, String requestMethod) {
Map<String, Object> sessionAttributes, String requestMethod, String screenPath = null) {
this.ecfi = ecfi
this.parameters = parameters ?: [:]
this.sessionAttributes = sessionAttributes ?: [:]
this.requestMethod = requestMethod ?: "GET"
this.screenPath = screenPath
// Create mock HTTP objects
createMockHttpObjects()
......@@ -111,11 +113,23 @@ class WebFacadeStub implements WebFacade {
}
@Override
String getPathInfo() { return "/test" }
String getPathInfo() {
// For standalone screens, return empty path to render the screen itself
// For screens with subscreen paths, return the relative path
return screenPath ? "/${screenPath}" : ""
}
@Override
ArrayList<String> getPathInfoList() {
return new ArrayList<String>(["test"])
// IMPORTANT: Don't delegate to WebFacadeImpl - it expects real HTTP servlet context
// Return mock path info for MCP screen rendering based on actual screen path
def pathInfo = getPathInfo()
if (pathInfo && pathInfo.startsWith("/")) {
// Split path and filter out empty parts
def pathParts = pathInfo.substring(1).split("/") as List
return new ArrayList<String>(pathParts.findAll { it && it.toString().length() > 0 })
}
return new ArrayList<String>() // Empty for standalone screens
}
@Override
......