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 }