001/* 002 * Copyright 2006-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.converter; 017 018import org.springframework.batch.core.JobInstance; 019import org.springframework.batch.core.JobParameter; 020import org.springframework.batch.core.JobParameter.ParameterType; 021import org.springframework.batch.core.JobParameters; 022import org.springframework.batch.core.JobParametersBuilder; 023import org.springframework.lang.Nullable; 024import org.springframework.util.StringUtils; 025 026import java.text.DateFormat; 027import java.text.DecimalFormat; 028import java.text.NumberFormat; 029import java.text.ParseException; 030import java.text.SimpleDateFormat; 031import java.util.Date; 032import java.util.Iterator; 033import java.util.Locale; 034import java.util.Map; 035import java.util.Map.Entry; 036import java.util.Properties; 037 038/** 039 * Converter for {@link JobParameters} instances using a simple naming 040 * convention for property keys. Key names that are prefixed with a - are 041 * considered non-identifying and will not contribute to the identity of a 042 * {@link JobInstance}. Key names ending with "(<type>)" where 043 * type is one of string, date, long are converted to the corresponding type. 044 * The default type is string. E.g. 045 * 046 * <pre> 047 * schedule.date(date)=2007/12/11 048 * department.id(long)=2345 049 * </pre> 050 * 051 * The literal values are converted to the correct type using the default Spring 052 * strategies, augmented if necessary by the custom editors provided. 053 * 054 * <br> 055 * 056 * If you need to be able to parse and format local-specific dates and numbers, 057 * you can inject formatters ({@link #setDateFormat(DateFormat)} and 058 * {@link #setNumberFormat(NumberFormat)}). 059 * 060 * @author Dave Syer 061 * @author Michael Minella 062 * @author Mahmoud Ben Hassine 063 * 064 */ 065public class DefaultJobParametersConverter implements JobParametersConverter { 066 067 public static final String DATE_TYPE = "(date)"; 068 069 public static final String STRING_TYPE = "(string)"; 070 071 public static final String LONG_TYPE = "(long)"; 072 073 private static final String DOUBLE_TYPE = "(double)"; 074 075 private static final String NON_IDENTIFYING_FLAG = "-"; 076 077 private static final String IDENTIFYING_FLAG = "+"; 078 079 private static NumberFormat DEFAULT_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); 080 081 private DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd"); 082 083 private NumberFormat numberFormat = DEFAULT_NUMBER_FORMAT; 084 085 private final NumberFormat longNumberFormat = new DecimalFormat("#"); 086 087 /** 088 * Check for suffix on keys and use those to decide how to convert the 089 * value. 090 * 091 * @throws IllegalArgumentException if a number or date is passed in that 092 * cannot be parsed, or cast to the correct type. 093 * 094 * @see org.springframework.batch.core.converter.JobParametersConverter#getJobParameters(java.util.Properties) 095 */ 096 @Override 097 public JobParameters getJobParameters(@Nullable Properties props) { 098 099 if (props == null || props.isEmpty()) { 100 return new JobParameters(); 101 } 102 103 JobParametersBuilder propertiesBuilder = new JobParametersBuilder(); 104 105 for (Iterator<Entry<Object, Object>> it = props.entrySet().iterator(); it.hasNext();) { 106 Entry<Object, Object> entry = it.next(); 107 String key = (String) entry.getKey(); 108 String value = (String) entry.getValue(); 109 110 boolean identifying = isIdentifyingKey(key); 111 if(!identifying) { 112 key = key.replaceFirst(NON_IDENTIFYING_FLAG, ""); 113 } else if(identifying && key.startsWith(IDENTIFYING_FLAG)) { 114 key = key.replaceFirst("\\" + IDENTIFYING_FLAG, ""); 115 } 116 117 if (key.endsWith(DATE_TYPE)) { 118 Date date; 119 synchronized (dateFormat) { 120 try { 121 date = dateFormat.parse(value); 122 } 123 catch (ParseException ex) { 124 String suffix = (dateFormat instanceof SimpleDateFormat) ? ", use " 125 + ((SimpleDateFormat) dateFormat).toPattern() : ""; 126 throw new IllegalArgumentException("Date format is invalid: [" + value + "]" + suffix); 127 } 128 } 129 propertiesBuilder.addDate(StringUtils.replace(key, DATE_TYPE, ""), date, identifying); 130 } 131 else if (key.endsWith(LONG_TYPE)) { 132 Long result; 133 try { 134 result = (Long) parseNumber(value); 135 } 136 catch (ClassCastException ex) { 137 throw new IllegalArgumentException("Number format is invalid for long value: [" + value 138 + "], use a format with no decimal places"); 139 } 140 propertiesBuilder.addLong(StringUtils.replace(key, LONG_TYPE, ""), result, identifying); 141 } 142 else if (key.endsWith(DOUBLE_TYPE)) { 143 Double result = parseNumber(value).doubleValue(); 144 propertiesBuilder.addDouble(StringUtils.replace(key, DOUBLE_TYPE, ""), result, identifying); 145 } 146 else if (StringUtils.endsWithIgnoreCase(key, STRING_TYPE)) { 147 propertiesBuilder.addString(StringUtils.replace(key, STRING_TYPE, ""), value, identifying); 148 } 149 else { 150 propertiesBuilder.addString(key, value, identifying); 151 } 152 } 153 154 return propertiesBuilder.toJobParameters(); 155 } 156 157 private boolean isIdentifyingKey(String key) { 158 boolean identifying = true; 159 160 if(key.startsWith(NON_IDENTIFYING_FLAG)) { 161 identifying = false; 162 } 163 164 return identifying; 165 } 166 167 /** 168 * Delegate to {@link NumberFormat} to parse the value 169 */ 170 private Number parseNumber(String value) { 171 synchronized (numberFormat) { 172 try { 173 return numberFormat.parse(value); 174 } 175 catch (ParseException ex) { 176 String suffix = (numberFormat instanceof DecimalFormat) ? ", use " 177 + ((DecimalFormat) numberFormat).toPattern() : ""; 178 throw new IllegalArgumentException("Number format is invalid: [" + value + "], use " + suffix); 179 } 180 } 181 } 182 183 /** 184 * Use the same suffixes to create properties (omitting the string suffix 185 * because it is the default). Non-identifying parameters will be prefixed 186 * with the {@link #NON_IDENTIFYING_FLAG}. However, since parameters are 187 * identifying by default, they will <em>not</em> be prefixed with the 188 * {@link #IDENTIFYING_FLAG}. 189 * 190 * @see org.springframework.batch.core.converter.JobParametersConverter#getProperties(org.springframework.batch.core.JobParameters) 191 */ 192 @Override 193 public Properties getProperties(@Nullable JobParameters params) { 194 195 if (params == null || params.isEmpty()) { 196 return new Properties(); 197 } 198 199 Map<String, JobParameter> parameters = params.getParameters(); 200 Properties result = new Properties(); 201 for (Entry<String, JobParameter> entry : parameters.entrySet()) { 202 203 String key = entry.getKey(); 204 JobParameter jobParameter = entry.getValue(); 205 Object value = jobParameter.getValue(); 206 if (value != null) { 207 key = (!jobParameter.isIdentifying()? NON_IDENTIFYING_FLAG : "") + key; 208 if (jobParameter.getType() == ParameterType.DATE) { 209 synchronized (dateFormat) { 210 result.setProperty(key + DATE_TYPE, dateFormat.format(value)); 211 } 212 } 213 else if (jobParameter.getType() == ParameterType.LONG) { 214 synchronized (longNumberFormat) { 215 result.setProperty(key + LONG_TYPE, longNumberFormat.format(value)); 216 } 217 } 218 else if (jobParameter.getType() == ParameterType.DOUBLE) { 219 result.setProperty(key + DOUBLE_TYPE, decimalFormat((Double)value)); 220 } 221 else { 222 result.setProperty(key, "" + value); 223 } 224 } 225 } 226 return result; 227 } 228 229 /** 230 * @param value a decimal value 231 * @return a best guess at the desired format 232 */ 233 private String decimalFormat(double value) { 234 if (numberFormat != DEFAULT_NUMBER_FORMAT) { 235 synchronized (numberFormat) { 236 return numberFormat.format(value); 237 } 238 } 239 return Double.toString(value); 240 } 241 242 /** 243 * Public setter for injecting a date format. 244 * 245 * @param dateFormat a {@link DateFormat}, defaults to "yyyy/MM/dd" 246 */ 247 public void setDateFormat(DateFormat dateFormat) { 248 this.dateFormat = dateFormat; 249 } 250 251 /** 252 * Public setter for the {@link NumberFormat}. Used to parse longs and 253 * doubles, so must not contain decimal place (e.g. use "#" or "#,###"). 254 * 255 * @param numberFormat the {@link NumberFormat} to set 256 */ 257 public void setNumberFormat(NumberFormat numberFormat) { 258 this.numberFormat = numberFormat; 259 } 260}