/* * Copyright 2013-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.xd.dirt.plugins.job; import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.SortedMap; import java.util.TreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.xd.rest.domain.util.TimeUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.MapType; /** * More flexible implementation of the {@link JobParametersConverter}. Allows to convert a wide variety of object types * to {@link JobParameters}. * * @author Gunnar Hillert * @since 1.0 * */ public class ExpandedJobParametersConverter extends DefaultJobParametersConverter { protected final Logger logger = LoggerFactory.getLogger(getClass()); public static final String ABSOLUTE_FILE_PATH = "absoluteFilePath"; public static final String UNIQUE_JOB_PARAMETER_KEY = "random"; public static final String IS_RESTART_JOB_PARAMETER_KEY = "XD_isRestart"; private volatile boolean makeParametersUnique = true; private final ObjectMapper objectMapper = new ObjectMapper(); /** * Default Constructor, initializing {@link DefaultJobParametersConverter#setDateFormat(DateFormat)} * with {@link TimeUtils#getDefaultDateFormat()}. */ public ExpandedJobParametersConverter() { this.setDateFormat(TimeUtils.getDefaultDateFormat()); } /** * Will set the {@link DateFormat} on the underlying {@link DefaultJobParametersConverter}. If not set explicitly, * the {@link DateFormat} will default to {@link TimeUtils#getDefaultDateFormat()}. * * @param dateFormat Must not be null */ @Override public void setDateFormat(DateFormat dateFormat) { Assert.notNull(dateFormat, "The provided dateFormat must not be null."); super.setDateFormat(dateFormat); } /** * Allows for setting the {@link DateFormat} using a {@link String}. If not * set, the default {@link DateFormat} used will be {@link TimeUtils#getDefaultDateFormat()}. * * @param dateFormatAsString Will be ignored if null or empty. */ public void setDateFormatAsString(String dateFormatAsString) { if (StringUtils.hasText(dateFormatAsString)) { super.setDateFormat(new SimpleDateFormat(dateFormatAsString)); } } /** * If not set, this property defaults to <code>true</code>. * * @param makeParametersUnique If not set defaults to {@code true} */ public void setMakeParametersUnique(boolean makeParametersUnique) { this.makeParametersUnique = makeParametersUnique; } /** * Setter for the {@link NumberFormat} which is set on the underlying {@link DefaultJobParametersConverter}. If not * set explicitly, defaults to {@code NumberFormat.getInstance(Locale.US);} * * @param numberFormat Must not be null. */ @Override public void setNumberFormat(NumberFormat numberFormat) { Assert.notNull(numberFormat, "The provided numberFormat must not be null."); super.setNumberFormat(numberFormat); } /** * Allows for setting the {@link NumberFormat} using a {@link String}. The passed-in String will be converted to a * {@link DecimalFormat}. * * @param numberFormatAsString Will be ignored if null or empty. */ public void setNumberFormatAsString(String numberFormatAsString) { if (StringUtils.hasText(numberFormatAsString)) { super.setNumberFormat(new DecimalFormat(numberFormatAsString)); } } /** * Return {@link JobParameters} for the passed-in {@link File}. Will set the {@link JobParameter} with key * {@link #ABSOLUTE_FILE_PATH} to the {@link File}'s absolutePath. Method will ultimately call * {@link #getJobParameters(Properties)}. * * @param file Must not be null. */ public JobParameters getJobParametersForFile(File file) { Assert.notNull(file, "The provided file must not be null."); final Properties parametersAsProperties = new Properties(); parametersAsProperties.put(ABSOLUTE_FILE_PATH, file.getAbsolutePath()); return this.getJobParameters(parametersAsProperties); } /** * Converts a {@link String}-based JSON map to {@link JobParameters}. The String is converted using Jackson's * {@link ObjectMapper}. * * The method will ultimately call {@link #getJobParametersForMap(Map)}. * * @param jobParametersAsJsonMap Can be null or empty. */ public JobParameters getJobParametersForJsonString(String jobParametersAsJsonMap) { final Map<String, Object> parameters; if (jobParametersAsJsonMap != null && !jobParametersAsJsonMap.isEmpty()) { final MapType mapType = objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, String.class); try { parameters = new ObjectMapper().readValue(jobParametersAsJsonMap, mapType); } catch (IOException e) { throw new IllegalArgumentException("Unable to convert provided JSON to Map<String, Object>", e); } } else { parameters = null; } return getJobParametersForMap(parameters); } /** * Will convert the provided {@link Map} into {@link JobParameters}. The method will ultimately call * {@link #getJobParameters(Properties)}. * * @param map Can be null or an empty {@link Map}. */ public JobParameters getJobParametersForMap(Map<?, ?> map) { final Properties parametersAsProperties = new Properties(); if (map != null) { parametersAsProperties.putAll(map); } return this.getJobParameters(parametersAsProperties); } /** * If {@link #makeParametersUnique} is {@code true} the {@link JobParameter} with key * {@link #UNIQUE_JOB_PARAMETER_KEY} will be added with a random number value. * * The method will ultimately call {@link DefaultJobParametersConverter#getJobParameters(Properties)}. * * @param properties Can be null. */ @Override public JobParameters getJobParameters(Properties properties) { final Properties localProperties; if (properties != null) { localProperties = properties; } else { localProperties = new Properties(); } final boolean isRestart; if (localProperties.containsKey(IS_RESTART_JOB_PARAMETER_KEY)) { isRestart = Boolean.valueOf(localProperties.getProperty(IS_RESTART_JOB_PARAMETER_KEY)); } else { isRestart = false; } if (this.makeParametersUnique && !isRestart) { if (localProperties.containsKey(UNIQUE_JOB_PARAMETER_KEY)) { throw new IllegalStateException(String.format( "Parameter '%s' is already used to identify uniqueness for the executing Batch job.", UNIQUE_JOB_PARAMETER_KEY)); } localProperties.put(UNIQUE_JOB_PARAMETER_KEY, String.valueOf(Math.random())); } return super.getJobParameters(localProperties); } /** * This method will convert {@link JobParameters} to a JSON String. The parameters in the resulting JSON String are * sorted by the name of the parameters. * * This method will delegate to {@link #getJobParametersAsString(JobParameters, boolean)} * * @param jobParameters Must not be null * @return A JSON String representation of the {@link JobParameters} */ public String getJobParametersAsString(JobParameters jobParameters) { return this.getJobParametersAsString(jobParameters, false); } /** * This method will convert {@link JobParameters} to a JSON String. The parameters in the resulting JSON String are * sorted by the name of the parameters. * * @param jobParameters Must not be null * @param isRestart When {@code true}, add a restart flag * @return A JSON String representation of the {@link JobParameters} */ public String getJobParametersAsString(JobParameters jobParameters, boolean isRestart) { Assert.notNull(jobParameters, "jobParameters must not be null."); final Properties properties = this.getProperties(jobParameters); if (isRestart) { properties.put(IS_RESTART_JOB_PARAMETER_KEY, Boolean.TRUE.toString()); } @SuppressWarnings({ "unchecked", "rawtypes" }) final SortedMap<String, String> sortedJobParameters = new TreeMap(properties); final String jobParametersAsString; try { jobParametersAsString = new ObjectMapper().writeValueAsString(sortedJobParameters); } catch (JsonProcessingException e) { throw new IllegalArgumentException("Unable to convert provided job parameters to JSON String.", e); } return jobParametersAsString; } /** * If {@link JobParameters} contains a parameters named {@value #IS_RESTART_JOB_PARAMETER_KEY} removed it. * * @param jobParameters Must not be null * @return A new instance of {@link JobParameters} */ public JobParameters removeRestartParameterIfExists(JobParameters jobParameters) { Assert.notNull(jobParameters, "'jobParameters' must not be null."); final JobParametersBuilder jobParametersBuilder = new JobParametersBuilder(); for (Map.Entry<String, JobParameter> entry : jobParameters.getParameters().entrySet()) { if (!IS_RESTART_JOB_PARAMETER_KEY.equalsIgnoreCase(entry.getKey())) { jobParametersBuilder.addParameter(entry.getKey(), entry.getValue()); } } return jobParametersBuilder.toJobParameters(); } }