001/* 002 * Copyright 2006-2009 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.configuration.xml; 017 018import java.util.List; 019 020import org.w3c.dom.Element; 021 022import org.springframework.batch.core.listener.StepListenerMetaData; 023import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; 024import org.springframework.beans.BeanMetadataElement; 025import org.springframework.beans.MutablePropertyValues; 026import org.springframework.beans.factory.config.BeanDefinition; 027import org.springframework.beans.factory.config.BeanDefinitionHolder; 028import org.springframework.beans.factory.config.RuntimeBeanReference; 029import org.springframework.beans.factory.config.TypedStringValue; 030import org.springframework.beans.factory.parsing.CompositeComponentDefinition; 031import org.springframework.beans.factory.support.AbstractBeanDefinition; 032import org.springframework.beans.factory.support.BeanDefinitionBuilder; 033import org.springframework.beans.factory.support.GenericBeanDefinition; 034import org.springframework.beans.factory.support.ManagedList; 035import org.springframework.beans.factory.support.ManagedMap; 036import org.springframework.beans.factory.xml.ParserContext; 037import org.springframework.util.CollectionUtils; 038import org.springframework.util.StringUtils; 039import org.springframework.util.xml.DomUtils; 040 041/** 042 * Internal parser for the <chunk/> element inside a step. 043 * 044 * @author Thomas Risberg 045 * @since 2.0 046 */ 047public class ChunkElementParser { 048 049 private static final String REF_ATTR = "ref"; 050 051 private static final String MERGE_ATTR = "merge"; 052 053 private static final String COMMIT_INTERVAL_ATTR = "commit-interval"; 054 055 private static final String CHUNK_COMPLETION_POLICY_ATTR = "chunk-completion-policy"; 056 057 private static final String BEAN_ELE = "bean"; 058 059 private static final String REF_ELE = "ref"; 060 061 private static final String ITEM_READER_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemReaderAdapter"; 062 063 private static final String ITEM_PROCESSOR_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemProcessorAdapter"; 064 065 private static final String ITEM_WRITER_ADAPTER_CLASS = "org.springframework.batch.item.adapter.ItemWriterAdapter"; 066 067 private static final StepListenerParser stepListenerParser = new StepListenerParser( 068 StepListenerMetaData.itemListenerMetaData()); 069 070 /** 071 * @param bd {@link AbstractBeanDefinition} instance of the containing bean. 072 * @param element the element to parse 073 * @param parserContext the context to use 074 * @param underspecified if true, a fatal error will not be raised if attribute 075 * or element is missing. 076 */ 077 protected void parse(Element element, AbstractBeanDefinition bd, ParserContext parserContext, boolean underspecified) { 078 079 MutablePropertyValues propertyValues = bd.getPropertyValues(); 080 081 propertyValues.addPropertyValue("hasChunkElement", Boolean.TRUE); 082 083 handleItemHandler(bd, "reader", "itemReader", ITEM_READER_ADAPTER_CLASS, true, element, parserContext, 084 propertyValues, underspecified); 085 handleItemHandler(bd, "processor", "itemProcessor", ITEM_PROCESSOR_ADAPTER_CLASS, false, element, parserContext, 086 propertyValues, underspecified); 087 handleItemHandler(bd, "writer", "itemWriter", ITEM_WRITER_ADAPTER_CLASS, true, element, parserContext, 088 propertyValues, underspecified); 089 090 String commitInterval = element.getAttribute(COMMIT_INTERVAL_ATTR); 091 if (StringUtils.hasText(commitInterval)) { 092 if (commitInterval.startsWith("#")) { 093 // It's a late binding expression, so we need step scope... 094 BeanDefinitionBuilder completionPolicy = BeanDefinitionBuilder 095 .genericBeanDefinition(SimpleCompletionPolicy.class); 096 completionPolicy.addConstructorArgValue(commitInterval); 097 completionPolicy.setScope("step"); 098 propertyValues.addPropertyValue("chunkCompletionPolicy", completionPolicy.getBeanDefinition()); 099 } 100 else { 101 propertyValues.addPropertyValue("commitInterval", commitInterval); 102 } 103 } 104 105 String completionPolicyRef = element.getAttribute(CHUNK_COMPLETION_POLICY_ATTR); 106 if (StringUtils.hasText(completionPolicyRef)) { 107 RuntimeBeanReference completionPolicy = new RuntimeBeanReference(completionPolicyRef); 108 propertyValues.addPropertyValue("chunkCompletionPolicy", completionPolicy); 109 } 110 111 if (!underspecified 112 && propertyValues.contains("commitInterval") == propertyValues.contains("chunkCompletionPolicy")) { 113 if (propertyValues.contains("commitInterval")) { 114 parserContext.getReaderContext().error( 115 "The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR 116 + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "', but not both.", element); 117 } 118 else { 119 parserContext.getReaderContext().error( 120 "The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR 121 + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "'.", element); 122 123 } 124 } 125 126 String skipLimit = element.getAttribute("skip-limit"); 127 ManagedMap<TypedStringValue, Boolean> skippableExceptions = 128 new ExceptionElementParser().parse(element, parserContext, "skippable-exception-classes"); 129 if (StringUtils.hasText(skipLimit)) { 130 if (skippableExceptions == null) { 131 skippableExceptions = new ManagedMap<TypedStringValue, Boolean>(); 132 skippableExceptions.setMergeEnabled(true); 133 } 134 propertyValues.addPropertyValue("skipLimit", skipLimit); 135 } 136 if (skippableExceptions != null) { 137 List<Element> exceptionClassElements = DomUtils.getChildElementsByTagName(element, "skippable-exception-classes"); 138 139 if(!CollectionUtils.isEmpty(exceptionClassElements)) { 140 skippableExceptions.setMergeEnabled(exceptionClassElements.get(0).hasAttribute(MERGE_ATTR) 141 && Boolean.valueOf(exceptionClassElements.get(0).getAttribute(MERGE_ATTR))); 142 } 143 // Even if there is no retryLimit, we can still accept exception 144 // classes for an abstract parent bean definition 145 propertyValues.addPropertyValue("skippableExceptionClasses", skippableExceptions); 146 } 147 148 handleItemHandler(bd, "skip-policy", "skipPolicy", null, false, element, parserContext, propertyValues, 149 underspecified); 150 151 String retryLimit = element.getAttribute("retry-limit"); 152 ManagedMap<TypedStringValue, Boolean> retryableExceptions = 153 new ExceptionElementParser().parse(element, parserContext, "retryable-exception-classes"); 154 if (StringUtils.hasText(retryLimit)) { 155 if (retryableExceptions == null) { 156 retryableExceptions = new ManagedMap<TypedStringValue, Boolean>(); 157 retryableExceptions.setMergeEnabled(true); 158 } 159 propertyValues.addPropertyValue("retryLimit", retryLimit); 160 } 161 if (retryableExceptions != null) { 162 List<Element> exceptionClassElements = DomUtils.getChildElementsByTagName(element, "retryable-exception-classes"); 163 164 if(!CollectionUtils.isEmpty(exceptionClassElements)) { 165 retryableExceptions.setMergeEnabled(exceptionClassElements.get(0).hasAttribute(MERGE_ATTR) 166 && Boolean.valueOf(exceptionClassElements.get(0).getAttribute(MERGE_ATTR))); 167 } 168 // Even if there is no retryLimit, we can still accept exception 169 // classes for an abstract parent bean definition 170 propertyValues.addPropertyValue("retryableExceptionClasses", retryableExceptions); 171 } 172 173 handleItemHandler(bd, "retry-policy", "retryPolicy", null, false, element, parserContext, propertyValues, 174 underspecified); 175 176 String cacheCapacity = element.getAttribute("cache-capacity"); 177 if (StringUtils.hasText(cacheCapacity)) { 178 propertyValues.addPropertyValue("cacheCapacity", cacheCapacity); 179 } 180 181 String isReaderTransactionalQueue = element.getAttribute("reader-transactional-queue"); 182 if (StringUtils.hasText(isReaderTransactionalQueue)) { 183 propertyValues.addPropertyValue("isReaderTransactionalQueue", isReaderTransactionalQueue); 184 } 185 186 String isProcessorTransactional = element.getAttribute("processor-transactional"); 187 if (StringUtils.hasText(isProcessorTransactional)) { 188 propertyValues.addPropertyValue("processorTransactional", isProcessorTransactional); 189 } 190 191 handleRetryListenersElement(element, propertyValues, parserContext, bd); 192 193 handleStreamsElement(element, propertyValues, parserContext); 194 195 stepListenerParser.handleListenersElement(element, bd, parserContext); 196 197 } 198 199 /** 200 * Handle the ItemReader, ItemProcessor, and ItemWriter attributes/elements. 201 */ 202 private void handleItemHandler(AbstractBeanDefinition enclosing, String handlerName, String propertyName, String adapterClassName, boolean required, 203 Element element, ParserContext parserContext, MutablePropertyValues propertyValues, boolean underspecified) { 204 String refName = element.getAttribute(handlerName); 205 List<Element> children = DomUtils.getChildElementsByTagName(element, handlerName); 206 if (children.size() == 1) { 207 if (StringUtils.hasText(refName)) { 208 parserContext.getReaderContext().error( 209 "The <" + element.getNodeName() + "/> element may not have both a '" + handlerName 210 + "' attribute and a <" + handlerName + "/> element.", element); 211 } 212 handleItemHandlerElement(enclosing, propertyName, adapterClassName, propertyValues, children.get(0), parserContext); 213 } 214 else if (children.size() > 1) { 215 parserContext.getReaderContext().error( 216 "The <" + handlerName + "/> element may not appear more than once in a single <" 217 + element.getNodeName() + "/>.", element); 218 } 219 else if (StringUtils.hasText(refName)) { 220 propertyValues.addPropertyValue(propertyName, new RuntimeBeanReference(refName)); 221 } 222 else if (required && !underspecified) { 223 parserContext.getReaderContext().error( 224 "The <" + element.getNodeName() + "/> element has neither a '" + handlerName 225 + "' attribute nor a <" + handlerName + "/> element.", element); 226 } 227 } 228 229 /** 230 * Handle the <reader/>, <processor/>, or <writer/> that 231 * is defined within the item handler. 232 */ 233 private void handleItemHandlerElement(AbstractBeanDefinition enclosing, String propertyName, String adapterClassName, 234 MutablePropertyValues propertyValues, Element element, ParserContext parserContext) { 235 List<Element> beanElements = DomUtils.getChildElementsByTagName(element, BEAN_ELE); 236 List<Element> refElements = DomUtils.getChildElementsByTagName(element, REF_ELE); 237 if (beanElements.size() + refElements.size() != 1) { 238 parserContext.getReaderContext().error( 239 "The <" + element.getNodeName() + "/> must have exactly one of either a <" + BEAN_ELE 240 + "/> element or a <" + REF_ELE + "/> element.", element); 241 } 242 else if (beanElements.size() == 1) { 243 Element beanElement = beanElements.get(0); 244 BeanDefinitionHolder beanDefinitionHolder = parserContext.getDelegate().parseBeanDefinitionElement( 245 beanElement, enclosing); 246 parserContext.getDelegate().decorateBeanDefinitionIfRequired(beanElement, beanDefinitionHolder); 247 248 propertyValues.addPropertyValue(propertyName, beanDefinitionHolder); 249 } 250 else if (refElements.size() == 1) { 251 propertyValues.addPropertyValue(propertyName, 252 parserContext.getDelegate().parsePropertySubElement(refElements.get(0), null)); 253 } 254 255 handleAdapterMethodAttribute(propertyName, adapterClassName, propertyValues, element); 256 } 257 258 /** 259 * Handle the adapter-method attribute by using an 260 * AbstractMethodInvokingDelegator 261 */ 262 private void handleAdapterMethodAttribute(String propertyName, String adapterClassName, 263 MutablePropertyValues stepPvs, Element element) { 264 String adapterMethodName = element.getAttribute("adapter-method"); 265 if (StringUtils.hasText(adapterMethodName)) { 266 // 267 // Create an adapter 268 // 269 AbstractBeanDefinition adapterDef = new GenericBeanDefinition(); 270 adapterDef.setBeanClassName(adapterClassName); 271 MutablePropertyValues adapterPvs = adapterDef.getPropertyValues(); 272 adapterPvs.addPropertyValue("targetMethod", adapterMethodName); 273 // Inject the bean into the adapter 274 adapterPvs.addPropertyValue("targetObject", stepPvs.getPropertyValue(propertyName).getValue()); 275 276 // 277 // Inject the adapter into the step 278 // 279 stepPvs.addPropertyValue(propertyName, adapterDef); 280 } 281 } 282 283 private void handleRetryListenersElement(Element element, MutablePropertyValues propertyValues, 284 ParserContext parserContext, BeanDefinition enclosing) { 285 Element listenersElement = DomUtils.getChildElementByTagName(element, "retry-listeners"); 286 if (listenersElement != null) { 287 CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(listenersElement.getTagName(), 288 parserContext.extractSource(element)); 289 parserContext.pushContainingComponent(compositeDef); 290 ManagedList<BeanMetadataElement> retryListenerBeans = new ManagedList<BeanMetadataElement>(); 291 retryListenerBeans.setMergeEnabled(listenersElement.hasAttribute(MERGE_ATTR) 292 && Boolean.valueOf(listenersElement.getAttribute(MERGE_ATTR))); 293 handleRetryListenerElements(parserContext, listenersElement, retryListenerBeans, enclosing); 294 propertyValues.addPropertyValue("retryListeners", retryListenerBeans); 295 parserContext.popAndRegisterContainingComponent(); 296 } 297 } 298 299 private void handleRetryListenerElements(ParserContext parserContext, Element element, ManagedList<BeanMetadataElement> beans, 300 BeanDefinition enclosing) { 301 List<Element> listenerElements = DomUtils.getChildElementsByTagName(element, "listener"); 302 if (listenerElements != null) { 303 for (Element listenerElement : listenerElements) { 304 beans.add(AbstractListenerParser.parseListenerElement(listenerElement, parserContext, enclosing)); 305 } 306 } 307 } 308 309 private void handleStreamsElement(Element element, MutablePropertyValues propertyValues, ParserContext parserContext) { 310 Element streamsElement = DomUtils.getChildElementByTagName(element, "streams"); 311 if (streamsElement != null) { 312 ManagedList<RuntimeBeanReference> streamBeans = new ManagedList<RuntimeBeanReference>(); 313 streamBeans.setMergeEnabled(streamsElement.hasAttribute(MERGE_ATTR) 314 && Boolean.valueOf(streamsElement.getAttribute(MERGE_ATTR))); 315 List<Element> streamElements = DomUtils.getChildElementsByTagName(streamsElement, "stream"); 316 if (streamElements != null) { 317 for (Element streamElement : streamElements) { 318 String streamRef = streamElement.getAttribute(REF_ATTR); 319 if (StringUtils.hasText(streamRef)) { 320 streamBeans.add(new RuntimeBeanReference(streamRef)); 321 } 322 else { 323 parserContext.getReaderContext().error( 324 REF_ATTR + " not specified for <" + streamElement.getTagName() + "> element", element); 325 } 326 } 327 } 328 propertyValues.addPropertyValue("streams", streamBeans); 329 } 330 } 331 332}