001/*
002 * Copyright 2006-2014 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.springframework.batch.core;
017
018import org.springframework.util.StringUtils;
019
020import java.io.PrintWriter;
021import java.io.Serializable;
022import java.io.StringWriter;
023
024/**
025 * Value object used to carry information about the status of a
026 * job or step execution.
027 *
028 * ExitStatus is immutable and therefore thread-safe.
029 *
030 * @author Dave Syer
031 *
032 */
033@SuppressWarnings("serial")
034public class ExitStatus implements Serializable, Comparable<ExitStatus> {
035
036        /**
037         * Convenient constant value representing unknown state - assumed not
038         * continuable.
039         */
040        public static final ExitStatus UNKNOWN = new ExitStatus("UNKNOWN");
041
042        /**
043         * Convenient constant value representing continuable state where processing
044         * is still taking place, so no further action is required. Used for
045         * asynchronous execution scenarios where the processing is happening in
046         * another thread or process and the caller is not required to wait for the
047         * result.
048         */
049        public static final ExitStatus EXECUTING = new ExitStatus("EXECUTING");
050
051        /**
052         * Convenient constant value representing finished processing.
053         */
054        public static final ExitStatus COMPLETED = new ExitStatus("COMPLETED");
055
056        /**
057         * Convenient constant value representing job that did no processing (e.g.
058         * because it was already complete).
059         */
060        public static final ExitStatus NOOP = new ExitStatus("NOOP");
061
062        /**
063         * Convenient constant value representing finished processing with an error.
064         */
065        public static final ExitStatus FAILED = new ExitStatus("FAILED");
066
067        /**
068         * Convenient constant value representing finished processing with
069         * interrupted status.
070         */
071        public static final ExitStatus STOPPED = new ExitStatus("STOPPED");
072
073        private final String exitCode;
074
075        private final String exitDescription;
076
077        public ExitStatus(String exitCode) {
078                this(exitCode, "");
079        }
080
081        public ExitStatus(String exitCode, String exitDescription) {
082                super();
083                this.exitCode = exitCode;
084                this.exitDescription = exitDescription == null ? "" : exitDescription;
085        }
086
087        /**
088         * Getter for the exit code (defaults to blank).
089         *
090         * @return the exit code.
091         */
092        public String getExitCode() {
093                return exitCode;
094        }
095
096        /**
097         * Getter for the exit description (defaults to blank)
098         *
099         * @return {@link String} containing the exit description.
100         */
101        public String getExitDescription() {
102                return exitDescription;
103        }
104
105        /**
106         * Create a new {@link ExitStatus} with a logical combination of the exit
107         * code, and a concatenation of the descriptions. If either value has a
108         * higher severity then its exit code will be used in the result. In the
109         * case of equal severity, the exit code is replaced if the new value is
110         * alphabetically greater.<br>
111         * <br>
112         *
113         * Severity is defined by the exit code:
114         * <ul>
115         * <li>Codes beginning with EXECUTING have severity 1</li>
116         * <li>Codes beginning with COMPLETED have severity 2</li>
117         * <li>Codes beginning with NOOP have severity 3</li>
118         * <li>Codes beginning with STOPPED have severity 4</li>
119         * <li>Codes beginning with FAILED have severity 5</li>
120         * <li>Codes beginning with UNKNOWN have severity 6</li>
121         * </ul>
122         * Others have severity 7, so custom exit codes always win.<br>
123         *
124         * If the input is null just return this.
125         *
126         * @param status an {@link ExitStatus} to combine with this one.
127         * @return a new {@link ExitStatus} combining the current value and the
128         * argument provided.
129         */
130        public ExitStatus and(ExitStatus status) {
131                if (status == null) {
132                        return this;
133                }
134                ExitStatus result = addExitDescription(status.exitDescription);
135                if (compareTo(status) < 0) {
136                        result = result.replaceExitCode(status.exitCode);
137                }
138                return result;
139        }
140
141        /**
142         * @param status an {@link ExitStatus} to compare
143         * @return greater than zero, 0, less than zero according to the severity and exit code
144         * @see java.lang.Comparable
145         */
146        @Override
147        public int compareTo(ExitStatus status) {
148                if (severity(status) > severity(this)) {
149                        return -1;
150                }
151                if (severity(status) < severity(this)) {
152                        return 1;
153                }
154                return this.getExitCode().compareTo(status.getExitCode());
155        }
156
157        /**
158         * @param status
159         * @return
160         */
161        private int severity(ExitStatus status) {
162                if (status.exitCode.startsWith(EXECUTING.exitCode)) {
163                        return 1;
164                }
165                if (status.exitCode.startsWith(COMPLETED.exitCode)) {
166                        return 2;
167                }
168                if (status.exitCode.startsWith(NOOP.exitCode)) {
169                        return 3;
170                }
171                if (status.exitCode.startsWith(STOPPED.exitCode)) {
172                        return 4;
173                }
174                if (status.exitCode.startsWith(FAILED.exitCode)) {
175                        return 5;
176                }
177                if (status.exitCode.startsWith(UNKNOWN.exitCode)) {
178                        return 6;
179                }
180                return 7;
181        }
182
183        /*
184         * (non-Javadoc)
185         *
186         * @see java.lang.Object#toString()
187         */
188        @Override
189        public String toString() {
190                return String.format("exitCode=%s;exitDescription=%s", exitCode, exitDescription);
191        }
192
193        /**
194         * Compare the fields one by one.
195         *
196         * @see java.lang.Object#equals(java.lang.Object)
197         */
198        @Override
199        public boolean equals(Object obj) {
200                if (obj == null) {
201                        return false;
202                }
203                return toString().equals(obj.toString());
204        }
205
206        /**
207         * Compatible with the equals implementation.
208         *
209         * @see java.lang.Object#hashCode()
210         */
211        @Override
212        public int hashCode() {
213                return toString().hashCode();
214        }
215
216        /**
217         * Add an exit code to an existing {@link ExitStatus}. If there is already a
218         * code present tit will be replaced.
219         *
220         * @param code the code to add
221         * @return a new {@link ExitStatus} with the same properties but a new exit
222         * code.
223         */
224        public ExitStatus replaceExitCode(String code) {
225                return new ExitStatus(code, exitDescription);
226        }
227
228        /**
229         * Check if this status represents a running process.
230         *
231         * @return true if the exit code is "EXECUTING" or "UNKNOWN"
232         */
233        public boolean isRunning() {
234                return "EXECUTING".equals(this.exitCode) || "UNKNOWN".equals(this.exitCode);
235        }
236
237        /**
238         * Add an exit description to an existing {@link ExitStatus}. If there is
239         * already a description present the two will be concatenated with a
240         * semicolon.
241         *
242         * @param description the description to add
243         * @return a new {@link ExitStatus} with the same properties but a new exit
244         * description
245         */
246        public ExitStatus addExitDescription(String description) {
247                StringBuilder buffer = new StringBuilder();
248                boolean changed = StringUtils.hasText(description) && !exitDescription.equals(description);
249                if (StringUtils.hasText(exitDescription)) {
250                        buffer.append(exitDescription);
251                        if (changed) {
252                                buffer.append("; ");
253                        }
254                }
255                if (changed) {
256                        buffer.append(description);
257                }
258                return new ExitStatus(exitCode, buffer.toString());
259        }
260
261        /**
262         * Extract the stack trace from the throwable provided and append it to
263         * the exist description.
264         *
265         * @param throwable {@link Throwable} instance containing the stack trace.
266         * @return a new ExitStatus with the stack trace appended
267         */
268        public ExitStatus addExitDescription(Throwable throwable) {
269                StringWriter writer = new StringWriter();
270                throwable.printStackTrace(new PrintWriter(writer));
271                String message = writer.toString();
272                return addExitDescription(message);
273        }
274
275        /**
276         * @param status the exit code to be evaluated
277         * @return true if the value matches a known exit code
278         */
279        public static boolean isNonDefaultExitStatus(ExitStatus status) {
280                return status == null || status.getExitCode() == null ||
281                                status.getExitCode().equals(ExitStatus.COMPLETED.getExitCode()) ||
282                                status.getExitCode().equals(ExitStatus.EXECUTING.getExitCode()) ||
283                                status.getExitCode().equals(ExitStatus.FAILED.getExitCode()) ||
284                                status.getExitCode().equals(ExitStatus.NOOP.getExitCode()) ||
285                                status.getExitCode().equals(ExitStatus.STOPPED.getExitCode()) ||
286                                status.getExitCode().equals(ExitStatus.UNKNOWN.getExitCode());
287        }
288}