View Javadoc

1   /*
2    * Copyright 2006-2009 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.springframework.batch.core.configuration.xml;
17  
18  import org.springframework.batch.core.listener.StepListenerMetaData;
19  import org.springframework.batch.core.step.item.ForceRollbackForWriteSkipException;
20  import org.springframework.batch.repeat.policy.SimpleCompletionPolicy;
21  import org.springframework.beans.MutablePropertyValues;
22  import org.springframework.beans.factory.config.BeanDefinition;
23  import org.springframework.beans.factory.config.BeanDefinitionHolder;
24  import org.springframework.beans.factory.config.RuntimeBeanReference;
25  import org.springframework.beans.factory.config.TypedStringValue;
26  import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
27  import org.springframework.beans.factory.support.AbstractBeanDefinition;
28  import org.springframework.beans.factory.support.BeanDefinitionBuilder;
29  import org.springframework.beans.factory.support.GenericBeanDefinition;
30  import org.springframework.beans.factory.support.ManagedList;
31  import org.springframework.beans.factory.support.ManagedMap;
32  import org.springframework.beans.factory.xml.ParserContext;
33  import org.springframework.util.StringUtils;
34  import org.springframework.util.xml.DomUtils;
35  import org.w3c.dom.Element;
36  
37  import java.util.List;
38  
39  /**
40   * Internal parser for the <chunk/> element inside a step.
41   * 
42   * @author Thomas Risberg
43   * @since 2.0
44   */
45  public class ChunkElementParser {
46  
47  	private static final String REF_ATTR = "ref";
48  
49  	private static final String MERGE_ATTR = "merge";
50  
51  	private static final String COMMIT_INTERVAL_ATTR = "commit-interval";
52  
53  	private static final String CHUNK_COMPLETION_POLICY_ATTR = "chunk-completion-policy";
54  
55  	private static final String BEAN_ELE = "bean";
56  
57  	private static final String REF_ELE = "ref";
58  
59  	private static final String ITEM_READER_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemReaderAdapter";
60  
61  	private static final String ITEM_PROCESSOR_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemProcessorAdapter";
62  
63  	private static final String ITEM_WRITER_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemWriterAdapter";
64  
65  	private static final StepListenerParser stepListenerParser = new StepListenerParser(
66  			StepListenerMetaData.itemListenerMetaData());
67  
68  	/**
69  	 * @param element
70  	 * @param parserContext
71  	 */
72  	protected void parse(Element element, AbstractBeanDefinition bd, ParserContext parserContext, boolean underspecified) {
73  
74  		MutablePropertyValues propertyValues = bd.getPropertyValues();
75  
76  		propertyValues.addPropertyValue("hasChunkElement", Boolean.TRUE);
77  
78  		handleItemHandler(bd, "reader", "itemReader", ITEM_READER_ADAPTER_CLASS, true, element, parserContext,
79  				propertyValues, underspecified);
80  		handleItemHandler(bd, "processor", "itemProcessor", ITEM_PROCESSOR_ADAPTER_CLASS, false, element, parserContext,
81  				propertyValues, underspecified);
82  		handleItemHandler(bd, "writer", "itemWriter", ITEM_WRITER_ADAPTER_CLASS, true, element, parserContext,
83  				propertyValues, underspecified);
84  
85  		String commitInterval = element.getAttribute(COMMIT_INTERVAL_ATTR);
86  		if (StringUtils.hasText(commitInterval)) {
87  			if (commitInterval.startsWith("#")) {
88  				// It's a late binding expression, so we need step scope...
89  				BeanDefinitionBuilder completionPolicy = BeanDefinitionBuilder
90  						.genericBeanDefinition(SimpleCompletionPolicy.class);
91  				completionPolicy.addConstructorArgValue(commitInterval);
92  				completionPolicy.setScope("step");
93  				propertyValues.addPropertyValue("chunkCompletionPolicy", completionPolicy.getBeanDefinition());
94  			}
95  			else {
96  				propertyValues.addPropertyValue("commitInterval", commitInterval);
97  			}
98  		}
99  
100 		String completionPolicyRef = element.getAttribute(CHUNK_COMPLETION_POLICY_ATTR);
101 		if (StringUtils.hasText(completionPolicyRef)) {
102 			RuntimeBeanReference completionPolicy = new RuntimeBeanReference(completionPolicyRef);
103 			propertyValues.addPropertyValue("chunkCompletionPolicy", completionPolicy);
104 		}
105 
106 		if (!underspecified
107 				&& propertyValues.contains("commitInterval") == propertyValues.contains("chunkCompletionPolicy")) {
108 			if (propertyValues.contains("commitInterval")) {
109 				parserContext.getReaderContext().error(
110 						"The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR
111 								+ "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "', but not both.", element);
112 			}
113 			else {
114 				parserContext.getReaderContext().error(
115 						"The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR
116 								+ "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "'.", element);
117 
118 			}
119 		}
120 
121 		String skipLimit = element.getAttribute("skip-limit");
122 		ManagedMap skippableExceptions = handleExceptionElement(element, parserContext, "skippable-exception-classes");
123 		if (StringUtils.hasText(skipLimit)) {
124 			if (skippableExceptions == null) {
125 				skippableExceptions = new ManagedMap();
126 				skippableExceptions.setMergeEnabled(true);
127 			}
128 			propertyValues.addPropertyValue("skipLimit", skipLimit);
129 		}
130 		if (skippableExceptions != null) {
131 			// Even if there is no retryLimit, we can still accept exception
132 			// classes for an abstract parent bean definition
133 			propertyValues.addPropertyValue("skippableExceptionClasses", skippableExceptions);
134 		}
135 
136 		handleItemHandler(bd, "skip-policy", "skipPolicy", null, false, element, parserContext, propertyValues,
137 				underspecified);
138 
139 		String retryLimit = element.getAttribute("retry-limit");
140 		ManagedMap retryableExceptions = handleExceptionElement(element, parserContext, "retryable-exception-classes");
141 		if (StringUtils.hasText(retryLimit)) {
142 			if (retryableExceptions == null) {
143 				retryableExceptions = new ManagedMap();
144 				retryableExceptions.setMergeEnabled(true);
145 			}
146 			propertyValues.addPropertyValue("retryLimit", retryLimit);
147 		}
148 		if (retryableExceptions != null) {
149 			// Even if there is no retryLimit, we can still accept exception
150 			// classes for an abstract parent bean definition
151 			propertyValues.addPropertyValue("retryableExceptionClasses", retryableExceptions);
152 		}
153 
154 		handleItemHandler(bd, "retry-policy", "retryPolicy", null, false, element, parserContext, propertyValues,
155 				underspecified);
156 
157 		String cacheCapacity = element.getAttribute("cache-capacity");
158 		if (StringUtils.hasText(cacheCapacity)) {
159 			propertyValues.addPropertyValue("cacheCapacity", cacheCapacity);
160 		}
161 
162 		String isReaderTransactionalQueue = element.getAttribute("reader-transactional-queue");
163 		if (StringUtils.hasText(isReaderTransactionalQueue)) {
164 			propertyValues.addPropertyValue("isReaderTransactionalQueue", isReaderTransactionalQueue);
165 		}
166 
167 		String isProcessorTransactional = element.getAttribute("processor-transactional");
168 		if (StringUtils.hasText(isProcessorTransactional)) {
169 			propertyValues.addPropertyValue("processorTransactional", isProcessorTransactional);
170 		}
171 
172 		handleRetryListenersElement(element, propertyValues, parserContext, bd);
173 
174 		handleStreamsElement(element, propertyValues, parserContext);
175 
176 		stepListenerParser.handleListenersElement(element, bd, parserContext);
177 
178 	}
179 
180 	/**
181 	 * Handle the ItemReader, ItemProcessor, and ItemWriter attributes/elements.
182 	 */
183 	private void handleItemHandler(AbstractBeanDefinition enclosing, String handlerName, String propertyName, String adapterClassName, boolean required,
184 			Element element, ParserContext parserContext, MutablePropertyValues propertyValues, boolean underspecified) {
185 		String refName = element.getAttribute(handlerName);
186 		@SuppressWarnings("unchecked")
187 		List<Element> children = DomUtils.getChildElementsByTagName(element, handlerName);
188 		if (children.size() == 1) {
189 			if (StringUtils.hasText(refName)) {
190 				parserContext.getReaderContext().error(
191 						"The <" + element.getNodeName() + "/> element may not have both a '" + handlerName
192 								+ "' attribute and a <" + handlerName + "/> element.", element);
193 			}
194 			handleItemHandlerElement(enclosing, propertyName, adapterClassName, propertyValues, children.get(0), parserContext);
195 		}
196 		else if (children.size() > 1) {
197 			parserContext.getReaderContext().error(
198 					"The <" + handlerName + "/> element may not appear more than once in a single <"
199 							+ element.getNodeName() + "/>.", element);
200 		}
201 		else if (StringUtils.hasText(refName)) {
202 			propertyValues.addPropertyValue(propertyName, new RuntimeBeanReference(refName));
203 		}
204 		else if (required && !underspecified) {
205 			parserContext.getReaderContext().error(
206 					"The <" + element.getNodeName() + "/> element has neither a '" + handlerName
207 							+ "' attribute nor a <" + handlerName + "/> element.", element);
208 		}
209 	}
210 
211 	/**
212 	 * Handle the &lt;reader/&gt;, &lt;processor/&gt;, or &lt;writer/&gt; that
213 	 * is defined within the item handler.
214 	 */
215 	@SuppressWarnings("unchecked")
216 	private void handleItemHandlerElement(AbstractBeanDefinition enclosing, String propertyName, String adapterClassName,
217 			MutablePropertyValues propertyValues, Element element, ParserContext parserContext) {
218 		List<Element> beanElements = DomUtils.getChildElementsByTagName(element, BEAN_ELE);
219 		List<Element> refElements = DomUtils.getChildElementsByTagName(element, REF_ELE);
220 		if (beanElements.size() + refElements.size() != 1) {
221 			parserContext.getReaderContext().error(
222 					"The <" + element.getNodeName() + "/> must have exactly one of either a <" + BEAN_ELE
223 							+ "/> element or a <" + REF_ELE + "/> element.", element);
224 		}
225 		else if (beanElements.size() == 1) {
226 			Element beanElement = beanElements.get(0);
227 			BeanDefinitionHolder beanDefinitionHolder = parserContext.getDelegate().parseBeanDefinitionElement(
228 					beanElement, enclosing);
229 			parserContext.getDelegate().decorateBeanDefinitionIfRequired(beanElement, beanDefinitionHolder);
230 
231 			propertyValues.addPropertyValue(propertyName, beanDefinitionHolder);
232 		}
233 		else if (refElements.size() == 1) {
234 			propertyValues.addPropertyValue(propertyName,
235 					parserContext.getDelegate().parsePropertySubElement(refElements.get(0), null));
236 		}
237 
238 		handleAdapterMethodAttribute(propertyName, adapterClassName, propertyValues, element);
239 	}
240 
241 	/**
242 	 * Handle the adapter-method attribute by using an
243 	 * AbstractMethodInvokingDelegator
244 	 */
245 	private void handleAdapterMethodAttribute(String propertyName, String adapterClassName,
246 			MutablePropertyValues stepPvs, Element element) {
247 		String adapterMethodName = element.getAttribute("adapter-method");
248 		if (StringUtils.hasText(adapterMethodName)) {
249 			//
250 			// Create an adapter
251 			//
252 			AbstractBeanDefinition adapterDef = new GenericBeanDefinition();
253 			adapterDef.setBeanClassName(adapterClassName);
254 			MutablePropertyValues adapterPvs = adapterDef.getPropertyValues();
255 			adapterPvs.addPropertyValue("targetMethod", adapterMethodName);
256 			// Inject the bean into the adapter
257 			adapterPvs.addPropertyValue("targetObject", stepPvs.getPropertyValue(propertyName).getValue());
258 
259 			//
260 			// Inject the adapter into the step
261 			//
262 			stepPvs.addPropertyValue(propertyName, adapterDef);
263 		}
264 	}
265 
266 	private void handleRetryListenersElement(Element element, MutablePropertyValues propertyValues,
267 			ParserContext parserContext, BeanDefinition enclosing) {
268 		Element listenersElement = DomUtils.getChildElementByTagName(element, "retry-listeners");
269 		if (listenersElement != null) {
270 			CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(listenersElement.getTagName(),
271 					parserContext.extractSource(element));
272 			parserContext.pushContainingComponent(compositeDef);
273 			ManagedList retryListenerBeans = new ManagedList();
274 			retryListenerBeans.setMergeEnabled(listenersElement.hasAttribute(MERGE_ATTR)
275 					&& Boolean.valueOf(listenersElement.getAttribute(MERGE_ATTR)));
276 			handleRetryListenerElements(parserContext, listenersElement, retryListenerBeans, enclosing);
277 			propertyValues.addPropertyValue("retryListeners", retryListenerBeans);
278 			parserContext.popAndRegisterContainingComponent();
279 		}
280 	}
281 
282 	@SuppressWarnings("unchecked")
283 	private void handleRetryListenerElements(ParserContext parserContext, Element element, ManagedList beans,
284 			BeanDefinition enclosing) {
285 		List<Element> listenerElements = DomUtils.getChildElementsByTagName(element, "listener");
286 		if (listenerElements != null) {
287 			for (Element listenerElement : listenerElements) {
288 				beans.add(AbstractListenerParser.parseListenerElement(listenerElement, parserContext, enclosing));
289 			}
290 		}
291 	}
292 
293 	@SuppressWarnings("unchecked")
294 	private void handleStreamsElement(Element element, MutablePropertyValues propertyValues, ParserContext parserContext) {
295 		Element streamsElement = DomUtils.getChildElementByTagName(element, "streams");
296 		if (streamsElement != null) {
297 			ManagedList streamBeans = new ManagedList();
298 			streamBeans.setMergeEnabled(streamsElement.hasAttribute(MERGE_ATTR)
299 					&& Boolean.valueOf(streamsElement.getAttribute(MERGE_ATTR)));
300 			List<Element> streamElements = DomUtils.getChildElementsByTagName(streamsElement, "stream");
301 			if (streamElements != null) {
302 				for (Element streamElement : streamElements) {
303 					String streamRef = streamElement.getAttribute(REF_ATTR);
304 					if (StringUtils.hasText(streamRef)) {
305 						streamBeans.add(new RuntimeBeanReference(streamRef));
306 					}
307 					else {
308 						parserContext.getReaderContext().error(
309 								REF_ATTR + " not specified for <" + streamElement.getTagName() + "> element", element);
310 					}
311 				}
312 			}
313 			propertyValues.addPropertyValue("streams", streamBeans);
314 		}
315 	}
316 
317 	@SuppressWarnings("unchecked")
318 	private ManagedMap handleExceptionElement(Element element, ParserContext parserContext, String exceptionListName) {
319 		List<Element> children = DomUtils.getChildElementsByTagName(element, exceptionListName);
320 		if (children.size() == 1) {
321 			ManagedMap map = new ManagedMap();
322 			Element exceptionClassesElement = children.get(0);
323 			map.setMergeEnabled(exceptionClassesElement.hasAttribute(MERGE_ATTR)
324 					&& Boolean.valueOf(exceptionClassesElement.getAttribute(MERGE_ATTR)));
325 			addExceptionClasses("include", true, exceptionClassesElement, map, parserContext);
326 			addExceptionClasses("exclude", false, exceptionClassesElement, map, parserContext);
327 			map.put(ForceRollbackForWriteSkipException.class, true);
328 			return map;
329 		}
330 		else if (children.size() > 1) {
331 			parserContext.getReaderContext().error(
332 					"The <" + exceptionListName + "/> element may not appear more than once in a single <"
333 							+ element.getNodeName() + "/>.", element);
334 		}
335 		return null;
336 	}
337 
338 	@SuppressWarnings("unchecked")
339 	private void addExceptionClasses(String elementName, boolean include, Element exceptionClassesElement,
340 			ManagedMap map, ParserContext parserContext) {
341 		for (Element child : (List<Element>) DomUtils.getChildElementsByTagName(exceptionClassesElement, elementName)) {
342 			String className = child.getAttribute("class");
343 			map.put(new TypedStringValue(className, Class.class), include);
344 		}
345 	}
346 
347 }