1 package org.apache.maven.plugin.surefire.booterclient.output;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.commons.codec.binary.Base64;
23 import org.apache.maven.surefire.booter.ForkedProcessEvent;
24 import org.apache.maven.surefire.report.ReportEntry;
25 import org.apache.maven.surefire.report.RunMode;
26 import org.apache.maven.surefire.report.StackTraceWriter;
27
28 import java.nio.charset.Charset;
29 import java.util.Collections;
30 import java.util.StringTokenizer;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.concurrent.ConcurrentMap;
33
34 import static java.nio.charset.StandardCharsets.US_ASCII;
35 import static org.apache.maven.surefire.booter.ForkedProcessEvent.MAGIC_NUMBER;
36 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDERR;
37 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDERR_NEW_LINE;
38 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDOUT;
39 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDOUT_NEW_LINE;
40 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_BYE;
41 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_DEBUG;
42 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_INFO;
43 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_WARNING;
44 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_NEXT_TEST;
45 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STOP_ON_NEXT_TEST;
46 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
47 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_ERROR;
48 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_FAILED;
49 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_SKIPPED;
50 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_STARTING;
51 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_SUCCEEDED;
52 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TESTSET_COMPLETED;
53 import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TESTSET_STARTING;
54 import static org.apache.maven.surefire.booter.ForkedProcessEvent.EVENTS;
55 import static org.apache.maven.surefire.report.CategorizedReportEntry.reportEntry;
56 import static org.apache.maven.surefire.report.RunMode.MODES;
57 import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
58 import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
59 import static java.util.Objects.requireNonNull;
60
61
62
63
64
65
66
67 public final class ForkedChannelDecoder
68 {
69 private static final Base64 BASE64 = new Base64();
70
71 private volatile ForkedProcessPropertyEventListener propertyEventListener;
72 private volatile ForkedProcessStackTraceEventListener consoleErrorEventListener;
73 private volatile ForkedProcessExitErrorListener exitErrorEventListener;
74
75 private final ConcurrentMap<ForkedProcessEvent, ForkedProcessReportEventListener<?>> reportEventListeners =
76 new ConcurrentHashMap<>();
77
78 private final ConcurrentMap<ForkedProcessEvent, ForkedProcessStandardOutErrEventListener> stdOutErrEventListeners =
79 new ConcurrentHashMap<>();
80
81 private final ConcurrentMap<ForkedProcessEvent, ForkedProcessStringEventListener> consoleEventListeners =
82 new ConcurrentHashMap<>();
83
84 private final ConcurrentMap<ForkedProcessEvent, ForkedProcessEventListener> controlEventListeners =
85 new ConcurrentHashMap<>();
86
87 public void setSystemPropertiesListener( ForkedProcessPropertyEventListener listener )
88 {
89 propertyEventListener = requireNonNull( listener );
90 }
91
92 public <T extends ReportEntry> void setTestSetStartingListener( ForkedProcessReportEventListener<T> listener )
93 {
94 reportEventListeners.put( BOOTERCODE_TESTSET_STARTING, requireNonNull( listener ) );
95 }
96
97 public void setTestSetCompletedListener( ForkedProcessReportEventListener<?> listener )
98 {
99 reportEventListeners.put( BOOTERCODE_TESTSET_COMPLETED, requireNonNull( listener ) );
100 }
101
102 public void setTestStartingListener( ForkedProcessReportEventListener<?> listener )
103 {
104 reportEventListeners.put( BOOTERCODE_TEST_STARTING, requireNonNull( listener ) );
105 }
106
107 public void setTestSucceededListener( ForkedProcessReportEventListener<?> listener )
108 {
109 reportEventListeners.put( BOOTERCODE_TEST_SUCCEEDED, requireNonNull( listener ) );
110 }
111
112 public void setTestFailedListener( ForkedProcessReportEventListener<?> listener )
113 {
114 reportEventListeners.put( BOOTERCODE_TEST_FAILED, requireNonNull( listener ) );
115 }
116
117 public void setTestSkippedListener( ForkedProcessReportEventListener<?> listener )
118 {
119 reportEventListeners.put( BOOTERCODE_TEST_SKIPPED, requireNonNull( listener ) );
120 }
121
122 public void setTestErrorListener( ForkedProcessReportEventListener<?> listener )
123 {
124 reportEventListeners.put( BOOTERCODE_TEST_ERROR, requireNonNull( listener ) );
125 }
126
127 public void setTestAssumptionFailureListener( ForkedProcessReportEventListener<?> listener )
128 {
129 reportEventListeners.put( BOOTERCODE_TEST_ASSUMPTIONFAILURE, requireNonNull( listener ) );
130 }
131
132 public void setStdOutListener( ForkedProcessStandardOutErrEventListener listener )
133 {
134 stdOutErrEventListeners.put( BOOTERCODE_STDOUT, requireNonNull( listener ) );
135 stdOutErrEventListeners.put( BOOTERCODE_STDOUT_NEW_LINE, requireNonNull( listener ) );
136 }
137
138 public void setStdErrListener( ForkedProcessStandardOutErrEventListener listener )
139 {
140 stdOutErrEventListeners.put( BOOTERCODE_STDERR, requireNonNull( listener ) );
141 stdOutErrEventListeners.put( BOOTERCODE_STDERR_NEW_LINE, requireNonNull( listener ) );
142 }
143
144 public void setConsoleInfoListener( ForkedProcessStringEventListener listener )
145 {
146 consoleEventListeners.put( BOOTERCODE_CONSOLE_INFO, requireNonNull( listener ) );
147 }
148
149 public void setConsoleErrorListener( ForkedProcessStackTraceEventListener listener )
150 {
151 consoleErrorEventListener = requireNonNull( listener );
152 }
153
154 public void setConsoleDebugListener( ForkedProcessStringEventListener listener )
155 {
156 consoleEventListeners.put( BOOTERCODE_CONSOLE_DEBUG, requireNonNull( listener ) );
157 }
158
159 public void setConsoleWarningListener( ForkedProcessStringEventListener listener )
160 {
161 consoleEventListeners.put( BOOTERCODE_CONSOLE_WARNING, requireNonNull( listener ) );
162 }
163
164 public void setByeListener( ForkedProcessEventListener listener )
165 {
166 controlEventListeners.put( BOOTERCODE_BYE, requireNonNull( listener ) );
167 }
168
169 public void setStopOnNextTestListener( ForkedProcessEventListener listener )
170 {
171 controlEventListeners.put( BOOTERCODE_STOP_ON_NEXT_TEST, requireNonNull( listener ) );
172 }
173
174 public void setAcquireNextTestListener( ForkedProcessEventListener listener )
175 {
176 controlEventListeners.put( BOOTERCODE_NEXT_TEST, requireNonNull( listener ) );
177 }
178
179 public void setExitErrorEventListener( ForkedProcessExitErrorListener listener )
180 {
181 exitErrorEventListener = requireNonNull( listener );
182 }
183
184 public void handleEvent( String line, ForkedChannelDecoderErrorHandler errorHandler )
185 {
186 if ( line == null || !line.startsWith( MAGIC_NUMBER ) )
187 {
188 errorHandler.handledError( line, null );
189 return;
190 }
191
192 StringTokenizer tokenizer = new StringTokenizer( line.substring( MAGIC_NUMBER.length() ), ":" );
193 String opcode = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
194 ForkedProcessEvent event = opcode == null ? null : EVENTS.get( opcode );
195 if ( event == null )
196 {
197 errorHandler.handledError( line, null );
198 return;
199 }
200
201 try
202 {
203 if ( event.isControlCategory() )
204 {
205 ForkedProcessEventListener listener = controlEventListeners.get( event );
206 if ( listener != null )
207 {
208 listener.handle();
209 }
210 }
211 else if ( event.isConsoleCategory() )
212 {
213 ForkedProcessStringEventListener listener = consoleEventListeners.get( event );
214 Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
215 if ( listener != null && encoding != null )
216 {
217 String msg = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
218 listener.handle( msg );
219 }
220 }
221 else if ( event.isConsoleErrorCategory() )
222 {
223 Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
224 if ( consoleErrorEventListener != null && encoding != null )
225 {
226 String msg = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : null;
227 String smartStackTrace =
228 tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : null;
229 String stackTrace = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : null;
230 consoleErrorEventListener.handle( msg, smartStackTrace, stackTrace );
231 }
232 }
233 else if ( event.isStandardStreamCategory() )
234 {
235 ForkedProcessStandardOutErrEventListener listener = stdOutErrEventListeners.get( event );
236 RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
237 Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
238 if ( listener != null && encoding != null && mode != null )
239 {
240 boolean newLine = event == BOOTERCODE_STDOUT_NEW_LINE || event == BOOTERCODE_STDERR_NEW_LINE;
241 String output = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
242 listener.handle( mode, output, newLine );
243 }
244 }
245 else if ( event.isSysPropCategory() )
246 {
247 RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
248 Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
249 String key = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
250 if ( propertyEventListener != null && isNotBlank( key ) )
251 {
252 String value = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
253 propertyEventListener.handle( mode, key, value );
254 }
255 }
256 else if ( event.isTestCategory() )
257 {
258 ForkedProcessReportEventListener listener = reportEventListeners.get( event );
259 RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
260 Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
261 if ( listener != null && encoding != null && mode != null )
262 {
263 String sourceName = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
264 String sourceText = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
265 String name = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
266 String nameText = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
267 String group = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
268 String message = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
269 String elapsed = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
270 String traceMessage = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
271 String smartTrimmedStackTrace = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
272 String stackTrace = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
273 ReportEntry reportEntry = toReportEntry( encoding, sourceName, sourceText, name, nameText,
274 group, message, elapsed, traceMessage, smartTrimmedStackTrace, stackTrace );
275 listener.handle( mode, reportEntry );
276 }
277 }
278 else if ( event.isJvmExitError() )
279 {
280 if ( exitErrorEventListener != null )
281 {
282 Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
283 String message = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
284 String smartTrimmedStackTrace =
285 tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
286 String stackTrace = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
287 exitErrorEventListener.handle( message, smartTrimmedStackTrace, stackTrace );
288 }
289 }
290 }
291 catch ( IllegalArgumentException e )
292 {
293 errorHandler.handledError( line, e );
294 }
295 }
296
297 static ReportEntry toReportEntry( Charset encoding,
298
299 String encSource, String encSourceText, String encName, String encNameText,
300 String encGroup, String encMessage, String encTimeElapsed,
301
302 String encTraceMessage, String encSmartTrimmedStackTrace, String encStackTrace )
303 throws NumberFormatException
304 {
305 if ( encoding == null )
306 {
307
308 return null;
309 }
310
311 String source = decode( encSource, encoding );
312 String sourceText = decode( encSourceText, encoding );
313 String name = decode( encName, encoding );
314 String nameText = decode( encNameText, encoding );
315 String group = decode( encGroup, encoding );
316 StackTraceWriter stackTraceWriter =
317 decodeTrace( encoding, encTraceMessage, encSmartTrimmedStackTrace, encStackTrace );
318 Integer elapsed = decodeToInteger( encTimeElapsed );
319 String message = decode( encMessage, encoding );
320 return reportEntry( source, sourceText, name, nameText,
321 group, stackTraceWriter, elapsed, message, Collections.<String, String>emptyMap() );
322 }
323
324 static String decode( String line, Charset encoding )
325 {
326
327 return line == null || "-".equals( line )
328 ? null
329 : new String( BASE64.decode( line.getBytes( US_ASCII ) ), encoding );
330 }
331
332 static Integer decodeToInteger( String line )
333 {
334 return line == null || "-".equals( line ) ? null : Integer.decode( line );
335 }
336
337 private static StackTraceWriter decodeTrace( Charset encoding, String encTraceMessage,
338 String encSmartTrimmedStackTrace, String encStackTrace )
339 {
340 if ( isBlank( encStackTrace ) || "-".equals( encStackTrace ) )
341 {
342 return null;
343 }
344 else
345 {
346 String traceMessage = decode( encTraceMessage, encoding );
347 String stackTrace = decode( encStackTrace, encoding );
348 String smartTrimmedStackTrace = decode( encSmartTrimmedStackTrace, encoding );
349 return new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace );
350 }
351 }
352 }