001/*
002 * Copyright 2013-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.jsr.configuration.xml;
017
018import java.util.Enumeration;
019import java.util.HashMap;
020import java.util.Map;
021import java.util.Properties;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.springframework.batch.core.jsr.configuration.support.JsrExpressionParser;
028import org.springframework.beans.factory.config.BeanDefinition;
029import org.springframework.beans.factory.support.AbstractBeanDefinition;
030import org.springframework.beans.factory.support.BeanDefinitionBuilder;
031import org.springframework.beans.factory.support.BeanDefinitionRegistry;
032import org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader;
033import org.springframework.util.ClassUtils;
034import org.w3c.dom.Element;
035import org.w3c.dom.NamedNodeMap;
036import org.w3c.dom.Node;
037import org.w3c.dom.NodeList;
038import org.w3c.dom.ls.DOMImplementationLS;
039import org.w3c.dom.traversal.DocumentTraversal;
040import org.w3c.dom.traversal.NodeFilter;
041import org.w3c.dom.traversal.NodeIterator;
042
043/**
044 * <p>
045 * {@link DefaultBeanDefinitionDocumentReader} extension to hook into the pre processing of the provided
046 * XML document, ensuring any references to property operators such as jobParameters and jobProperties are
047 * resolved prior to loading the context. Since we know these initial values upfront, doing this transformation
048 * allows us to ensure values are retrieved in their resolved form prior to loading the context and property
049 * operators can be used on any element. This document reader will also look for references to artifacts by
050 * the same name and create new bean definitions to provide the ability to create new instances.
051 * </p>
052 *
053 * @author Chris Schaefer
054 * @author Mahmoud Ben Hassine
055 * @since 3.0
056 */
057public class JsrBeanDefinitionDocumentReader extends DefaultBeanDefinitionDocumentReader {
058        private static final String NULL = "null";
059        private static final String ROOT_JOB_ELEMENT_NAME = "job";
060        private static final String JOB_PROPERTY_ELEMENT_NAME = "property";
061        private static final String JOB_PROPERTIES_ELEMENT_NAME = "properties";
062        private static final String JOB_PROPERTY_ELEMENT_NAME_ATTRIBUTE = "name";
063        private static final String JOB_PROPERTY_ELEMENT_VALUE_ATTRIBUTE = "value";
064        private static final String JOB_PROPERTIES_KEY_NAME = "jobProperties";
065        private static final String JOB_PARAMETERS_KEY_NAME = "jobParameters";
066        private static final String JOB_PARAMETERS_BEAN_DEFINITION_NAME = "jsr_jobParameters";
067        private static final Log LOG = LogFactory.getLog(JsrBeanDefinitionDocumentReader.class);
068        private static final Pattern PROPERTY_KEY_SEPARATOR = Pattern.compile("'([^']*?)'");
069        private static final Pattern OPERATOR_PATTERN = Pattern.compile("(#\\{(job(Properties|Parameters))[^}]+\\})");
070
071        private BeanDefinitionRegistry beanDefinitionRegistry;
072        private JsrExpressionParser expressionParser = new JsrExpressionParser();
073        private Map<String, Properties> propertyMap = new HashMap<String, Properties>();
074
075        /**
076         * <p>
077         * Creates a new {@link JsrBeanDefinitionDocumentReader} instance.
078         * </p>
079         */
080        public JsrBeanDefinitionDocumentReader() { }
081
082        /**
083         * <p>
084         * Create a new {@link JsrBeanDefinitionDocumentReader} instance with the provided
085         * {@link BeanDefinitionRegistry}.
086         * </p>
087         *
088         * @param beanDefinitionRegistry the {@link BeanDefinitionRegistry} to use
089         */
090        public JsrBeanDefinitionDocumentReader(BeanDefinitionRegistry beanDefinitionRegistry) {
091                this.beanDefinitionRegistry = beanDefinitionRegistry;
092        }
093
094        @Override
095        protected void preProcessXml(Element root) {
096                if (ROOT_JOB_ELEMENT_NAME.equals(root.getLocalName())) {
097                        initProperties(root);
098                        transformDocument(root);
099
100                        if (LOG.isDebugEnabled()) {
101                                LOG.debug("Transformed XML from preProcessXml: " + elementToString(root));
102                        }
103                }
104        }
105
106        protected void initProperties(Element root) {
107                propertyMap.put(JOB_PARAMETERS_KEY_NAME, initJobParameters());
108                propertyMap.put(JOB_PROPERTIES_KEY_NAME, initJobProperties(root));
109
110                resolvePropertyValues(propertyMap.get(JOB_PARAMETERS_KEY_NAME));
111                resolvePropertyValues(propertyMap.get(JOB_PROPERTIES_KEY_NAME));
112        }
113
114        private Properties initJobParameters() {
115                Properties jobParameters = new Properties();
116
117                if (getBeanDefinitionRegistry().containsBeanDefinition(JOB_PARAMETERS_BEAN_DEFINITION_NAME)) {
118                        BeanDefinition beanDefinition = getBeanDefinitionRegistry().getBeanDefinition(JOB_PARAMETERS_BEAN_DEFINITION_NAME);
119
120                        Properties properties = (Properties) beanDefinition.getConstructorArgumentValues()
121                                        .getGenericArgumentValue(Properties.class)
122                                        .getValue();
123
124                        if (properties == null) {
125                                return new Properties();
126                        }
127
128                        Enumeration<?> propertyNames = properties.propertyNames();
129
130                        while(propertyNames.hasMoreElements()) {
131                                String curName = (String) propertyNames.nextElement();
132                                jobParameters.put(curName, properties.getProperty(curName));
133                        }
134                }
135
136                return jobParameters;
137        }
138
139        private Properties initJobProperties(Element root) {
140                Properties properties = new Properties();
141                Node propertiesNode = root.getElementsByTagName(JOB_PROPERTIES_ELEMENT_NAME).item(0);
142
143                if(propertiesNode != null) {
144                        NodeList children = propertiesNode.getChildNodes();
145
146                        for(int i=0; i < children.getLength(); i++) {
147                                Node child = children.item(i);
148
149                                if(JOB_PROPERTY_ELEMENT_NAME.equals(child.getLocalName())) {
150                                        NamedNodeMap attributes = child.getAttributes();
151                                        Node name = attributes.getNamedItem(JOB_PROPERTY_ELEMENT_NAME_ATTRIBUTE);
152                                        Node value = attributes.getNamedItem(JOB_PROPERTY_ELEMENT_VALUE_ATTRIBUTE);
153
154                                        properties.setProperty(name.getNodeValue(), value.getNodeValue());
155                                }
156                        }
157                }
158
159                return properties;
160        }
161
162        private void resolvePropertyValues(Properties properties) {
163                for (String propertyKey : properties.stringPropertyNames()) {
164                        String resolvedPropertyValue = resolvePropertyValue(properties.getProperty(propertyKey));
165
166                        if(!properties.getProperty(propertyKey).equals(resolvedPropertyValue)) {
167                                properties.setProperty(propertyKey, resolvedPropertyValue);
168                        }
169                }
170        }
171
172        private String resolvePropertyValue(String propertyValue) {
173                String resolvedValue = resolveValue(propertyValue);
174
175                Matcher jobParameterMatcher = OPERATOR_PATTERN.matcher(resolvedValue);
176
177                while (jobParameterMatcher.find()) {
178                        resolvedValue = resolvePropertyValue(resolvedValue);
179                }
180
181                return resolvedValue;
182        }
183
184        private String resolveValue(String value) {
185                StringBuffer valueBuffer = new StringBuffer();
186                Matcher jobParameterMatcher = OPERATOR_PATTERN.matcher(value);
187
188                while (jobParameterMatcher.find()) {
189                        Matcher jobParameterKeyMatcher = PROPERTY_KEY_SEPARATOR.matcher(jobParameterMatcher.group(1));
190
191                        if (jobParameterKeyMatcher.find()) {
192                                String propertyType = jobParameterMatcher.group(2);
193                                String extractedProperty = jobParameterKeyMatcher.group(1);
194
195                                Properties properties = propertyMap.get(propertyType);
196
197                                if(properties == null) {
198                                        throw new IllegalArgumentException("Unknown property type: " + propertyType);
199                                }
200
201                                String resolvedProperty = properties.getProperty(extractedProperty, NULL);
202
203                                if (NULL.equals(resolvedProperty)) {
204                                        LOG.info(propertyType + " with key of: " + extractedProperty + " could not be resolved. Possible configuration error?");
205                                }
206
207                                jobParameterMatcher.appendReplacement(valueBuffer, resolvedProperty);
208                        }
209                }
210
211                jobParameterMatcher.appendTail(valueBuffer);
212                String resolvedValue = valueBuffer.toString();
213
214                if (NULL.equals(resolvedValue)) {
215                        return "";
216                }
217
218                return expressionParser.parseExpression(resolvedValue);
219        }
220
221        private BeanDefinitionRegistry getBeanDefinitionRegistry() {
222                return beanDefinitionRegistry != null ? beanDefinitionRegistry : getReaderContext().getRegistry();
223        }
224
225        private void transformDocument(Element root) {
226                DocumentTraversal traversal = (DocumentTraversal) root.getOwnerDocument();
227                NodeIterator iterator = traversal.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, null, true);
228
229                BeanDefinitionRegistry registry = getBeanDefinitionRegistry();
230                Map<String, Integer> referenceCountMap = new HashMap<String, Integer>();
231
232                for (Node n = iterator.nextNode(); n != null; n = iterator.nextNode()) {
233                        NamedNodeMap map = n.getAttributes();
234
235                        if (map.getLength() > 0) {
236                                for (int i = 0; i < map.getLength(); i++) {
237                                        Node node = map.item(i);
238
239                                        String nodeName = node.getNodeName();
240                                        String nodeValue = node.getNodeValue();
241                                        String resolvedValue = resolveValue(nodeValue);
242                                        String newNodeValue = resolvedValue;
243
244                                        if("ref".equals(nodeName)) {
245                                                if(!referenceCountMap.containsKey(resolvedValue)) {
246                                                        referenceCountMap.put(resolvedValue, 0);
247                                                }
248
249                                                boolean isClass = isClass(resolvedValue);
250                                                Integer referenceCount = referenceCountMap.get(resolvedValue);
251
252                                                // possibly fully qualified class name in ref tag in the JSL or pointer to bean/artifact ref.
253                                                if(isClass && !registry.containsBeanDefinition(resolvedValue)) {
254                                                        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(resolvedValue)
255                                                                        .getBeanDefinition();
256                                                        beanDefinition.setScope("step");
257                                                        registry.registerBeanDefinition(resolvedValue, beanDefinition);
258
259                                                        newNodeValue = resolvedValue;
260                                                } else {
261                                                        if(registry.containsBeanDefinition(resolvedValue)) {
262                                                                referenceCount++;
263                                                                referenceCountMap.put(resolvedValue, referenceCount);
264
265                                                                newNodeValue = resolvedValue + referenceCount;
266
267                                                                BeanDefinition beanDefinition = registry.getBeanDefinition(resolvedValue);
268                                                                registry.registerBeanDefinition(newNodeValue, beanDefinition);
269                                                        }
270                                                }
271                                        }
272
273                                        if(!nodeValue.equals(newNodeValue)) {
274                                                node.setNodeValue(newNodeValue);
275                                        }
276                                }
277                        } else {
278                                String nodeValue = n.getTextContent();
279                                String resolvedValue = resolveValue(nodeValue);
280
281                                if(!nodeValue.equals(resolvedValue)) {
282                                        n.setTextContent(resolvedValue);
283                                }
284                        }
285                }
286        }
287
288        private boolean isClass(String className) {
289                try {
290                        Class.forName(className, false, ClassUtils.getDefaultClassLoader());
291                } catch (ClassNotFoundException e) {
292                        return false;
293                }
294
295                return true;
296        }
297
298        protected Properties getJobParameters() {
299                return propertyMap.get(JOB_PARAMETERS_KEY_NAME);
300        }
301
302        protected Properties getJobProperties() {
303                return propertyMap.get(JOB_PROPERTIES_KEY_NAME);
304        }
305
306        private String elementToString(Element root) {
307                DOMImplementationLS domImplLS = (DOMImplementationLS) root.getOwnerDocument().getImplementation();
308                return domImplLS.createLSSerializer().writeToString(root);
309        }
310}