5b1380c7 by Ean Schuessler

Fix compilation error by creating MCP-specific screen test infrastructure

- Created McpScreenTest and McpScreenTestRender interfaces
- Rewrote CustomScreenTestImpl to implement MCP-specific interfaces instead of extending framework's buggy ScreenTestImpl
- Added all required interface methods for complete functionality
- Resolves @Override annotation error on non-existent makeWebFacade method
- Maintains MCP functionality while providing independent screen test capabilities
1 parent fefecf27
...@@ -13,19 +13,330 @@ ...@@ -13,19 +13,330 @@
13 */ 13 */
14 package org.moqui.mcp 14 package org.moqui.mcp
15 15
16 import org.moqui.impl.screen.ScreenTestImpl 16 import groovy.transform.CompileStatic
17 import org.moqui.impl.context.ExecutionContextFactoryImpl 17 import org.moqui.impl.context.ExecutionContextFactoryImpl
18 import org.moqui.impl.context.ExecutionContextImpl
19 import org.moqui.context.WebFacade
20 import org.moqui.util.ContextStack
21 import org.moqui.impl.screen.ScreenUrlInfo
22 import org.moqui.impl.screen.ScreenFacadeImpl
23 import org.moqui.impl.screen.ScreenDefinition
24 import org.moqui.screen.ScreenRender
25 import org.moqui.BaseArtifactException
26 import org.moqui.util.MNode
27 import org.slf4j.Logger
28 import org.slf4j.LoggerFactory
18 29
19 /** 30 /**
20 * Custom ScreenTest implementation for MCP access 31 * MCP-specific ScreenTest implementation for simulating screen web requests
21 * This provides the necessary web context for screen rendering in MCP environment 32 * This provides a proper web context for screen rendering in MCP environment
33 * using the MCP component's WebFacadeStub instead of the framework's buggy one
22 */ 34 */
23 class CustomScreenTestImpl extends ScreenTestImpl { 35 @CompileStatic
36 class CustomScreenTestImpl implements McpScreenTest {
24 37
38 protected final static Logger logger = LoggerFactory.getLogger(CustomScreenTestImpl.class)
39
40 protected final ExecutionContextFactoryImpl ecfi
41 protected final ScreenFacadeImpl sfi
42 // see FtlTemplateRenderer.MoquiTemplateExceptionHandler, others
43 final List<String> errorStrings = ["[Template Error", "FTL stack trace", "Could not find subscreen or transition"]
44
45 protected String rootScreenLocation = null
46 protected ScreenDefinition rootScreenDef = null
47 protected String baseScreenPath = null
48 protected List<String> baseScreenPathList = null
49 protected ScreenDefinition baseScreenDef = null
50
51 protected String outputType = null
52 protected String characterEncoding = null
53 protected String macroTemplateLocation = null
54 protected String baseLinkUrl = null
55 protected String servletContextPath = null
56 protected String webappName = null
57 protected boolean skipJsonSerialize = false
58 protected static final String hostname = "localhost"
59
60 long renderCount = 0, errorCount = 0, totalChars = 0, startTime = System.currentTimeMillis()
61
62 final Map<String, Object> sessionAttributes = [:]
63
25 CustomScreenTestImpl(ExecutionContextFactoryImpl ecfi) { 64 CustomScreenTestImpl(ExecutionContextFactoryImpl ecfi) {
26 super(ecfi) 65 this.ecfi = ecfi
66 sfi = ecfi.screenFacade
67
68 // init default webapp, root screen
69 webappName('webroot')
70 }
71
72 /**
73 * Create WebFacade using our properly implemented WebFacadeStub
74 * instead of the framework's version that has null contextPath issues
75 */
76 protected WebFacade createWebFacade(ExecutionContextFactoryImpl ecfi, Map<String, Object> parameters,
77 Map<String, Object> sessionAttributes, String requestMethod) {
78 if (logger.isDebugEnabled()) {
79 logger.debug("CustomScreenTestImpl.createWebFacade() called with parameters: ${parameters?.keySet()}, sessionAttributes: ${sessionAttributes?.keySet()}, requestMethod: ${requestMethod}")
80 }
81
82 // Use our MCP component's WebFacadeStub which properly handles null contextPath
83 return new org.moqui.mcp.WebFacadeStub(ecfi, parameters, sessionAttributes, requestMethod)
84 }
85
86 @Override
87 McpScreenTest rootScreen(String screenLocation) {
88 rootScreenLocation = screenLocation
89 rootScreenDef = sfi.getScreenDefinition(rootScreenLocation)
90 if (rootScreenDef == null) throw new IllegalArgumentException("Root screen not found: ${rootScreenLocation}")
91 baseScreenDef = rootScreenDef
92 return this
27 } 93 }
28 94
29 // Use the default makeWebFacade from ScreenTestImpl 95 @Override
30 // It should work for basic screen rendering in MCP context 96 McpScreenTest baseScreenPath(String screenPath) {
97 if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified")
98 baseScreenPath = screenPath
99 if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1)
100 if (baseScreenPath) {
101 baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, [], baseScreenPath, null, sfi)
102 if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}")
103 for (String screenName in baseScreenPathList) {
104 ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName)
105 if (ssi == null) throw new BaseArtifactException("Error in baseScreenPath, could not find ${screenName} under ${baseScreenDef.location}")
106 baseScreenDef = sfi.getScreenDefinition(ssi.location)
107 if (baseScreenDef == null) throw new BaseArtifactException("Error in baseScreenPath, could not find screen ${screenName} at ${ssi.location}")
108 }
109 }
110 return this
111 }
112
113 @Override McpScreenTest renderMode(String outputType) { this.outputType = outputType; return this }
114 @Override McpScreenTest encoding(String characterEncoding) { this.characterEncoding = characterEncoding; return this }
115 @Override McpScreenTest macroTemplate(String macroTemplateLocation) { this.macroTemplateLocation = macroTemplateLocation; return this }
116 @Override McpScreenTest baseLinkUrl(String baseLinkUrl) { this.baseLinkUrl = baseLinkUrl; return this }
117 @Override McpScreenTest servletContextPath(String scp) { this.servletContextPath = scp; return this }
118 @Override McpScreenTest skipJsonSerialize(boolean skip) { this.skipJsonSerialize = skip; return this }
119
120 @Override
121 McpScreenTest webappName(String wan) {
122 webappName = wan
123
124 // set a default root screen based on config for "localhost"
125 MNode webappNode = ecfi.getWebappNode(webappName)
126 for (MNode rootScreenNode in webappNode.children("root-screen")) {
127 if (hostname.matches(rootScreenNode.attribute('host'))) {
128 String rsLoc = rootScreenNode.attribute('location')
129 rootScreen(rsLoc)
130 break
131 }
132 }
133
134 return this
135 }
136
137 @Override
138 List<String> getNoRequiredParameterPaths(Set<String> screensToSkip) {
139 if (!rootScreenLocation) throw new IllegalStateException("No rootScreen specified")
140
141 List<String> noReqParmLocations = baseScreenDef.nestedNoReqParmLocations("", screensToSkip)
142 // logger.info("======= rootScreenLocation=${rootScreenLocation}\nbaseScreenPath=${baseScreenPath}\nbaseScreenDef: ${baseScreenDef.location}\nnoReqParmLocations: ${noReqParmLocations}")
143 return noReqParmLocations
144 }
145
146 @Override
147 void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod) {
148 // NOTE: using single thread for now, doesn't actually make a lot of difference in overall test run time
149 int threads = 1
150 if (threads == 1) {
151 for (String screenPath in screenPathList) {
152 McpScreenTestRender str = render(screenPath, parameters, requestMethod)
153 logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.getOutput()?.length()} characters")
154 }
155 } else {
156 ExecutionContextImpl eci = ecfi.getEci()
157 ArrayList<java.util.concurrent.Future> threadList = new ArrayList<java.util.concurrent.Future>(threads)
158 int screenPathListSize = screenPathList.size()
159 for (int si = 0; si < screenPathListSize; si++) {
160 String screenPath = (String) screenPathList.get(si)
161 threadList.add(eci.runAsync({
162 McpScreenTestRender str = render(screenPath, parameters, requestMethod)
163 logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.getOutput()?.length()} characters")
164 }))
165 if (threadList.size() == threads || (si + 1) == screenPathList.size()) {
166 for (int i = 0; i < threadList.size(); i++) { ((java.util.concurrent.Future) threadList.get(i)).get() }
167 threadList.clear()
168 }
169 }
170 }
171 }
172
173 long getRenderCount() { return renderCount }
174 long getErrorCount() { return errorCount }
175 long getRenderTotalChars() { return totalChars }
176 long getStartTime() { return startTime }
177
178 /**
179 * Override render method to use our custom ScreenTestRenderImpl
180 */
181 @Override
182 McpScreenTestRender render(String screenPath, Map<String, Object> parameters, String requestMethod) {
183 if (!rootScreenLocation) throw new IllegalArgumentException("No rootScreenLocation specified")
184 return new CustomScreenTestRenderImpl(this, screenPath, parameters, requestMethod).render()
185 }
186
187 /**
188 * Custom ScreenTestRenderImpl that uses our WebFacadeStub
189 */
190 @CompileStatic
191 static class CustomScreenTestRenderImpl implements McpScreenTestRender {
192 protected final CustomScreenTestImpl sti
193 String screenPath = (String) null
194 Map<String, Object> parameters = [:]
195 String requestMethod = (String) null
196
197 ScreenRender screenRender = (ScreenRender) null
198 String outputString = (String) null
199 Object jsonObj = null
200 long renderTime = 0
201 Map postRenderContext = (Map) null
202 protected List<String> errorMessages = []
203
204 CustomScreenTestRenderImpl(CustomScreenTestImpl sti, String screenPath, Map<String, Object> parameters, String requestMethod) {
205 this.sti = sti
206 this.screenPath = screenPath
207 if (parameters != null) this.parameters.putAll(parameters)
208 this.requestMethod = requestMethod
209 }
210
211 McpScreenTestRender render() {
212 // render in separate thread with an independent ExecutionContext so it doesn't muck up the current one
213 ExecutionContextFactoryImpl ecfi = sti.ecfi
214 ExecutionContextImpl localEci = ecfi.getEci()
215 String username = localEci.userFacade.getUsername()
216 org.apache.shiro.subject.Subject loginSubject = localEci.userFacade.getCurrentSubject()
217 boolean authzDisabled = localEci.artifactExecutionFacade.getAuthzDisabled()
218 CustomScreenTestRenderImpl stri = this
219 Throwable threadThrown = null
220
221 Thread newThread = new Thread("CustomScreenTestRender") {
222 @Override void run() {
223 try {
224 ExecutionContextImpl threadEci = ecfi.getEci()
225 if (loginSubject != null) threadEci.userFacade.internalLoginSubject(loginSubject)
226 else if (username != null && !username.isEmpty()) threadEci.userFacade.internalLoginUser(username)
227 if (authzDisabled) threadEci.artifactExecutionFacade.disableAuthz()
228 // as this is used for server-side transition calls don't do tarpit checks
229 threadEci.artifactExecutionFacade.disableTarpit()
230 renderInternalCustom(threadEci, stri)
231 threadEci.destroy()
232 } catch (Throwable t) {
233 threadThrown = t
234 }
235 }
236 }
237 newThread.start()
238 newThread.join()
239 if (threadThrown != null) throw threadThrown
240 return this
241 }
242
243 private static void renderInternalCustom(ExecutionContextImpl eci, CustomScreenTestRenderImpl stri) {
244 CustomScreenTestImpl csti = stri.sti
245 long startTime = System.currentTimeMillis()
246
247 // parse the screenPath
248 ArrayList<String> screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef,
249 csti.baseScreenPathList, stri.screenPath, stri.parameters, csti.sfi)
250 if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${csti.baseScreenDef.location}")
251
252 // push the context
253 ContextStack cs = eci.getContext()
254 cs.push()
255 // create the WebFacadeStub using our custom method
256 org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod)
257 // set stub on eci, will also put parameters in the context
258 eci.setWebFacade(wfs)
259 // make the ScreenRender
260 ScreenRender screenRender = csti.sfi.makeRender()
261 stri.screenRender = screenRender
262 // pass through various settings
263 if (csti.rootScreenLocation != null && csti.rootScreenLocation.length() > 0) screenRender.rootScreen(csti.rootScreenLocation)
264 if (csti.outputType != null && csti.outputType.length() > 0) screenRender.renderMode(csti.outputType)
265 if (csti.characterEncoding != null && csti.characterEncoding.length() > 0) screenRender.encoding(csti.characterEncoding)
266 if (csti.macroTemplateLocation != null && csti.macroTemplateLocation.length() > 0) screenRender.macroTemplate(csti.macroTemplateLocation)
267 if (csti.baseLinkUrl != null && csti.baseLinkUrl.length() > 0) screenRender.baseLinkUrl(csti.baseLinkUrl)
268 if (csti.servletContextPath != null && csti.servletContextPath.length() > 0) screenRender.servletContextPath(csti.servletContextPath)
269 screenRender.webappName(csti.webappName)
270 if (csti.skipJsonSerialize) {
271 // Set skipJsonSerialize on our WebFacadeStub
272 wfs.skipJsonSerialize = true
273 }
274
275 // set the screenPath
276 screenRender.screenPath(screenPathList)
277
278 // do the render
279 try {
280 screenRender.render(wfs.getRequest(), wfs.getResponse())
281 // get the response text from the WebFacadeStub
282 stri.outputString = wfs.getResponseText()
283 stri.jsonObj = wfs.getResponseJsonObj()
284 } catch (Throwable t) {
285 String errMsg = "Exception in render of ${stri.screenPath}: ${t.toString()}"
286 logger.warn(errMsg, t)
287 stri.errorMessages.add(errMsg)
288 csti.errorCount++
289 }
290 // calc renderTime
291 stri.renderTime = System.currentTimeMillis() - startTime
292
293 // pop the context stack, get rid of var space
294 stri.postRenderContext = cs.pop()
295
296 // check, pass through, error messages
297 if (eci.message.hasError()) {
298 stri.errorMessages.addAll(eci.message.getErrors())
299 eci.message.clearErrors()
300 StringBuilder sb = new StringBuilder("Error messages from ${stri.screenPath}: ")
301 for (String errorMessage in stri.errorMessages) sb.append("\n").append(errorMessage)
302 logger.warn(sb.toString())
303 csti.errorCount += stri.errorMessages.size()
304 }
305
306 // check for error strings in output
307 if (stri.outputString != null) for (String errorStr in csti.errorStrings) if (stri.outputString.contains(errorStr)) {
308 String errMsg = "Found error [${errorStr}] in output from ${stri.screenPath}"
309 stri.errorMessages.add(errMsg)
310 csti.errorCount++
311 logger.warn(errMsg)
312 }
313
314 // update stats
315 csti.renderCount++
316 if (stri.outputString != null) csti.totalChars += stri.outputString.length()
317 }
318
319 @Override ScreenRender getScreenRender() { return screenRender }
320 @Override String getOutput() { return outputString }
321 @Override Object getJsonObject() { return jsonObj }
322 @Override long getRenderTime() { return renderTime }
323 @Override Map getPostRenderContext() { return postRenderContext }
324 @Override List<String> getErrorMessages() { return errorMessages }
325
326 @Override
327 boolean assertContains(String text) {
328 if (!outputString) return false
329 return outputString.contains(text)
330 }
331 @Override
332 boolean assertNotContains(String text) {
333 if (!outputString) return true
334 return !outputString.contains(text)
335 }
336 @Override
337 boolean assertRegex(String regex) {
338 if (!outputString) return false
339 return outputString.matches(regex)
340 }
341 }
31 } 342 }
...\ No newline at end of file ...\ No newline at end of file
......
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
18 /**
19 * MCP-specific ScreenTest interface for simulating screen web requests
20 * This is separate from the core Moqui ScreenTest system and tailored for MCP needs
21 */
22 @CompileStatic
23 interface McpScreenTest {
24 McpScreenTest rootScreen(String screenLocation)
25 McpScreenTest baseScreenPath(String screenPath)
26 McpScreenTest renderMode(String outputType)
27 McpScreenTest encoding(String characterEncoding)
28 McpScreenTest macroTemplate(String macroTemplateLocation)
29 McpScreenTest baseLinkUrl(String baseLinkUrl)
30 McpScreenTest servletContextPath(String scp)
31 McpScreenTest skipJsonSerialize(boolean skip)
32 McpScreenTest webappName(String wan)
33
34 McpScreenTestRender render(String screenPath, Map<String, Object> parameters, String requestMethod)
35 void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod)
36
37 List<String> getNoRequiredParameterPaths(Set<String> screensToSkip)
38
39 long getRenderCount()
40 long getErrorCount()
41 long getRenderTotalChars()
42 long getStartTime()
43 }
...\ No newline at end of file ...\ No newline at end of file
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.moqui.screen.ScreenRender
18
19 /**
20 * MCP-specific ScreenTestRender interface for screen rendering results
21 * This is separate from the core Moqui ScreenTest system and tailored for MCP needs
22 */
23 @CompileStatic
24 interface McpScreenTestRender {
25 ScreenRender getScreenRender()
26 String getOutput()
27 Object getJsonObject()
28 long getRenderTime()
29 Map getPostRenderContext()
30 List<String> getErrorMessages()
31
32 boolean assertContains(String text)
33 boolean assertNotContains(String text)
34 boolean assertRegex(String regex)
35 }
...\ No newline at end of file ...\ No newline at end of file