    001/*
    002 * Copyright (c) 2007-2016 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  }