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 &lt;reader/&gt;, &lt;processor/&gt;, or &lt;writer/&gt; 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}