View Javadoc

1   package com.atlassian.scheduler;
2   
3   import com.atlassian.annotations.PublicApi;
4   import com.atlassian.scheduler.status.RunOutcome;
5   
6   import javax.annotation.Nonnull;
7   import javax.annotation.Nullable;
8   import javax.annotation.concurrent.Immutable;
9   
10  import java.util.Objects;
11  
12  import static com.atlassian.scheduler.status.RunDetails.MAXIMUM_MESSAGE_LENGTH;
13  import static com.atlassian.scheduler.status.RunOutcome.ABORTED;
14  import static com.atlassian.scheduler.status.RunOutcome.FAILED;
15  import static com.atlassian.scheduler.status.RunOutcome.SUCCESS;
16  import static com.atlassian.util.concurrent.Assertions.isTrue;
17  import static com.atlassian.util.concurrent.Assertions.notNull;
18  
19  /**
20   * An object that represents the result of a call to {@link JobRunner#runJob(JobRunnerRequest)}.
21   * The job runner can use this to customize the reporting of its status; otherwise it can simply
22   * return {@code null} on success and throw an exception on failure.
23   *
24   * @since v1.0
25   */
26  @PublicApi
27  @Immutable
28  public final class JobRunnerResponse {
29      /**
30       * Creates a successful response with no additional message.
31       *
32       * @return the response
33       */
34      public static JobRunnerResponse success() {
35          return success(null);
36      }
37  
38      /**
39       * Creates a successful response with the specified message.
40       *
41       * @param message the message to return, which is optional and will be truncated to
42       *                {@link com.atlassian.scheduler.status.RunDetails#MAXIMUM_MESSAGE_LENGTH} if necessary
43       * @return the response
44       */
45      public static JobRunnerResponse success(@Nullable final String message) {
46          return new JobRunnerResponse(SUCCESS, message);
47      }
48  
49      /**
50       * Creates a response that indicates the request was aborted.  In most cases, it will make
51       * more sense to report the job as either having {@link #success() succeeded} with nothing to
52       * do or {@link #failed(String) failed}, instead.
53       *
54       * @param message the message to return, which will be truncated to {@link com.atlassian.scheduler.status.RunDetails#MAXIMUM_MESSAGE_LENGTH} if
55       *                necessary.  The message is <strong>required</strong> when reporting that the job was aborted.
56       * @return the response
57       */
58      public static JobRunnerResponse aborted(final String message) {
59          isTrue("The message must be specified when reporting a job as aborted!", isNotBlank(message));
60          return new JobRunnerResponse(ABORTED, message);
61      }
62  
63      /**
64       * Creates a response that indicates the request has failed.
65       *
66       * @param message the message to return, which will be truncated to {@link com.atlassian.scheduler.status.RunDetails#MAXIMUM_MESSAGE_LENGTH} if
67       *                necessary.  The message is <strong>required</strong> when reporting that the job has failed.
68       * @return the response
69       */
70      public static JobRunnerResponse failed(final String message) {
71          isTrue("The message must be specified when reporting a job as failed!", isNotBlank(message));
72          return new JobRunnerResponse(FAILED, message);
73      }
74  
75      /**
76       * Creates a response that indicates the request has failed.  The {@link #getMessage() message} is set to
77       * to an abbreviated representation of the exception and its causes, but the
78       * {@link com.atlassian.scheduler.status.RunDetails#MAXIMUM_MESSAGE_LENGTH} still applies, so this information may be incomplete.
79       * When possible, the {@link JobRunner} is encouraged to trap its exceptions and report more specific
80       * diagnostic messages with {@link #failed(String)}, instead.
81       *
82       * @param cause the exception that caused this failure
83       * @return the response
84       */
85      public static JobRunnerResponse failed(final Throwable cause) {
86          return new JobRunnerResponse(FAILED, toMessage(notNull("cause", cause)));
87      }
88  
89  
90      private final RunOutcome runOutcome;
91      private final String message;
92  
93      private JobRunnerResponse(final RunOutcome runOutcome, @Nullable final String message) {
94          this.runOutcome = runOutcome;
95          this.message = message;
96      }
97  
98      // Implementation note: This class is intended to follow the same immutable object builder pattern that
99      // JobConfig does; there just isn't anything else to set on it at this time.
100 
101 
102     @Nonnull
103     public RunOutcome getRunOutcome() {
104         return runOutcome;
105     }
106 
107     @Nullable
108     public String getMessage() {
109         return message;
110     }
111 
112     @Override
113     public boolean equals(@Nullable final Object o) {
114         if (this == o) {
115             return true;
116         }
117         if (o == null || getClass() != o.getClass()) {
118             return false;
119         }
120         final JobRunnerResponse other = (JobRunnerResponse) o;
121         return runOutcome == other.runOutcome && Objects.equals(message, other.message);
122     }
123 
124     @Override
125     public int hashCode() {
126         return Objects.hash(runOutcome, message);
127     }
128 
129     @Override
130     public String toString() {
131         return "JobRunnerResponse[runOutcome=" + runOutcome + ",message='" + message + "']";
132     }
133 
134 
135     private static boolean isNotBlank(@Nullable final String message) {
136         return message != null && !message.trim().isEmpty();
137     }
138 
139     /**
140      * Creates an abbreviated representation of the specified exception.
141      * <p>
142      * The current implementation of this is to return the {@link Class#getSimpleName() simple class name}
143      * of {@code e}.  If {@code e} has a {@link Throwable#getMessage() message}, then {@code ": "} is
144      * added, followed by that message.  This is repeated for each {@link Throwable#getCause() cause} in
145      * {@code e}'s exception chain, with newlines in between, until they are all exhausted or the
146      * {@link com.atlassian.scheduler.status.RunDetails#MAXIMUM_MESSAGE_LENGTH} has been reached.  This is just a rough guess at what
147      * is moderately likely to be useful information.
148      * </p>
149      *
150      * @param e the exception to convert into an abbreviated troubleshooting message
151      * @return the message
152      */
153     private static String toMessage(final Throwable e) {
154         final StringBuilder message = new StringBuilder(MAXIMUM_MESSAGE_LENGTH);
155         appendShortForm(message, e);
156         Throwable cause = e.getCause();
157         while (message.length() < MAXIMUM_MESSAGE_LENGTH && cause != null) {
158             message.append('\n');
159             appendShortForm(message, cause);
160             cause = cause.getCause();
161         }
162         if (message.length() > MAXIMUM_MESSAGE_LENGTH) {
163             message.setLength(MAXIMUM_MESSAGE_LENGTH);
164         }
165         return message.toString();
166     }
167 
168     private static void appendShortForm(final StringBuilder sb, final Throwable e) {
169         sb.append(e.getClass().getSimpleName());
170 
171         final String msg = e.getMessage();
172         if (msg != null) {
173             sb.append(": ").append(msg);
174         }
175     }
176 }