View Javadoc
1   package org.apache.maven.plugin.surefire.booterclient.output;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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   * magic number : run mode : opcode [: opcode specific data]*
63   *
64   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
65   * @since 3.0.0-M4
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                    // ReportEntry:
299                    String encSource, String encSourceText, String encName, String encNameText,
300                                       String encGroup, String encMessage, String encTimeElapsed,
301                    // StackTraceWriter:
302                    String encTraceMessage, String encSmartTrimmedStackTrace, String encStackTrace )
303             throws NumberFormatException
304     {
305         if ( encoding == null )
306         {
307             // corrupted or incomplete stream
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         // ForkedChannelEncoder is encoding the stream with US_ASCII
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 }