001/*
002 * Copyright 2006-2018 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.scope.context;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Properties;
026import java.util.Set;
027
028import org.springframework.batch.core.JobExecution;
029import org.springframework.batch.core.JobInstance;
030import org.springframework.batch.core.JobParameter;
031import org.springframework.batch.core.JobParameters;
032import org.springframework.batch.core.UnexpectedJobExecutionException;
033import org.springframework.batch.core.scope.StepScope;
034import org.springframework.batch.item.ExecutionContext;
035import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor;
036import org.springframework.lang.Nullable;
037import org.springframework.util.Assert;
038
039/**
040 * A context object that can be used to interrogate the current {@link JobExecution} and some of its associated
041 * properties using expressions
042 * based on bean paths. Has public getters for the job execution and
043 * convenience methods for accessing commonly used properties like the {@link ExecutionContext} associated with the job
044 * execution.
045 *
046 * @author Dave Syer
047 * @author Jimmy Praet (create JobContext based on {@link StepContext})
048 * @author Mahmoud Ben Hassine
049 * @since 3.0
050 */
051public class JobContext extends SynchronizedAttributeAccessor {
052
053        private JobExecution jobExecution;
054
055        private Map<String, Set<Runnable>> callbacks = new HashMap<String, Set<Runnable>>();
056
057        public JobContext(JobExecution jobExecution) {
058                super();
059                Assert.notNull(jobExecution, "A JobContext must have a non-null JobExecution");
060                this.jobExecution = jobExecution;
061        }
062
063        /**
064         * Convenient accessor for current job name identifier.
065         *
066         * @return the job name identifier of the enclosing {@link JobInstance} associated with the current
067         *         {@link JobExecution}
068         */
069        public String getJobName() {
070                Assert.state(jobExecution.getJobInstance() != null, "JobExecution does not have a JobInstance");
071                return jobExecution.getJobInstance().getJobName();
072        }
073
074        /**
075         * Convenient accessor for System properties to make it easy to access them
076         * from placeholder expressions.
077         *
078         * @return the current System properties
079         */
080        public Properties getSystemProperties() {
081                return System.getProperties();
082        }
083
084        /**
085         * @return a map containing the items from the job {@link ExecutionContext}
086         */
087        public Map<String, Object> getJobExecutionContext() {
088                Map<String, Object> result = new HashMap<String, Object>();
089                for (Entry<String, Object> entry : jobExecution.getExecutionContext().entrySet()) {
090                        result.put(entry.getKey(), entry.getValue());
091                }
092                return Collections.unmodifiableMap(result);
093        }
094
095        /**
096         * @return a map containing the items from the {@link JobParameters}
097         */
098        public Map<String, Object> getJobParameters() {
099                Map<String, Object> result = new HashMap<String, Object>();
100                for (Entry<String, JobParameter> entry : jobExecution.getJobParameters().getParameters()
101                                .entrySet()) {
102                        result.put(entry.getKey(), entry.getValue().getValue());
103                }
104                return Collections.unmodifiableMap(result);
105        }
106
107        /**
108         * Allow clients to register callbacks for clean up on close.
109         *
110         * @param name
111         *        the callback id (unique attribute key in this context)
112         * @param callback
113         *        a callback to execute on close
114         */
115        public void registerDestructionCallback(String name, Runnable callback) {
116                synchronized (callbacks) {
117                        Set<Runnable> set = callbacks.get(name);
118                        if (set == null) {
119                                set = new HashSet<Runnable>();
120                                callbacks.put(name, set);
121                        }
122                        set.add(callback);
123                }
124        }
125
126        private void unregisterDestructionCallbacks(String name) {
127                synchronized (callbacks) {
128                        callbacks.remove(name);
129                }
130        }
131
132        /**
133         * Override base class behaviour to ensure destruction callbacks are
134         * unregistered as well as the default behaviour.
135         *
136         * @see SynchronizedAttributeAccessor#removeAttribute(String)
137         */
138        @Override
139        @Nullable
140        public Object removeAttribute(String name) {
141                unregisterDestructionCallbacks(name);
142                return super.removeAttribute(name);
143        }
144
145        /**
146         * Clean up the context at the end of a step execution. Must be called once
147         * at the end of a step execution to honour the destruction callback
148         * contract from the {@link StepScope}.
149         */
150        public void close() {
151
152                List<Exception> errors = new ArrayList<Exception>();
153
154                Map<String, Set<Runnable>> copy = Collections.unmodifiableMap(callbacks);
155
156                for (Entry<String, Set<Runnable>> entry : copy.entrySet()) {
157                        Set<Runnable> set = entry.getValue();
158                        for (Runnable callback : set) {
159                                if (callback != null) {
160                                        /*
161                                         * The documentation of the interface says that these
162                                         * callbacks must not throw exceptions, but we don't trust
163                                         * them necessarily...
164                                         */
165                                        try {
166                                                callback.run();
167                                        } catch (RuntimeException t) {
168                                                errors.add(t);
169                                        }
170                                }
171                        }
172                }
173
174                if (errors.isEmpty()) {
175                        return;
176                }
177
178                Exception error = errors.get(0);
179                if (error instanceof RuntimeException) {
180                        throw (RuntimeException) error;
181                } else {
182                        throw new UnexpectedJobExecutionException("Could not close step context, rethrowing first of "
183                                        + errors.size() + " exceptions.", error);
184                }
185        }
186
187        /**
188         * The current {@link JobExecution} that is active in this context.
189         *
190         * @return the current {@link JobExecution}
191         */
192        public JobExecution getJobExecution() {
193                return jobExecution;
194        }
195
196        /**
197         * @return unique identifier for this context based on the step execution
198         */
199        public String getId() {
200                Assert.state(jobExecution.getId() != null, "JobExecution has no id.  "
201                                + "It must be saved before it can be used in job scope.");
202                return "jobExecution#" + jobExecution.getId();
203        }
204
205        /**
206         * Extend the base class method to include the job execution itself as a key
207         * (i.e. two contexts are only equal if their job executions are the same).
208         */
209        @Override
210        public boolean equals(Object other) {
211                if (!(other instanceof JobContext)) {
212                        return false;
213                }
214                if (other == this) {
215                        return true;
216                }
217                JobContext context = (JobContext) other;
218                if (context.jobExecution == jobExecution) {
219                        return true;
220                }
221                return jobExecution.equals(context.jobExecution);
222        }
223
224        /**
225         * Overrides the default behaviour to provide a hash code based only on the
226         * job execution.
227         */
228        @Override
229        public int hashCode() {
230                return jobExecution.hashCode();
231        }
232
233        @Override
234        public String toString() {
235                return super.toString() + ", jobExecutionContext=" + getJobExecutionContext() + ", jobParameters="
236                                + getJobParameters();
237        }
238
239}