You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProgramError.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /*
  2. * Copyright (c) 2006-2014 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.logger;
  23. import java.io.File;
  24. import java.io.FileOutputStream;
  25. import java.io.IOException;
  26. import java.io.OutputStream;
  27. import java.io.PrintWriter;
  28. import java.io.Serializable;
  29. import java.text.DateFormat;
  30. import java.text.SimpleDateFormat;
  31. import java.util.Date;
  32. import java.util.Objects;
  33. import java.util.concurrent.Semaphore;
  34. import java.util.concurrent.atomic.AtomicInteger;
  35. import javax.annotation.Nullable;
  36. /**
  37. * Stores a program error.
  38. */
  39. public final class ProgramError implements Serializable {
  40. /** A version number for this class. */
  41. private static final long serialVersionUID = 3;
  42. /** Directory used to store errors. */
  43. private static File errorDir;
  44. /** Semaphore used to serialise write access. */
  45. private static final Semaphore WRITING_SEM = new Semaphore(1);
  46. /** The reporter to use to send this error. */
  47. private final ErrorReporter reporter;
  48. /** Error ID. */
  49. private final long id;
  50. /** Error icon. */
  51. private final ErrorLevel level;
  52. /** Error message. */
  53. private final String message;
  54. /** Underlying exception. */
  55. private final Throwable exception;
  56. /** Underlying details message. */
  57. private final String details;
  58. /** Date/time error first occurred. */
  59. private final Date firstDate;
  60. /** Date/time error last occurred. */
  61. private Date lastDate;
  62. /** Number of occurrences. */
  63. private final AtomicInteger count;
  64. /** Error report Status. */
  65. private ErrorReportStatus reportStatus;
  66. /**
  67. * Creates a new instance of ProgramError.
  68. *
  69. * @param id error id
  70. * @param level Error level
  71. * @param message Error message
  72. * @param exception The exception that caused the error, if any.
  73. * @param details The detailed cause of the error, if any.
  74. * @param date Error time and date
  75. */
  76. public ProgramError(final long id, final ErrorLevel level, final String message,
  77. @Nullable final Throwable exception,
  78. @Nullable final String details,
  79. final Date date) {
  80. if (id < 0) {
  81. throw new IllegalArgumentException("ID must be a positive integer: " + id);
  82. }
  83. if (level == null) {
  84. throw new IllegalArgumentException("Level cannot be null");
  85. }
  86. if (message == null || message.isEmpty()) {
  87. throw new IllegalArgumentException("Message cannot be null or an empty string");
  88. }
  89. if (date == null) {
  90. throw new IllegalArgumentException("date cannot be null");
  91. }
  92. this.id = id;
  93. this.level = level;
  94. this.message = message;
  95. this.exception = exception;
  96. this.details = details;
  97. this.firstDate = (Date) date.clone();
  98. this.lastDate = (Date) date.clone();
  99. this.count = new AtomicInteger(1);
  100. this.reportStatus = ErrorReportStatus.WAITING;
  101. this.reporter = new ErrorReporter();
  102. }
  103. /**
  104. * Returns this errors level.
  105. *
  106. * @return Error level
  107. */
  108. public ErrorLevel getLevel() {
  109. return level;
  110. }
  111. /**
  112. * Returns this errors message.
  113. *
  114. * @return Error message
  115. */
  116. public String getMessage() {
  117. return message;
  118. }
  119. /**
  120. * Returns this errors trace.
  121. *
  122. * @return Error trace
  123. */
  124. public String[] getTrace() {
  125. return exception == null ? (message == null ? new String[0] : new String[]{message})
  126. : getTrace(exception);
  127. }
  128. /**
  129. * Returns this errors time.
  130. *
  131. * @return Error time
  132. */
  133. public Date getDate() {
  134. return (Date) firstDate.clone();
  135. }
  136. /**
  137. * Returns the number of times this error has occurred.
  138. *
  139. * @return Error count
  140. */
  141. public int getCount() {
  142. return count.get();
  143. }
  144. /**
  145. * Returns the last time this error occurred.
  146. *
  147. * @return Last occurrence
  148. */
  149. public Date getLastDate() {
  150. return (Date) lastDate.clone();
  151. }
  152. /**
  153. * Returns the reportStatus of this error.
  154. *
  155. * @return Error reportStatus
  156. */
  157. public ErrorReportStatus getReportStatus() {
  158. return reportStatus;
  159. }
  160. /**
  161. * Sets the report Status of this error.
  162. *
  163. * @param newStatus new ErrorReportStatus for the error
  164. */
  165. public void setReportStatus(final ErrorReportStatus newStatus) {
  166. if (newStatus != null && !reportStatus.equals(newStatus)) {
  167. reportStatus = newStatus;
  168. ErrorManager.getErrorManager().fireErrorStatusChanged(this);
  169. synchronized (this) {
  170. notifyAll();
  171. }
  172. }
  173. }
  174. /**
  175. * Returns the ID of this error.
  176. *
  177. * @return Error ID
  178. */
  179. public long getID() {
  180. return id;
  181. }
  182. /**
  183. * Saves this error to disk.
  184. *
  185. * @param directory The directory to save the error in.
  186. */
  187. public void save(final String directory) {
  188. try (PrintWriter out = new PrintWriter(getErrorFile(directory), true)) {
  189. out.println("Date:" + getDate());
  190. out.println("Level: " + getLevel());
  191. out.println("Description: " + getMessage());
  192. out.println("Details:");
  193. for (String traceLine : getTrace()) {
  194. out.println('\t' + traceLine);
  195. }
  196. }
  197. }
  198. /**
  199. * Creates a new file for an error and returns the output stream.
  200. *
  201. * @param directory The directory to save the error in.
  202. *
  203. * @return BufferedOutputStream to write to the error file
  204. */
  205. @SuppressWarnings("PMD.SystemPrintln")
  206. private OutputStream getErrorFile(final String directory) {
  207. WRITING_SEM.acquireUninterruptibly();
  208. if (errorDir == null || !errorDir.exists()) {
  209. errorDir = new File(directory);
  210. if (!errorDir.exists()) {
  211. errorDir.mkdirs();
  212. }
  213. }
  214. final String logName = getDate().getTime() + "-" + getLevel();
  215. final File errorFile = new File(errorDir, logName + ".log");
  216. if (errorFile.exists()) {
  217. boolean rename = false;
  218. for (int i = 0; !rename; i++) {
  219. rename = errorFile.renameTo(new File(errorDir, logName + "-" + i + ".log"));
  220. }
  221. }
  222. try {
  223. errorFile.createNewFile();
  224. return new FileOutputStream(errorFile);
  225. } catch (IOException ex) {
  226. System.err.println("Error creating new file: ");
  227. ex.printStackTrace();
  228. return new NullOutputStream();
  229. } finally {
  230. WRITING_SEM.release();
  231. }
  232. }
  233. /**
  234. * Sends this error report to the DMDirc developers.
  235. */
  236. public void send() {
  237. setReportStatus(ErrorReportStatus.SENDING);
  238. reporter.sendException(message, level, firstDate, exception, details);
  239. setReportStatus(ErrorReportStatus.FINISHED);
  240. }
  241. /**
  242. * Determines whether or not the stack trace associated with this error is from a valid source.
  243. * A valid source is one that is within a DMDirc package (com.dmdirc), and is not the DMDirc
  244. * event queue.
  245. *
  246. * @return True if the source is valid, false otherwise
  247. */
  248. public boolean isValidSource() {
  249. final String line = getSourceLine();
  250. return line.startsWith("com.dmdirc")
  251. && !line.startsWith("com.dmdirc.addons.ui_swing.DMDircEventQueue");
  252. }
  253. /**
  254. * Returns the "source line" of this error, which is defined as the first line starting with a
  255. * DMDirc package name (com.dmdirc). If no such line is found, returns the first line of the
  256. * message.
  257. *
  258. * @return This error's source line
  259. */
  260. public String getSourceLine() {
  261. final String[] trace = getTrace();
  262. for (String line : trace) {
  263. if (line.startsWith("com.dmdirc")) {
  264. return line;
  265. }
  266. }
  267. return trace[0];
  268. }
  269. /**
  270. * Updates the last date this error occurred.
  271. */
  272. public void updateLastDate() {
  273. updateLastDate(new Date());
  274. }
  275. /**
  276. * Updates the last date this error occurred.
  277. *
  278. * @param date Date error occurred
  279. */
  280. public void updateLastDate(final Date date) {
  281. lastDate = date;
  282. count.getAndIncrement();
  283. ErrorManager.getErrorManager().fireErrorStatusChanged(this);
  284. synchronized (this) {
  285. notifyAll();
  286. }
  287. }
  288. /**
  289. * Retruns a human readable string describing the number of times this error occurred and when
  290. * these occurrences were.
  291. *
  292. * @return Occurrences description
  293. */
  294. public String occurrencesString() {
  295. final DateFormat format = new SimpleDateFormat("MMM dd hh:mm aa");
  296. if (count.get() == 1) {
  297. return "1 occurrence on " + format.format(getDate());
  298. } else {
  299. return count.get() + " occurrences between " + format.format(
  300. getDate()) + " and " + format.format(getLastDate()) + ".";
  301. }
  302. }
  303. @Override
  304. public String toString() {
  305. return "ID" + id + " Level: " + getLevel() + " Status: " + getReportStatus()
  306. + " Message: '" + getMessage() + "'";
  307. }
  308. @Override
  309. public boolean equals(final Object obj) {
  310. if (obj == null) {
  311. return false;
  312. }
  313. if (getClass() != obj.getClass()) {
  314. return false;
  315. }
  316. final ProgramError other = (ProgramError) obj;
  317. if (this.level != other.level) {
  318. return false;
  319. }
  320. if (!this.message.equals(other.message)) {
  321. return false;
  322. }
  323. if (!Objects.equals(this.exception, other.exception)) {
  324. return false;
  325. }
  326. if (!Objects.equals(this.details, other.details)) {
  327. return false;
  328. }
  329. return true;
  330. }
  331. @Override
  332. public int hashCode() {
  333. int hash = 7;
  334. hash = 67 * hash + this.level.hashCode();
  335. hash = 67 * hash + this.message.hashCode();
  336. hash = 67 * hash + (this.exception == null ? 1 : this.exception.hashCode());
  337. hash = 67 * hash + (this.details == null ? 1 : this.details.hashCode());
  338. return hash;
  339. }
  340. /**
  341. * Converts an exception into a string array.
  342. *
  343. * @param throwable Exception to convert
  344. *
  345. * @since 0.6.3m1
  346. * @return Exception string array
  347. */
  348. private static String[] getTrace(final Throwable throwable) {
  349. String[] trace;
  350. if (throwable == null) {
  351. trace = new String[0];
  352. } else {
  353. final StackTraceElement[] traceElements = throwable.getStackTrace();
  354. trace = new String[traceElements.length + 1];
  355. trace[0] = throwable.toString();
  356. for (int i = 0; i < traceElements.length; i++) {
  357. trace[i + 1] = traceElements[i].toString();
  358. }
  359. if (throwable.getCause() != null) {
  360. final String[] causeTrace = getTrace(throwable.getCause());
  361. final String[] newTrace = new String[trace.length + causeTrace.length];
  362. trace[0] = "\nWhich caused: " + trace[0];
  363. System.arraycopy(causeTrace, 0, newTrace, 0, causeTrace.length);
  364. System.arraycopy(trace, 0, newTrace, causeTrace.length, trace.length);
  365. trace = newTrace;
  366. }
  367. }
  368. return trace;
  369. }
  370. }