001/*
002 * Copyright (c) 2016 Chris K Wensel <chris@wensel.net>. All Rights Reserved.
003 * Copyright (c) 2007-2017 Xplenty, Inc. All Rights Reserved.
004 *
005 * Project and contact information: http://www.cascading.org/
006 *
007 * This file is part of the Cascading project.
008 *
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *     http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 */
021
022package cascading.util;
023
024import java.io.IOException;
025import java.io.LineNumberReader;
026import java.io.PrintWriter;
027import java.io.StringReader;
028import java.io.StringWriter;
029import java.io.Writer;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.List;
033import java.util.Map;
034import java.util.concurrent.ConcurrentHashMap;
035import java.util.regex.Pattern;
036
037import cascading.flow.FlowElement;
038import cascading.operation.BaseOperation;
039import cascading.operation.Operation;
040import cascading.pipe.Pipe;
041import cascading.scheme.Scheme;
042import cascading.tap.Tap;
043
044/**
045 *
046 */
047public class TraceUtil
048  {
049  /**
050   * The set of regex patterns specifying fully qualified class or package names that serve as boundaries
051   * for collecting traces and apiCalls in captureDebugTraceAndApiCall.
052   */
053  private static final Map<String, Pattern> registeredApiBoundaries = new ConcurrentHashMap<String, Pattern>();
054
055  private static interface TraceFormatter
056    {
057    String format( String trace );
058    }
059
060  /**
061   * Add a regex that serves as a boundary for tracing. That is to say any trace
062   * captured by {@link #captureDebugTrace(Object)} will be from a caller that comes higher in the
063   * stack than any apiBoundary package or class.
064   *
065   * @param apiBoundary
066   */
067  public static void registerApiBoundary( String apiBoundary )
068    {
069    registeredApiBoundaries.put( apiBoundary, Pattern.compile( apiBoundary ) );
070    }
071
072  /**
073   * Remove a regex as a boundary for tracing. See registerApiBoundary.
074   *
075   * @param apiBoundary
076   */
077  public static void unregisterApiBoundary( String apiBoundary )
078    {
079    registeredApiBoundaries.remove( apiBoundary );
080    }
081
082  /**
083   * Allows for custom trace fields on Pipe, Tap, and Scheme types
084   *
085   * @param object
086   * @param trace
087   */
088  public static void setTrace( Object object, String trace )
089    {
090    Util.setInstanceFieldIfExists( object, "trace", trace );
091    }
092
093  private static String formatTrace( Traceable traceable, String message, TraceFormatter formatter )
094    {
095    if( traceable == null )
096      return message;
097
098    String trace = traceable.getTrace();
099
100    if( trace == null )
101      return message;
102
103    return formatter.format( trace ) + " " + message;
104    }
105
106  public static String formatTraces( Collection<FlowElement> flowElements, String delim )
107    {
108    List<String> messages = new ArrayList<>( flowElements.size() );
109
110    for( FlowElement flowElement : flowElements )
111      messages.add( formatTrace( (Traceable) flowElement, flowElement.toString(), new TraceFormatter()
112        {
113        @Override
114        public String format( String trace )
115          {
116          return "[" + trace + "] ->";
117          }
118        } ) );
119
120    return Util.join( messages, delim );
121    }
122
123  public static String formatTrace( final Scheme scheme, String message )
124    {
125    return formatTrace( scheme, message, new TraceFormatter()
126      {
127      @Override
128      public String format( String trace )
129        {
130        return "[" + Util.truncate( scheme.toString(), 25 ) + "][" + trace + "]";
131        }
132      } );
133    }
134
135  public static String formatTrace( FlowElement flowElement, String message )
136    {
137    if( flowElement == null )
138      return message;
139
140    if( flowElement instanceof Pipe )
141      return formatTrace( (Pipe) flowElement, message );
142
143    if( flowElement instanceof Tap )
144      return formatTrace( (Tap) flowElement, message );
145
146    throw new UnsupportedOperationException( "cannot format type: " + flowElement.getClass().getName() );
147    }
148
149  /**
150   * Method formatRawTrace does not include the pipe name
151   *
152   * @param pipe    of type Pipe
153   * @param message of type String
154   * @return String
155   */
156  public static String formatRawTrace( Pipe pipe, String message )
157    {
158    return formatTrace( pipe, message, new TraceFormatter()
159      {
160      @Override
161      public String format( String trace )
162        {
163        return "[" + trace + "]";
164        }
165      } );
166    }
167
168  public static String formatTrace( final Pipe pipe, String message )
169    {
170    return formatTrace( pipe, message, new TraceFormatter()
171      {
172      @Override
173      public String format( String trace )
174        {
175        return "[" + Util.truncate( pipe.getName(), 25 ) + "][" + trace + "]";
176        }
177      } );
178    }
179
180  public static String formatTrace( final Tap tap, String message )
181    {
182    return formatTrace( tap, message, new TraceFormatter()
183      {
184      @Override
185      public String format( String trace )
186        {
187        return "[" + Util.truncate( tap.toString(), 25 ) + "][" + trace + "]";
188        }
189      } );
190    }
191
192  public static String formatTrace( Operation operation, String message )
193    {
194    if( !( operation instanceof BaseOperation ) )
195      return message;
196
197    String trace = ( (BaseOperation) operation ).getTrace();
198
199    if( trace == null )
200      return message;
201
202    return "[" + trace + "] " + message;
203    }
204
205  public static String captureDebugTrace( Object target )
206    {
207    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
208
209    StackTraceElement candidateUserCodeElement = null;
210    StackTraceElement apiCallElement = null;
211    Class<?> tracingBoundary = target.getClass();
212    String boundaryClassName = tracingBoundary.getName();
213
214    // walk from the bottom of the stack(which is at the end of the array) upwards towards any boundary.
215    // The apiCall is the element at the boundary and the previous stack element is the user code
216    for( int i = stackTrace.length - 1; i >= 0; i-- )
217      {
218      StackTraceElement stackTraceElement = stackTrace[ i ];
219      String stackClassName = stackTraceElement.getClassName();
220
221      boolean atApiBoundary = atApiBoundary( stackTraceElement.toString() );
222
223      if( ( stackClassName != null && ( stackClassName.startsWith( boundaryClassName ) ) || atApiBoundary ) )
224        {
225        // only record the apiCallElement if we're at an apiBoundary. That means
226        // Trace will only have "apiMethod() @ call_location" when a registered
227        // api boundary is found. The default for Cascading will just have
228        // call_location.
229        if( atApiBoundary )
230          apiCallElement = stackTraceElement;
231
232        break;
233        }
234
235      candidateUserCodeElement = stackTraceElement;
236      }
237
238    String userCode = candidateUserCodeElement == null ? "" : candidateUserCodeElement.toString();
239    String apiCall = "";
240
241    if( apiCallElement != null )
242      {
243      String method = apiCallElement.getMethodName();
244
245      if( method.equals( "<init>" ) )
246        apiCall = String.format( "new %s()", getSimpleClassName( apiCallElement.getClassName() ) );
247      else
248        apiCall = String.format( "%s()", method );
249      }
250
251    return userCode.isEmpty() ? apiCall : apiCall.isEmpty() ? userCode : String.format( "%s @ %s", apiCall, userCode );
252    }
253
254  private static Object getSimpleClassName( String className )
255    {
256    if( className == null || className.isEmpty() )
257      return "";
258
259    String parts[] = className.split( "\\." );
260
261    if( parts.length == 0 )
262      return "";
263
264    return parts[ parts.length - 1 ];
265    }
266
267  private static boolean atApiBoundary( String stackTraceElement )
268    {
269    for( Pattern boundary : registeredApiBoundaries.values() )
270      {
271      if( boundary.matcher( stackTraceElement ).matches() )
272        return true;
273      }
274
275    return false;
276    }
277
278  public static String stringifyStackTrace( Throwable throwable, String lineSeparator, boolean trimLines, int lineLimit )
279    {
280    if( lineLimit == 0 )
281      return null;
282
283    Writer traceWriter = new StringWriter();
284    PrintWriter printWriter = new PrintWriter( traceWriter );
285
286    throwable.printStackTrace( printWriter );
287
288    String trace = traceWriter.toString();
289
290    if( lineSeparator.equals( System.getProperty( "line.separator" ) ) && !trimLines && lineLimit == -1 )
291      return trace;
292
293    lineLimit = lineLimit == -1 ? Integer.MAX_VALUE : lineLimit;
294
295    StringBuilder buffer = new StringBuilder();
296    LineNumberReader reader = new LineNumberReader( new StringReader( trace ) );
297
298    try
299      {
300      String line = reader.readLine();
301
302      while( line != null && reader.getLineNumber() - 1 < lineLimit )
303        {
304        if( reader.getLineNumber() > 1 )
305          buffer.append( lineSeparator );
306
307        if( trimLines )
308          line = line.trim();
309
310        buffer.append( line );
311
312        line = reader.readLine();
313        }
314      }
315    catch( IOException exception )
316      {
317      // ignore - reading a string
318      }
319
320    return buffer.toString();
321    }
322  }