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
...@@ -436,22 +436,6 @@ ...@@ -436,22 +436,6 @@
436 ec.logger.info("Found original screen path for tool ${name}: ${screenPath}") 436 ec.logger.info("Found original screen path for tool ${name}: ${screenPath}")
437 } 437 }
438 438
439 /*
440 // Map common screen patterns to actual screen locations
441 if (screenPath.startsWith("OrderFind") || screenPath.startsWith("ProductFind") || screenPath.startsWith("PartyFind")) {
442 // For find screens, try to use appropriate component screens
443 if (screenPath.startsWith("Order")) {
444 screenPath = "webroot/apps/order" // Try to map to order app
445 } else if (screenPath.startsWith("Product")) {
446 screenPath = "webroot/apps/product" // Try to map to product app
447 } else if (screenPath.startsWith("Party")) {
448 screenPath = "webroot/apps/party" // Try to map to party app
449 }
450 } else if (screenPath == "apps") {
451 screenPath = "component://webroot/screen/webroot.xml" // Use full component path to webroot screen
452 }
453 */
454
455 // Restore user context from sessionId before calling screen tool 439 // Restore user context from sessionId before calling screen tool
456 def serviceResult = null 440 def serviceResult = null
457 def visit = null 441 def visit = null
...@@ -483,7 +467,7 @@ ...@@ -483,7 +467,7 @@
483 // Now call the screen tool with proper user context 467 // Now call the screen tool with proper user context
484 def screenParams = arguments ?: [:] 468 def screenParams = arguments ?: [:]
485 serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool") 469 serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
486 .parameters([screenPath: screenPath, parameters: screenParams, renderMode: "html"]) 470 .parameters([screenPath: screenPath, parameters: screenParams, renderMode: "html", sessionId: sessionId])
487 .call() 471 .call()
488 472
489 } finally { 473 } finally {
...@@ -530,41 +514,6 @@ ...@@ -530,41 +514,6 @@
530 def originalUsername = ec.user.username 514 def originalUsername = ec.user.username
531 UserInfo adminUserInfo = null 515 UserInfo adminUserInfo = null
532 516
533 // Timing already started above
534
535 // Validate session if provided
536 /*
537 if (sessionId) {
538 def visit = null
539 adminUserInfo = null
540 try {
541 adminUserInfo = ec.user.pushUser("ADMIN")
542 visit = ec.entity.find("moqui.server.Visit")
543 .condition("visitId", sessionId)
544 .one()
545 } finally {
546 if (adminUserInfo != null) {
547 ec.user.popUser()
548 }
549 }
550
551 // Validate session - allow special MCP case where Visit was created with ADMIN but accessed by MCP_USER or MCP_BUSINESS
552 boolean sessionValid = false
553 if (visit) {
554 if (visit.userId == ec.user.userId) {
555 sessionValid = true
556 } else if (visit.userId == "ADMIN" && (ec.user.username == "mcp-user" || ec.user.username == "mcp-business")) {
557 // Special MCP case: Visit created with ADMIN for privileged access but accessed by MCP users
558 sessionValid = true
559 }
560 }
561
562 if (!sessionValid) {
563 //throw new Exception("Invalid session: ${sessionId}")
564 }
565 }
566 */
567
568 try { 517 try {
569 // Execute service with elevated privileges for system access 518 // Execute service with elevated privileges for system access
570 // but maintain audit context with actual user 519 // but maintain audit context with actual user
...@@ -844,11 +793,48 @@ try { ...@@ -844,11 +793,48 @@ try {
844 </out-parameters> 793 </out-parameters>
845 <actions> 794 <actions>
846 <script><![CDATA[ 795 <script><![CDATA[
796 import org.moqui.impl.context.UserFacadeImpl.UserInfo
797
798 // Get current user information
799 def currentUser = ec.user.username
800 def currentUserId = ec.user.userId
801
802 // Try to get visit information if sessionId is provided
803 def visitInfo = null
804 if (sessionId) {
805 try {
806 def adminUserInfo = ec.user.pushUser("ADMIN")
807 try {
808 def visit = ec.entity.find("moqui.server.Visit")
809 .condition("visitId", sessionId)
810 .disableAuthz()
811 .one()
812
813 if (visit) {
814 visitInfo = [
815 visitId: visit.visitId,
816 userId: visit.userId,
817 fromDate: visit.fromDate,
818 lastUpdatedStamp: visit.lastUpdatedStamp
819 ]
820 }
821 } finally {
822 ec.user.popUser()
823 }
824 } catch (Exception e) {
825 // Log but don't fail the ping
826 ec.logger.warn("Error getting visit info for sessionId ${sessionId}: ${e.message}")
827 }
828 }
829
847 result = [ 830 result = [
848 timestamp: ec.user.getNowTimestamp(), 831 timestamp: ec.user.getNowTimestamp(),
849 status: "healthy", 832 status: "healthy",
850 version: "2.0.2", 833 version: "2.0.2",
851 sessionId: sessionId, 834 sessionId: sessionId,
835 currentUser: currentUser,
836 currentUserId: currentUserId,
837 visitInfo: visitInfo,
852 architecture: "Visit-based sessions" 838 architecture: "Visit-based sessions"
853 ] 839 ]
854 ]]></script> 840 ]]></script>
...@@ -1080,9 +1066,11 @@ try { ...@@ -1080,9 +1066,11 @@ try {
1080 1066
1081 try { 1067 try {
1082 screenDefinition = ec.screen.getScreenDefinition(screenPath) 1068 screenDefinition = ec.screen.getScreenDefinition(screenPath)
1083 if (screenDefinition?.screenNode?.first("description")?.text) { 1069 // Screen XML doesn't have description elements, so use screen path as description
1084 title = screenDefinition.screenNode.first("description").text 1070 // We could potentially use default-menu-title attribute if available
1085 description = screenDefinition.screenNode.first("description").text 1071 if (screenDefinition?.screenNode?.attribute('default-menu-title')) {
1072 title = screenDefinition.screenNode.attribute('default-menu-title')
1073 description = "Moqui screen: ${screenPath} (${title})"
1086 } 1074 }
1087 } catch (Exception e) { 1075 } catch (Exception e) {
1088 ec.logger.info("MCP Screen Discovery: No screen definition for ${screenPath}, using basic info") 1076 ec.logger.info("MCP Screen Discovery: No screen definition for ${screenPath}, using basic info")
...@@ -1170,8 +1158,17 @@ try { ...@@ -1170,8 +1158,17 @@ try {
1170 1158
1171 // Extract screen information 1159 // Extract screen information
1172 def screenName = screenPath.replaceAll("[^a-zA-Z0-9]", "_") 1160 def screenName = screenPath.replaceAll("[^a-zA-Z0-9]", "_")
1173 def title = screenDef.getScreenName() ?: screenPath.split("/")[-1] 1161 def title = screenPath.split("/")[-1]
1174 def description = screenDef.getDescription() ?: "Moqui screen: ${screenPath}" 1162 def description = "Moqui screen: ${screenPath}"
1163
1164 // Safely get screen description - screen XML doesn't have description elements
1165 try {
1166 if (screenDef?.screenNode?.attribute('default-menu-title')) {
1167 description = screenDef.screenNode.attribute('default-menu-title')
1168 }
1169 } catch (Exception e) {
1170 ec.logger.debug("Could not get screen title: ${e.message}")
1171 }
1175 1172
1176 // Get screen parameters from transitions and forms 1173 // Get screen parameters from transitions and forms
1177 def parameters = [:] 1174 def parameters = [:]
...@@ -1284,19 +1281,19 @@ try { ...@@ -1284,19 +1281,19 @@ try {
1284 <parameter name="screenPath" required="true"/> 1281 <parameter name="screenPath" required="true"/>
1285 <parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter> 1282 <parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter>
1286 <parameter name="renderMode" default="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter> 1283 <parameter name="renderMode" default="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter>
1284 <parameter name="sessionId"><description>Session ID for user context restoration</description></parameter>
1287 </in-parameters> 1285 </in-parameters>
1288 <out-parameters> 1286 <out-parameters>
1289 <parameter name="result" type="Map"/> 1287 <parameter name="result" type="Map"/>
1290 </out-parameters> 1288 </out-parameters>
1291 <actions> 1289 <actions>
1292 <script><![CDATA[ 1290 <script><![CDATA[
1293 import org.moqui.context.ExecutionContext 1291 import org.moqui.context.ExecutionContext
1294 import groovy.json.JsonBuilder 1292 import groovy.json.JsonBuilder
1295 1293
1296 ExecutionContext ec = context.ec 1294 ExecutionContext ec = context.ec
1297 1295
1298 def startTime = System.currentTimeMillis() 1296 def startTime = System.currentTimeMillis()
1299 try {
1300 // Note: Screen validation will happen during render 1297 // Note: Screen validation will happen during render
1301 // if (!ec.screen.isScreenDefined(screenPath)) { 1298 // if (!ec.screen.isScreenDefined(screenPath)) {
1302 // throw new Exception("Screen not found: ${screenPath}") 1299 // throw new Exception("Screen not found: ${screenPath}")
...@@ -1319,16 +1316,126 @@ try { ...@@ -1319,16 +1316,126 @@ try {
1319 def testScreenPath = screenPath 1316 def testScreenPath = screenPath
1320 def rootScreen = "component://webroot/screen/webroot.xml" 1317 def rootScreen = "component://webroot/screen/webroot.xml"
1321 1318
1319 // Initialize standalone flag outside the if block
1320 def targetScreenDef = null
1321 def isStandalone = false
1322
1322 // If the screen path is already a full component:// path, we need to handle it differently 1323 // If the screen path is already a full component:// path, we need to handle it differently
1323 if (screenPath.startsWith("component://")) { 1324 if (screenPath.startsWith("component://")) {
1324 // Extract the path after component:// for ScreenTest 1325 // For component:// paths, we need to use the component's root screen, not webroot
1325 testScreenPath = screenPath.substring(12) // Remove "component://" 1326 // Extract the component name and use its root screen
1326 ec.logger.info("MCP Screen Execution: Converted component path to test path: ${testScreenPath}") 1327 def pathAfterComponent = screenPath.substring(12) // Remove "component://"
1328 def pathParts = pathAfterComponent.split("/")
1329 if (pathParts.length >= 2) {
1330 def componentName = pathParts[0]
1331 def remainingPath = pathParts[1..-1].join("/")
1332
1333 // Check if the target screen itself is standalone FIRST
1334 try {
1335 targetScreenDef = ec.screen.getScreenDefinition(screenPath)
1336 ec.logger.info("MCP Screen Execution: Target screen def for ${screenPath}: ${targetScreenDef?.getClass()?.getSimpleName()}")
1337 if (targetScreenDef?.screenNode) {
1338 def standaloneAttr = targetScreenDef.screenNode.attribute('standalone')
1339 ec.logger.info("MCP Screen Execution: Target screen ${screenPath} standalone attribute: '${standaloneAttr}'")
1340 isStandalone = standaloneAttr == "true"
1341 } else {
1342 ec.logger.warn("MCP Screen Execution: Target screen ${screenPath} has no screenNode")
1343 }
1344 ec.logger.info("MCP Screen Execution: Target screen ${screenPath} standalone=${isStandalone}")
1345
1346 if (isStandalone) {
1347 // For standalone screens, try to render with minimal context or fall back to URL
1348 ec.logger.info("MCP Screen Execution: Standalone screen detected, will try direct rendering")
1349 rootScreen = screenPath
1350 testScreenPath = "" // Empty path for standalone screens
1351 // We'll handle standalone screens specially below
1352 }
1353 } catch (Exception e) {
1354 ec.logger.warn("MCP Screen Execution: Error checking target screen ${screenPath}: ${e.message}")
1355 ec.logger.error("MCP Screen Execution: Full exception", e)
1356 }
1357
1358 // Only look for component root if target is not standalone
1359 if (!isStandalone) {
1360 // For component://webroot/screen/... paths, always use webroot as root
1361 if (pathAfterComponent.startsWith("webroot/screen/")) {
1362 // This is a webroot screen, use webroot as root and the rest as path
1363 rootScreen = "component://webroot/screen/webroot.xml"
1364 testScreenPath = pathAfterComponent.substring("webroot/screen/".length())
1365 // Remove any leading "webroot/" from the path since we're already using webroot as root
1366 if (testScreenPath.startsWith("webroot/")) {
1367 testScreenPath = testScreenPath.substring("webroot/".length())
1368 }
1369 ec.logger.info("MCP Screen Execution: Using webroot root for webroot screen: ${rootScreen} with path: ${testScreenPath}")
1370 } else {
1371 // For other component screens, check if this is a direct screen path (not a subscreen path)
1372 def pathSegments = remainingPath.split("/")
1373 def isDirectScreenPath = false
1374
1375 // Try to check if the full path is a valid screen
1376 try {
1377 def directScreenDef = ec.screen.getScreenDefinition(screenPath)
1378 if (directScreenDef) {
1379 isDirectScreenPath = true
1380 ec.logger.info("MCP Screen Execution: Found direct screen path: ${screenPath}")
1381 }
1382 } catch (Exception e) {
1383 ec.logger.debug("MCP Screen Execution: Direct screen check failed for ${screenPath}: ${e.message}")
1384 }
1385
1386 if (isDirectScreenPath) {
1387 // For direct screen paths, use the screen itself as root
1388 rootScreen = screenPath
1389 testScreenPath = ""
1390 ec.logger.info("MCP Screen Execution: Using direct screen as root: ${rootScreen}")
1391 } else {
1392 // Try to find the actual root screen for this component
1393 def componentRootScreen = null
1394 def possibleRootScreens = [
1395 "${componentName}.xml",
1396 "${componentName}Root.xml",
1397 "${componentName}Admin.xml"
1398 ]
1399
1400 for (rootScreenName in possibleRootScreens) {
1401 def candidateRoot = "component://${componentName}/screen/${rootScreenName}"
1402 try {
1403 def testDef = ec.screen.getScreenDefinition(candidateRoot)
1404 if (testDef) {
1405 componentRootScreen = candidateRoot
1406 ec.logger.info("MCP Screen Execution: Found component root screen: ${componentRootScreen}")
1407 break
1408 }
1409 } catch (Exception e) {
1410 ec.logger.debug("MCP Screen Execution: Root screen ${candidateRoot} not found: ${e.message}")
1411 }
1327 } 1412 }
1328 1413
1329 def screenTest = ec.screen.makeTest() 1414 if (componentRootScreen) {
1415 rootScreen = componentRootScreen
1416 testScreenPath = remainingPath
1417 ec.logger.info("MCP Screen Execution: Using component root ${rootScreen} for path ${testScreenPath}")
1418 } else {
1419 // Fallback: try using webroot with the full path
1420 rootScreen = "component://webroot/screen/webroot.xml"
1421 testScreenPath = pathAfterComponent
1422 ec.logger.warn("MCP Screen Execution: Could not find component root for ${componentName}, using webroot fallback: ${testScreenPath}")
1423 }
1424 }
1425 }
1426 }
1427 } else {
1428 // Fallback for malformed component paths
1429 testScreenPath = pathAfterComponent
1430 ec.logger.warn("MCP Screen Execution: Malformed component path, using fallback: ${testScreenPath}")
1431 }
1432 }
1433
1434 // Restore user context from sessionId before creating ScreenTest
1435 // Regular screen rendering with restored user context - use our custom ScreenTestImpl
1436 def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
1330 .rootScreen(rootScreen) 1437 .rootScreen(rootScreen)
1331 .renderMode(renderMode ? renderMode : "text") 1438 .renderMode(renderMode ? renderMode : "html")
1332 1439
1333 ec.logger.info("MCP Screen Execution: ScreenTest object created: ${screenTest?.getClass()?.getSimpleName()}") 1440 ec.logger.info("MCP Screen Execution: ScreenTest object created: ${screenTest?.getClass()?.getSimpleName()}")
1334 1441
...@@ -1339,7 +1446,12 @@ try { ...@@ -1339,7 +1446,12 @@ try {
1339 renderParams.userId = ec.user.userId 1446 renderParams.userId = ec.user.userId
1340 renderParams.username = ec.user.username 1447 renderParams.username = ec.user.username
1341 1448
1342 // Add timeout to prevent hanging 1449 // Set user context in ScreenTest to maintain authentication
1450 // Note: ScreenTestImpl may not have direct userAccountId property,
1451 // the user context should be inherited from the current ExecutionContext
1452 ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}")
1453
1454 // Regular screen rendering with timeout
1343 def future = java.util.concurrent.Executors.newSingleThreadExecutor().submit({ 1455 def future = java.util.concurrent.Executors.newSingleThreadExecutor().submit({
1344 return screenTest.render(testScreenPath, renderParams, null) 1456 return screenTest.render(testScreenPath, renderParams, null)
1345 } as java.util.concurrent.Callable) 1457 } as java.util.concurrent.Callable)
...@@ -1358,7 +1470,6 @@ try { ...@@ -1358,7 +1470,6 @@ try {
1358 } else { 1470 } else {
1359 throw new Exception("ScreenTest object is null") 1471 throw new Exception("ScreenTest object is null")
1360 } 1472 }
1361
1362 } catch (Exception e) { 1473 } catch (Exception e) {
1363 ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, falling back to URL: ${e.message}") 1474 ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, falling back to URL: ${e.message}")
1364 ec.logger.warn("MCP Screen Execution: Exception details: ${e.getClass()?.getSimpleName()}: ${e.getMessage()}") 1475 ec.logger.warn("MCP Screen Execution: Exception details: ${e.getClass()?.getSimpleName()}: ${e.getMessage()}")
...@@ -1380,27 +1491,6 @@ try { ...@@ -1380,27 +1491,6 @@ try {
1380 ] 1491 ]
1381 1492
1382 ec.logger.info("MCP Screen Execution: Generated URL for screen ${screenPath} in ${executionTime}s") 1493 ec.logger.info("MCP Screen Execution: Generated URL for screen ${screenPath} in ${executionTime}s")
1383
1384 } catch (Exception e) {
1385 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
1386
1387 result = [
1388 content: [
1389 [
1390 type: "text",
1391 text: "Error executing screen ${screenPath}: ${e.message}"
1392 ]
1393 ],
1394 isError: true,
1395 metadata: [
1396 screenPath: screenPath,
1397 renderMode: renderMode,
1398 executionTime: executionTime
1399 ]
1400 ]
1401
1402 ec.logger.error("MCP Screen Execution error for ${screenPath}", e)
1403 }
1404 ]]></script> 1494 ]]></script>
1405 </actions> 1495 </actions>
1406 </service> 1496 </service>
......
1 /*
2 * This software is in the public domain under CC0 1.0 Universal plus a
3 * Grant of Patent License.
4 *
5 * To the extent possible under law, author(s) have dedicated all
6 * copyright and related and neighboring rights to this software to the
7 * public domain worldwide. This software is distributed without any
8 * warranty.
9 *
10 * You should have received a copy of the CC0 Public Domain Dedication
11 * along with this software (see the LICENSE.md file). If not, see
12 * <http://creativecommons.org/publicdomain/zero/1.0/>.
13 */
14 package org.moqui.mcp
15
16 import groovy.transform.CompileStatic
17 import org.apache.shiro.subject.Subject
18 import org.moqui.BaseArtifactException
19 import org.moqui.util.ContextStack
20 import org.moqui.impl.context.ExecutionContextFactoryImpl
21 import org.moqui.impl.context.ExecutionContextImpl
22 import org.moqui.impl.screen.ScreenDefinition
23 import org.moqui.impl.screen.ScreenFacadeImpl
24 import org.moqui.impl.screen.ScreenTestImpl
25 import org.moqui.impl.screen.ScreenUrlInfo
26 import org.moqui.screen.ScreenRender
27 import org.moqui.screen.ScreenTest
28 import org.moqui.util.MNode
29 import org.slf4j.Logger
30 import org.slf4j.LoggerFactory
31
32 import java.util.concurrent.Future
33
34 @CompileStatic
35 class CustomScreenTestImpl implements ScreenTest {
36 protected final static Logger logger = LoggerFactory.getLogger(CustomScreenTestImpl.class)
37
38 protected final ExecutionContextFactoryImpl ecfi
39 protected final ScreenFacadeImpl sfi
40 // see FtlTemplateRenderer.MoquiTemplateExceptionHandler, others
41 final List<String> errorStrings = ["[Template Error", "FTL stack trace", "Could not find subscreen or transition"]
42
43 protected String rootScreenLocation = null
44 protected ScreenDefinition rootScreenDef = null
45 protected String baseScreenPath = null
46 protected List<String> baseScreenPathList = null
47 protected ScreenDefinition baseScreenDef = null
48
49 protected String outputType = null
50 protected String characterEncoding = null
51 protected String macroTemplateLocation = null
52 protected String baseLinkUrl = null
53 protected String servletContextPath = null
54 protected String webappName = null
55 protected boolean skipJsonSerialize = false
56 protected static final String hostname = "localhost"
57
58 long renderCount = 0, errorCount = 0, totalChars = 0, startTime = System.currentTimeMillis()
59
60 final Map<String, Object> sessionAttributes = [:]
61
62 CustomScreenTestImpl(ExecutionContextFactoryImpl ecfi) {
63 this.ecfi = ecfi
64 sfi = ecfi.screenFacade
65
66 // init default webapp, root screen
67 webappName('webroot')
68 }
69
70 @Override
71 ScreenTest rootScreen(String screenLocation) {
72 rootScreenLocation = screenLocation
73 rootScreenDef = sfi.getScreenDefinition(rootScreenLocation)
74 if (rootScreenDef == null) throw new IllegalArgumentException("Root screen not found: ${rootScreenLocation}")
75 baseScreenDef = rootScreenDef
76 return this
77 }
78 @Override
79 ScreenTest baseScreenPath(String screenPath) {
80 if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified")
81 baseScreenPath = screenPath
82 if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1)
83 if (baseScreenPath) {
84 baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, [], baseScreenPath, null, sfi)
85 if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}")
86 for (String screenName in baseScreenPathList) {
87 ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName)
88 if (ssi == null) throw new BaseArtifactException("Error in baseScreenPath, could not find ${screenName} under ${baseScreenDef.location}")
89 baseScreenDef = sfi.getScreenDefinition(ssi.location)
90 if (baseScreenDef == null) throw new BaseArtifactException("Error in baseScreenPath, could not find screen ${screenName} at ${ssi.location}")
91 }
92 }
93 return this
94 }
95 @Override ScreenTest renderMode(String outputType) { this.outputType = outputType; return this }
96 @Override ScreenTest encoding(String characterEncoding) { this.characterEncoding = characterEncoding; return this }
97 @Override ScreenTest macroTemplate(String macroTemplateLocation) { this.macroTemplateLocation = macroTemplateLocation; return this }
98 @Override ScreenTest baseLinkUrl(String baseLinkUrl) { this.baseLinkUrl = baseLinkUrl; return this }
99 @Override ScreenTest servletContextPath(String scp) { this.servletContextPath = scp; return this }
100 @Override ScreenTest skipJsonSerialize(boolean skip) { this.skipJsonSerialize = skip; return this }
101
102 @Override
103 ScreenTest webappName(String wan) {
104 webappName = wan
105
106 // set a default root screen based on config for "localhost"
107 MNode webappNode = ecfi.getWebappNode(webappName)
108 for (MNode rootScreenNode in webappNode.children("root-screen")) {
109 if (hostname.matches(rootScreenNode.attribute('host'))) {
110 String rsLoc = rootScreenNode.attribute('location')
111 rootScreen(rsLoc)
112 break
113 }
114 }
115
116 return this
117 }
118
119 @Override
120 List<String> getNoRequiredParameterPaths(Set<String> screensToSkip) {
121 if (!rootScreenLocation) throw new IllegalStateException("No rootScreen specified")
122
123 List<String> noReqParmLocations = baseScreenDef.nestedNoReqParmLocations("", screensToSkip)
124 // logger.info("======= rootScreenLocation=${rootScreenLocation}\nbaseScreenPath=${baseScreenPath}\nbaseScreenDef: ${baseScreenDef.location}\nnoReqParmLocations: ${noReqParmLocations}")
125 return noReqParmLocations
126 }
127
128 @Override
129 ScreenTestRender render(String screenPath, Map<String, Object> parameters, String requestMethod) {
130 if (!rootScreenLocation) throw new IllegalArgumentException("No rootScreenLocation specified")
131 return new CustomScreenTestRenderImpl(this, screenPath, parameters, requestMethod).render()
132 }
133 @Override
134 void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod) {
135 // NOTE: using single thread for now, doesn't actually make a lot of difference in overall test run time
136 int threads = 1
137 if (threads == 1) {
138 for (String screenPath in screenPathList) {
139 ScreenTestRender str = render(screenPath, parameters, requestMethod)
140 logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.output?.length()} characters")
141 }
142 } else {
143 ExecutionContextImpl eci = ecfi.getEci()
144 ArrayList<Future> threadList = new ArrayList<Future>(threads)
145 int screenPathListSize = screenPathList.size()
146 for (int si = 0; si < screenPathListSize; si++) {
147 String screenPath = (String) screenPathList.get(si)
148 threadList.add(eci.runAsync({
149 ScreenTestRender str = render(screenPath, parameters, requestMethod)
150 logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.output?.length()} characters")
151 }))
152 if (threadList.size() == threads || (si + 1) == screenPathListSize) {
153 for (int i = 0; i < threadList.size(); i++) { ((Future) threadList.get(i)).get() }
154 threadList.clear()
155 }
156 }
157 }
158 }
159
160 long getRenderCount() { return renderCount }
161 long getErrorCount() { return errorCount }
162 long getRenderTotalChars() { return totalChars }
163 long getStartTime() { return startTime }
164
165 @CompileStatic
166 static class CustomScreenTestRenderImpl implements ScreenTestRender {
167 protected final CustomScreenTestImpl sti
168 String screenPath = (String) null
169 Map<String, Object> parameters = [:]
170 String requestMethod = (String) null
171
172 ScreenRender screenRender = (ScreenRender) null
173 String outputString = (String) null
174 Object jsonObj = null
175 long renderTime = 0
176 Map postRenderContext = (Map) null
177 protected List<String> errorMessages = []
178
179 CustomScreenTestRenderImpl(CustomScreenTestImpl sti, String screenPath, Map<String, Object> parameters, String requestMethod) {
180 this.sti = sti
181 this.screenPath = screenPath
182 if (parameters != null) this.parameters.putAll(parameters)
183 this.requestMethod = requestMethod
184 }
185
186
187 ScreenTestRender render() {
188 // render in separate thread with an independent ExecutionContext so it doesn't muck up the current one
189 ExecutionContextFactoryImpl ecfi = sti.ecfi
190 ExecutionContextImpl localEci = ecfi.getEci()
191 String username = localEci.userFacade.getUsername()
192 Subject loginSubject = localEci.userFacade.getCurrentSubject()
193 boolean authzDisabled = localEci.artifactExecutionFacade.getAuthzDisabled()
194 CustomScreenTestRenderImpl stri = this
195 Throwable threadThrown = null
196
197 Thread newThread = new Thread("CustomScreenTestRender") {
198 @Override void run() {
199 try {
200 ExecutionContextImpl threadEci = ecfi.getEci()
201 if (loginSubject != null) threadEci.userFacade.internalLoginSubject(loginSubject)
202 else if (username != null && !username.isEmpty()) threadEci.userFacade.internalLoginUser(username)
203 if (authzDisabled) threadEci.artifactExecutionFacade.disableAuthz()
204 // as this is used for server-side transition calls don't do tarpit checks
205 threadEci.artifactExecutionFacade.disableTarpit()
206 renderInternal(threadEci, stri)
207 threadEci.destroy()
208 } catch (Throwable t) {
209 threadThrown = t
210 }
211 }
212 }
213 newThread.start()
214 newThread.join()
215 if (threadThrown != null) throw threadThrown
216 return this
217 }
218 private static void renderInternal(ExecutionContextImpl eci, CustomScreenTestRenderImpl stri) {
219 CustomScreenTestImpl sti = stri.sti
220 long startTime = System.currentTimeMillis()
221
222 // parse the screenPath - if empty or null, use empty list to render the root screen
223 ArrayList<String> screenPathList = []
224 if (stri.screenPath != null && !stri.screenPath.trim().isEmpty()) {
225 screenPathList = ScreenUrlInfo.parseSubScreenPath(sti.rootScreenDef, sti.baseScreenDef,
226 sti.baseScreenPathList, stri.screenPath, stri.parameters, sti.sfi)
227 if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${sti.baseScreenDef.location}")
228 }
229
230 // push context
231 ContextStack cs = eci.getContext()
232 cs.push()
233 // create our custom WebFacadeStub instead of framework's, passing screen path for proper path handling
234 WebFacadeStub wfs = new WebFacadeStub(sti.ecfi, stri.parameters, sti.sessionAttributes, stri.requestMethod, stri.screenPath)
235 // set stub on eci, will also put parameters in context
236 eci.setWebFacade(wfs)
237 // make the ScreenRender
238 ScreenRender screenRender = sti.sfi.makeRender()
239 stri.screenRender = screenRender
240 // pass through various settings
241 if (sti.rootScreenLocation != null && sti.rootScreenLocation.length() > 0) screenRender.rootScreen(sti.rootScreenLocation)
242 if (sti.outputType != null && sti.outputType.length() > 0) screenRender.renderMode(sti.outputType)
243 if (sti.characterEncoding != null && sti.characterEncoding.length() > 0) screenRender.encoding(sti.characterEncoding)
244 if (sti.macroTemplateLocation != null && sti.macroTemplateLocation.length() > 0) screenRender.macroTemplate(sti.macroTemplateLocation)
245 if (sti.baseLinkUrl != null && sti.baseLinkUrl.length() > 0) screenRender.baseLinkUrl(sti.baseLinkUrl)
246 if (sti.servletContextPath != null && sti.servletContextPath.length() > 0) screenRender.servletContextPath(sti.servletContextPath)
247 screenRender.webappName(sti.webappName)
248 if (sti.skipJsonSerialize) wfs.skipJsonSerialize = true
249
250 // set the screenPath
251 screenRender.screenPath(screenPathList)
252
253 // do the render
254 try {
255 screenRender.render(wfs.httpServletRequest, wfs.httpServletResponse)
256 // get the response text from our WebFacadeStub
257 stri.outputString = wfs.getResponseText()
258 stri.jsonObj = wfs.getResponseJsonObj()
259 System.out.println("STRI: " + stri.outputString + " : " + stri.jsonObj)
260 } catch (Throwable t) {
261 String errMsg = "Exception in render of ${stri.screenPath}: ${t.toString()}"
262 logger.warn(errMsg, t)
263 stri.errorMessages.add(errMsg)
264 sti.errorCount++
265 }
266 // calc renderTime
267 stri.renderTime = System.currentTimeMillis() - startTime
268
269 // pop the context stack, get rid of var space
270 stri.postRenderContext = cs.pop()
271
272 // check, pass through, error messages
273 if (eci.message.hasError()) {
274 stri.errorMessages.addAll(eci.message.getErrors())
275 eci.message.clearErrors()
276 StringBuilder sb = new StringBuilder("Error messages from ${stri.screenPath}: ")
277 for (String errorMessage in stri.errorMessages) sb.append("\n").append(errorMessage)
278 logger.warn(sb.toString())
279 sti.errorCount += stri.errorMessages.size()
280 }
281
282 // check for error strings in output
283 if (stri.outputString != null) for (String errorStr in sti.errorStrings) if (stri.outputString.contains(errorStr)) {
284 String errMsg = "Found error [${errorStr}] in output from ${stri.screenPath}"
285 stri.errorMessages.add(errMsg)
286 sti.errorCount++
287 logger.warn(errMsg)
288 }
289
290 // update stats
291 sti.renderCount++
292 if (stri.outputString != null) sti.totalChars += stri.outputString.length()
293 }
294
295 @Override ScreenRender getScreenRender() { return screenRender }
296 @Override String getOutput() { return outputString }
297 @Override Object getJsonObject() { return jsonObj }
298 @Override long getRenderTime() { return renderTime }
299 @Override Map getPostRenderContext() { return postRenderContext }
300 @Override List<String> getErrorMessages() { return errorMessages }
301
302 @Override
303 boolean assertContains(String text) {
304 if (!outputString) return false
305 return outputString.contains(text)
306 }
307 @Override
308 boolean assertNotContains(String text) {
309 if (!outputString) return true
310 return !outputString.contains(text)
311 }
312 @Override
313 boolean assertRegex(String regex) {
314 if (!outputString) return false
315 return outputString.matches(regex)
316 }
317 }
318 }
...\ No newline at end of file ...\ No newline at end of file
...@@ -33,6 +33,7 @@ class WebFacadeStub implements WebFacade { ...@@ -33,6 +33,7 @@ class WebFacadeStub implements WebFacade {
33 protected final Map<String, Object> parameters 33 protected final Map<String, Object> parameters
34 protected final Map<String, Object> sessionAttributes 34 protected final Map<String, Object> sessionAttributes
35 protected final String requestMethod 35 protected final String requestMethod
36 protected final String screenPath
36 37
37 protected HttpServletRequest httpServletRequest 38 protected HttpServletRequest httpServletRequest
38 protected HttpServletResponse httpServletResponse 39 protected HttpServletResponse httpServletResponse
...@@ -54,11 +55,12 @@ class WebFacadeStub implements WebFacade { ...@@ -54,11 +55,12 @@ class WebFacadeStub implements WebFacade {
54 boolean skipJsonSerialize = false 55 boolean skipJsonSerialize = false
55 56
56 WebFacadeStub(ExecutionContextFactoryImpl ecfi, Map<String, Object> parameters, 57 WebFacadeStub(ExecutionContextFactoryImpl ecfi, Map<String, Object> parameters,
57 Map<String, Object> sessionAttributes, String requestMethod) { 58 Map<String, Object> sessionAttributes, String requestMethod, String screenPath = null) {
58 this.ecfi = ecfi 59 this.ecfi = ecfi
59 this.parameters = parameters ?: [:] 60 this.parameters = parameters ?: [:]
60 this.sessionAttributes = sessionAttributes ?: [:] 61 this.sessionAttributes = sessionAttributes ?: [:]
61 this.requestMethod = requestMethod ?: "GET" 62 this.requestMethod = requestMethod ?: "GET"
63 this.screenPath = screenPath
62 64
63 // Create mock HTTP objects 65 // Create mock HTTP objects
64 createMockHttpObjects() 66 createMockHttpObjects()
...@@ -111,11 +113,23 @@ class WebFacadeStub implements WebFacade { ...@@ -111,11 +113,23 @@ class WebFacadeStub implements WebFacade {
111 } 113 }
112 114
113 @Override 115 @Override
114 String getPathInfo() { return "/test" } 116 String getPathInfo() {
117 // For standalone screens, return empty path to render the screen itself
118 // For screens with subscreen paths, return the relative path
119 return screenPath ? "/${screenPath}" : ""
120 }
115 121
116 @Override 122 @Override
117 ArrayList<String> getPathInfoList() { 123 ArrayList<String> getPathInfoList() {
118 return new ArrayList<String>(["test"]) 124 // IMPORTANT: Don't delegate to WebFacadeImpl - it expects real HTTP servlet context
125 // Return mock path info for MCP screen rendering based on actual screen path
126 def pathInfo = getPathInfo()
127 if (pathInfo && pathInfo.startsWith("/")) {
128 // Split path and filter out empty parts
129 def pathParts = pathInfo.substring(1).split("/") as List
130 return new ArrayList<String>(pathParts.findAll { it && it.toString().length() > 0 })
131 }
132 return new ArrayList<String>() // Empty for standalone screens
119 } 133 }
120 134
121 @Override 135 @Override
......