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