/******************************************************************************* * Copyright 2014 Miami-Dade County * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package org.sharegov.cirm.utils; import static mjson.Json.object; import java.beans.PropertyDescriptor; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.URL; import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.bind.DatatypeConverter; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import mjson.Json; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.io.IOUtils; import org.hypergraphdb.type.BonesOfBeans; //import com.google.gson.*; import org.semanticweb.owlapi.model.OWLIndividual; import org.semanticweb.owlapi.model.OWLLiteral; import org.sharegov.cirm.CirmTransaction; import org.sharegov.cirm.OWL; import org.sharegov.cirm.Refs; import org.sharegov.cirm.StartUp; import org.sharegov.cirm.SysRefs; import org.sharegov.cirm.legacy.MessageManager; import org.sharegov.cirm.rest.RestServiceAdmin; import org.w3c.dom.Document; public class GenUtils { public static final String TIMETASK_NOTRANS_MARKER = "NOTRANS"; private static final ThreadLocal<SimpleDateFormat> ISO_DATE_FORMATS = new ThreadLocal<SimpleDateFormat>(); public static final String isoDatePattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; public static final String SERVER_NAME_2 = SysRefs.serverName2.resolve(); public static URL makeLocalURL(String relativePath) { try { if(StartUp.getConfig().is("ssl",true)) { return new URL("https://" + InetAddress.getLocalHost().getHostName().toLowerCase() + ":" + StartUp.getConfig().at("ssl-port") + relativePath); }else { return new URL("http://" + InetAddress.getLocalHost().getHostName().toLowerCase() + ":" + StartUp.getConfig().at("port") + relativePath); } } catch (Exception e) { throw new RuntimeException(e); } } public static String readString(Reader reader) throws IOException { StringBuilder sb = new StringBuilder(); BufferedReader b = new BufferedReader(reader); for (String l = b.readLine(); l != null; l = b.readLine()) sb.append(l); return sb.toString(); } public static String serializeAsString(Object x) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); out.writeObject(x); out.close(); return DatatypeConverter.printBase64Binary(bos.toByteArray()); } catch (Exception ex) { throw new RuntimeException(ex); } } public static Object deserializeFromString(String x) { try { byte[] A = DatatypeConverter.parseBase64Binary(x); ByteArrayInputStream bin = new ByteArrayInputStream(A); ObjectInputStream in = new ObjectInputStream(bin); return in.readObject(); } catch (Exception t) { throw new RuntimeException(t); } } public static String readTextFile(File f) { try { FileReader reader = new FileReader(f); StringBuilder sb = new StringBuilder(); try { char[] buf = new char[4096]; for (int cnt = reader.read(buf); cnt > -1; cnt = reader.read(buf)) sb.append(buf, 0, cnt); } finally { reader.close(); } return sb.toString(); } catch (IOException ex) { throw new RuntimeException(ex); } } @SuppressWarnings("unchecked") public static <T> T cloneBean(T bean) { try { T clone = (T) bean.getClass().newInstance(); for (PropertyDescriptor desc : BonesOfBeans .getAllPropertyDescriptors(bean).values()) { if (desc.getReadMethod() == null || desc.getWriteMethod() == null) continue; BonesOfBeans.setProperty(clone, desc, BonesOfBeans.getProperty(bean, desc)); } return clone; } catch (Exception ex) { throw new RuntimeException(ex); } } private static volatile boolean dbgLevelTracing = false; public static boolean dbg() { return dbgLevelTracing; } public static void dbg(boolean newdbgLevelTracing) { dbgLevelTracing = newdbgLevelTracing; } public static Json ok() { return Json.object("ok", true, "server", SERVER_NAME_2); } public static void pagination(Json paginationJson, Json paginationCriteria) { int currentPage = paginationCriteria.at("currentPage").asInteger(); int itemsPerPage = paginationCriteria.at("itemsPerPage").asInteger(); int minValue = ((currentPage - 1) * itemsPerPage) + 1; int maxValue = (minValue - 1) + itemsPerPage; paginationJson.set("minValue", minValue); paginationJson.set("maxValue", maxValue); } public static String trim(String str) { if (str == null || str.equals("null") || str.equals(null)) return "N/A"; else return str.trim(); } public static Json ko(String error) { return Json.object("ok", false, "error", error, "server", SERVER_NAME_2); } public static Json ko(Throwable t) { return Json.object("ok", false, "error", t.toString(), "stackTrace", stackTrace(t), "server", SERVER_NAME_2); } public static byte[] getBytesFromFile(File file) throws IOException { return getBytesFromStream(new FileInputStream(file), true); } // Returns the contents of the file in a byte array. public static byte[] getBytesFromStream(InputStream is, boolean close) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { byte[] A = new byte[4096]; // Read in the bytes for (int cnt = is.read(A); cnt > -1; cnt = is.read(A)) out.write(A, 0, cnt); return out.toByteArray(); // Close the input stream and return bytes } finally { if (close) is.close(); } } public static Json httpGetJson(String url) { HttpClient client = new HttpClient(); GetMethod method = new GetMethod(url); try { // disable retries from within the HTTP client client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(0, false)); int statusCode = client.executeMethod(method); if (statusCode != HttpStatus.SC_OK) throw new RuntimeException("HTTP Error " + statusCode + " while calling " + url.toString()); return Json.read(method.getResponseBodyAsString()); } catch (Exception ex) { throw new RuntimeException(ex); } finally { method.releaseConnection(); } } public static String httpDelete(String url, String...headers) { HttpClient client = new HttpClient(); DeleteMethod method = new DeleteMethod(url); if (headers != null) { if (headers.length % 2 != 0) throw new IllegalArgumentException("Odd number of headers argument, specify HTTP headers in pairs: name then value, etc."); for (int i = 0; i < headers.length; i++) method.addRequestHeader(headers[i], headers[++i]); } try { // disable retries from within the HTTP client client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(0, false)); int statusCode = client.executeMethod(method); if (statusCode != HttpStatus.SC_OK) throw new RuntimeException("HTTP Error " + statusCode + " while deleting " + url.toString()); return method.getResponseBodyAsString(); } catch (Exception ex) { throw new RuntimeException(ex); } finally { method.releaseConnection(); } } @SuppressWarnings("deprecation") public static String httpPost(String url, String data, String...headers) { HttpClient client = new HttpClient(); PostMethod method = new PostMethod(url); if (headers != null) { if (headers.length % 2 != 0) throw new IllegalArgumentException("Odd number of headers argument, specify HTTP headers in pairs: name then value, etc."); for (int i = 0; i < headers.length; i++) method.addRequestHeader(headers[i], headers[++i]); } method.setRequestBody(data); try { // disable retries from within the HTTP client client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(0, false)); client.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 0); int statusCode = client.executeMethod(method); if (statusCode != HttpStatus.SC_OK) throw new RuntimeException("HTTP Error " + statusCode + " while post to " + url.toString() + ", body " + data); return method.getResponseBodyAsString(); } catch (Exception ex) { throw new RuntimeException(ex); } finally { method.releaseConnection(); } } public static Json httpPostWithBasicAuth(String url, String username, String password, String postData, String...headers) { try { URL uri = new URL (url); String credentials = username + ":" + password; String encoding = Base64.encode(credentials.getBytes(), false); HttpURLConnection connection = (HttpURLConnection) uri.openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setRequestProperty ("Authorization", "Basic " + encoding); if (headers != null) { if (headers.length % 2 != 0) throw new IllegalArgumentException("Odd number of headers argument, specify HTTP headers in pairs: name then value, etc."); for (int i = 0; i < headers.length; i++) connection.setRequestProperty (headers[i], headers[++i]); } if (postData != null && !postData.isEmpty()){ OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream()); wr.write(postData); wr.flush(); } int HttpResult = connection.getResponseCode(); connection.disconnect(); Json result = Json.object().set("response_code", HttpResult); if(HttpResult == HttpURLConnection.HTTP_OK){ return result.set("response", IOUtils.toString((InputStream)connection.getInputStream())); } else return result; } catch(Exception e) { return Json.object().set("error", e.getMessage()); } } public static Json httpPostJson(String url, Json json) { return Json.read(httpPost(url, json.toString(), "Content-Type", "application/json")); } public static Document httpPostXml(String url, Document xml) { return XMLU.parse(httpPost(url, XMLU.stringify(xml), "Content-Type", "text/xml")); } /** * Return the string representation of a Json structure that is normalized * so that two Json's will yield the same string representation iff they are * equal. This is done simply by stringify-ing Json objects with the * properties ordered by name. The other Json types already have an * unambiguous unique format. */ public static String normalizeAsString(Json data) { if (!data.isObject()) return data.toString(); StringBuilder sb = new StringBuilder("{"); ArrayList<String> props = new ArrayList<String>(data.asJsonMap() .keySet()); Collections.sort(props); for (int i = 0; i < props.size(); i++) { String p = props.get(i); sb.append("\"" + p + "\":" + normalizeAsString(data.at(p))); if (i < props.size() - 1) sb.append(","); } sb.append("}"); return sb.toString(); } public static String readTextResource(String resource) { InputStream in = GenUtils.class.getResourceAsStream(resource); if (in == null) return null; else try { return new String(getBytesFromStream(in, true)); } catch (IOException e) { throw new RuntimeException(e); } } private static Pattern whiteSpacePattern = Pattern.compile(".*\\s+.*"); public static boolean containsWhiteSpace(String value) { return whiteSpacePattern.matcher(value).matches(); } public static <T> Set<T> set(T... elements) { HashSet<T> S = new HashSet<T>(); for (T x : elements) S.add(x); return S; } private static DatatypeFactory xmlDatatypeFactory; public static java.util.Date parseDate(OWLLiteral literal) { Date result = new Date(); String value = literal.getLiteral(); if(xmlDatatypeFactory == null) { try { xmlDatatypeFactory = DatatypeFactory.newInstance(); } catch (DatatypeConfigurationException e) { throw new RuntimeException("Failed to create xmlDataFactory", e); } } try { // parse ISO 8601 date synchronized (xmlDatatypeFactory) { try { result = xmlDatatypeFactory.newXMLGregorianCalendar(value) .toGregorianCalendar().getTime(); } catch (IllegalArgumentException t) { result = parseDate(value); } } } catch (Exception e) { ThreadLocalStopwatch.getWatch().time("Error: Could not parse date " + value + " as ISO 8601"); throw new RuntimeException(e); } return result; } public static java.util.Date parseDate(String s, String format) { try { SimpleDateFormat fmt = new SimpleDateFormat(format); return fmt.parse(s); } catch (ParseException ex) { throw new RuntimeException(ex); } } /** * Parses a date string in iso format. * Fully thread safe and non blocking, by keeping one date format per thread. * * @param s * @return */ public static java.util.Date parseDate(String isoDateString) { try { SimpleDateFormat myDateFormat = ISO_DATE_FORMATS.get(); if (myDateFormat == null) { myDateFormat = new SimpleDateFormat(isoDatePattern); ISO_DATE_FORMATS.set(myDateFormat); } return myDateFormat.parse(isoDateString); } catch (ParseException ex) { throw new RuntimeException(ex); } } /** * Formats a date in iso format. * Fully thread safe and non blocking, by keeping one date format per thread. * * @param d * @return a date as iso formatted string. */ public static String formatDate(java.util.Date d) { SimpleDateFormat myDateFormat = ISO_DATE_FORMATS.get(); if (myDateFormat == null) { myDateFormat = new SimpleDateFormat(isoDatePattern); ISO_DATE_FORMATS.set(myDateFormat); } return myDateFormat.format(d); } /** * This method returns the timestamp in the given format. * In case of null timestamp and exceptions return "N/A" */ public static String formatDate(Timestamp ts, String format) { SimpleDateFormat sdf = new SimpleDateFormat(format); try { if (ts == null) return "N/A"; else return sdf.format(ts); } catch (Exception e) { return "N/A"; } } /** * This method creates the Service Case Number in 'YY-1XXXXXXX' format * YY: 2 digit year * Starts with '1': identifies this Service Case was created via CiRM * * @param id : sequence No. generated by the database * @return */ public static String makeCaseNumber(long id) { int length = 7; String startsWith = "1"; String reqFormat = String.format("%%0%dd", length); String result = String.format(reqFormat, id); StringBuilder sb = new StringBuilder(); //sb.append("AC"); String year = Integer.toString(Calendar.getInstance().get(Calendar.YEAR)); //sb.append(year); sb.append(year.substring(year.length()-2)); sb.append("-"); sb.append(startsWith); sb.append(result); return sb.toString(); } public static void ensureArray(Json j, String field) { Json f = j.at(field); if (f == null) j.set(field, Json.array()); else if (!f.isArray()) j.set(field, Json.array().add(f)); } public static void timeStamp(Json A) { //java.text.DateFormat format = new java.text.SimpleDateFormat("MM/dd/yyyy HH:mm:ss a"); if (A == null || A.isPrimitive()) return; else if (A.isObject() && !A.has("hasDateCreated")) A.set("hasDateCreated", GenUtils.formatDate(new java.util.Date())); else if (A.isArray()) for (Json x : A.asJsonList()) timeStamp(x); //A.set("hasDateCreate", new Date().) } public static String stackTrace(Throwable t) { java.io.StringWriter strWriter = new java.io.StringWriter(); java.io.PrintWriter prWriter = new PrintWriter(strWriter); t.printStackTrace(prWriter); prWriter.flush(); return strWriter.toString(); } public static Throwable getRootCause(Throwable t) { if (t != null) while (t.getCause() != null) t = t.getCause(); return t; } public static void rethrowRuntime(Throwable t) { if (t instanceof RuntimeException) throw (RuntimeException)t; else throw new RuntimeException(t); } /** * Ensures that a string starts with a capitalized letter. * @param s accepts a string, null, empty * @return */ public static String capitalize(String s) { if (s == null || s.isEmpty()) return s; char first = s.charAt(0); if (Character.isLetter(first) && Character.isLowerCase(first)) return Character.toUpperCase(first) + s.substring(1); else return s; } public static void reportPWGisProblem(String caseNumber, Json error) { ThreadLocalStopwatch.getWatch().time("reportPWGisProblem email sent: " + caseNumber); String body = "<p>Case " + caseNumber + " has invalid extra GIS info.</p>"; body += "<p>" + error + "</p>"; MessageManager.get().sendEmail("cirm@miamidade.gov", "angel.martin@miamidade.gov;silval@miamidade.gov", "[PW GIS ISSUE] " + caseNumber, body); } public static void reportFatal(String subject, String msg, Throwable t) { ThreadLocalStopwatch.getWatch().time("ReportFatal email sent: " + msg + " " + t); logStackTrace(t.getStackTrace(), 10); OWLLiteral recipient = Refs.configSet.resolve().get("FatalErrorEmail"); if (recipient == null) return; String body = "<p>Exception message:</p><p><b>" + msg + "</b></p>"; body += "<p>" + new RestServiceAdmin().sysInfo().toString() + "</p>"; if (t != null) body += "<hr><p>Stack Trace:</p>" + "<pre>" + stackTrace(t) + "</pre>"; if (subject == null) subject = ""; MessageManager.get().sendEmail("cirm@miamidade.gov", recipient.getLiteral(), "[CIRM FATAL] " + subject, body); } /** * <p> * Ask the TimeServer to call back the <code>url</code> in <code>minutesFromNow</code> minutes. * If the <code>url</code> parameter starts with 'http' it is used as is, otherwise, the * <code>OperationsRestService</code> from the ontology is used. The callback will be an HTTP POST * if the <code>post</code> parameter is not null. * taskId will be url + minutesFromNow + transUUID. url + minutesFromNow should be the same for each transaction retry but unique during one. * </p> * * @param minutesFromNow * @param url * @param post * @return */ public static Json timeTask(int minutesFromNow, String url, Json post) { String taskId; UUID cirmTransactionUUID; long transactionBeginTime; if (CirmTransaction.isExecutingOnThisThread()) { cirmTransactionUUID = CirmTransaction.getTopLevelTransactionUUID(); transactionBeginTime = CirmTransaction.get().getBeginTimeMs(); taskId = transactionBeginTime + "_" + url + minutesFromNow; NewTimeTaskOnTxSuccessListener s = new NewTimeTaskOnTxSuccessListener(cirmTransactionUUID, taskId, minutesFromNow, url, post); CirmTransaction.get().addTopLevelEventListener(s); return ok(); } else { ThreadLocalStopwatch.getWatch().time("Genutils timetask with url/minsFromNow called outside of a transaction. Using now, a new RandomUUID for task a NOTRANS marker in taskid."); cirmTransactionUUID = UUID.randomUUID(); transactionBeginTime = new Date().getTime(); taskId = transactionBeginTime + "_" + TIMETASK_NOTRANS_MARKER + "_" + url + minutesFromNow; return timeTaskDirect(cirmTransactionUUID, taskId, minutesFromNow, url, post); } } /** * Schedules a time machine callback task at a given time (calendar) by posting a new task to the time machine. * Repeated calls due to retries inside a transaction will only be inserted into the time machine once after * the transaction succeeds (due to NewTimeTaskOnTxSuccessListener). * Creation time is established by prefixing the task name with the transaction begin time. * (This establishes some order in the time machine) * Used by activity manager (on each transaction retry). * @param taskId a taskId that should be unique for the task. * @param cal * @param url * @param post * @return */ public static Json timeTask(String taskId, Calendar cal, String url, Json post) { String taskIdMod; UUID cirmTransactionUUID; long transactionBeginTime; if (CirmTransaction.isExecutingOnThisThread()) { cirmTransactionUUID = CirmTransaction.getTopLevelTransactionUUID(); transactionBeginTime = CirmTransaction.get().getBeginTimeMs(); taskIdMod = transactionBeginTime + "_" + taskId; //Register Tx listener to be executed once only on success. NewTimeTaskOnTxSuccessListener s = new NewTimeTaskOnTxSuccessListener(cirmTransactionUUID, taskIdMod, cal, url, post); CirmTransaction.get().addTopLevelEventListener(s); return ok(); } else { System.err.println("Genutils timetask with taskId called outside of a transaction. Using now, a new RandomUUID for task and a NOTRANS marker in taskid."); cirmTransactionUUID = UUID.randomUUID(); transactionBeginTime = new Date().getTime(); taskIdMod = transactionBeginTime + "_" + TIMETASK_NOTRANS_MARKER + "_" + taskId; return timeTaskCalDirect(cirmTransactionUUID, taskIdMod, cal, url, post); } } /** * <p> * Ask the TimeServer to call back the <code>url</code> in <code>minutesFromNow</code> minutes. * If the <code>url</code> parameter starts with 'http' it is used as is, otherwise, the * <code>OperationsRestService</code> from the ontology is used. The callback will be an HTTP POST * if the <code>post</code> parameter is not null. * </p> * Task name will be cirmTransactionUUID + TaskId, to ensure a proper key value for overwrites during in transaction retries. * * @param minutesFromNow * @param url * @param post * @param CirmTransactionUUID a unique transaction identifier that remains the same in retries. * @param taskId task identifier for the parameter combination - must be the same during all retries to avoid duplicates. * @return */ protected static Json timeTaskDirect(UUID cirmTransactionUUID, String taskId, int minutesFromNow, String url, Json post) { if (!url.startsWith("http")) { Json thisService = OWL.toJSON((OWLIndividual)Refs.configSet.resolve().get("OperationsRestService")); url = thisService.at("hasUrl").asString() + url; } //Group & name constitutes the key in the time machine //To ensure A) safe retries that overwrite equal tasks //and B) allow multiple new tasks per transaction //we use a combination of cirmTransactionUUID + taskId as name part of the key. // This ensures that a retry will overwrite an existing and not add a new task. Calendar cal = Calendar.getInstance(); cal.add(Calendar.MINUTE, minutesFromNow); return timeTaskCalDirect(cirmTransactionUUID, taskId, cal, url, post); } protected static Json timeTaskCalDirect(UUID cirmTransactionUUID, String taskId, Calendar cal, String url, Json post) { //DBG String taskName = taskId + "-" + cirmTransactionUUID.toString(); System.out.println("GENUTIL TIMETASK ID: " + taskName); final Json taskSpec = object(); final Json restCall = object("url", url, "method", post == null ? "GET" : "POST"); if (post != null) restCall.set("content", post); taskSpec.set("restCall", restCall) .set("group", "cirm_services") .set("name", taskName) .set("state", "NORMAL") .set("scheduleType", "SIMPLE") .set("startTime", object() .set("day_of_month", cal.get(Calendar.DATE)) .set("month", cal.get(Calendar.MONTH) + 1) .set("year", cal.get(Calendar.YEAR)) .set("hour", cal.get(Calendar.HOUR_OF_DAY)) .set("minute", cal.get(Calendar.MINUTE)) .set("second", cal.get(Calendar.SECOND))); final Json timeMachine = OWL.toJSON((OWLIndividual)Refs.configSet.resolve().get("TimeMachineConfig")); System.out.println("Time Machine url:" + timeMachine.at("hasUrl")); return GenUtils.httpPostJson(timeMachine.at("hasUrl").asString() + "/task", taskSpec); } /** * Gets the first email address found in the string or null. * @param anyString */ public static String findEmailIn(String anyString) { Matcher m = Pattern.compile("[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+").matcher(anyString); if (m.find()) { return m.group(); } //TODO maybe return all semicolon separated. return null; } /** * Thread.sleep, but with an unchecked exception (wraps and rethrows as RuntimeException); * @param millis */ public static void sleep(int millis) { try { Thread.sleep(millis); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Reads UTF 8 from a URL into String (usable to load form file also). * * @param url * @return * @throws RuntimeException on any exception. */ public static String readAsStringUTF8(URL url) { StringBuffer str = new StringBuffer(10000); String cur; BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8")); do { cur = br.readLine(); if (cur != null) { str.append(cur); } } while(cur != null); } catch (Exception e) { throw new RuntimeException(e); } finally { try { if (br != null) br.close(); } catch (IOException e) { e.printStackTrace(); } } return str.toString(); } /** * Prints maxElems of a stack trace using ThreadLocalStopWatch to see the thread. * * use Thread.currentThread().getStackTrace() to get one. * * Thread safe. * * @param trace null tolerated, will print an error trace * @param maxElems all values tolerated, if !>0 an error message will be logged. */ public static void logStackTrace(StackTraceElement[] trace, int maxLines) { if (trace != null && maxLines > 0) { int i = 0; while (i < trace.length && i < maxLines) { ThreadLocalStopwatch.getWatch().time("" + trace[i].toString()); i++; } } else { ThreadLocalStopwatch.getWatch().time("Error: irgnored: GenUtils.logStackTrace() trace was " + trace + " maxLines was " + maxLines); } } }