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}