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