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