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}