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}