BATCH-2110: Updated batch to support Spring 4
* Refactored iBatis based readers and writers to not utilize SqlMapClientTemplate. * Depricated all iBatis based readers and writers in favor of MyBatis's native Spring support. * Updated XStream support to 1.4.4 and Jettison to 1.2 to be in alignment with Spring 4. * Added the PooledEmbeddedDataSource to address the issue outlined in SPR-11372.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 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.batch.core;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
|
||||
/**
|
||||
* As of Spring 3.2, when a context is closed, the shutdown method is
|
||||
* called on any beans that are registered. With an embedded database
|
||||
* that uses a connection pool, this can leave the connection pool open
|
||||
* with stale connections. This wraps an {@link EmbeddedDatabase} and
|
||||
* ignores calls to {@link EmbeddedDatabase#shutdown()}.
|
||||
*
|
||||
* @author Phil Webb
|
||||
* @since 3.0
|
||||
*/
|
||||
public class PooledEmbeddedDataSource implements EmbeddedDatabase {
|
||||
|
||||
private final EmbeddedDatabase dataSource;
|
||||
|
||||
/**
|
||||
* @param dataSource The database to be wrapped
|
||||
*/
|
||||
public PooledEmbeddedDataSource(EmbeddedDatabase dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.sql.DataSource#getConnection()
|
||||
*/
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
return this.dataSource.getConnection();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
return this.dataSource.getConnection(username, password);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.sql.CommonDataSource#getLogWriter()
|
||||
*/
|
||||
@Override
|
||||
public PrintWriter getLogWriter() throws SQLException {
|
||||
return this.dataSource.getLogWriter();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.sql.CommonDataSource#setLogWriter(java.io.PrintWriter)
|
||||
*/
|
||||
@Override
|
||||
public void setLogWriter(PrintWriter out) throws SQLException {
|
||||
this.dataSource.setLogWriter(out);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.sql.CommonDataSource#getLoginTimeout()
|
||||
*/
|
||||
@Override
|
||||
public int getLoginTimeout() throws SQLException {
|
||||
return this.dataSource.getLoginTimeout();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.sql.CommonDataSource#setLoginTimeout(int)
|
||||
*/
|
||||
@Override
|
||||
public void setLoginTimeout(int seconds) throws SQLException {
|
||||
this.dataSource.setLoginTimeout(seconds);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.sql.Wrapper#unwrap(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> T unwrap(Class<T> iface) throws SQLException {
|
||||
return this.dataSource.unwrap(iface);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public boolean isWrapperFor(Class<?> iface) throws SQLException {
|
||||
return this.dataSource.isWrapperFor(iface);
|
||||
}
|
||||
|
||||
public Logger getParentLogger() {
|
||||
return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.jdbc.datasource.embedded.EmbeddedDatabase#shutdown()
|
||||
*/
|
||||
@Override
|
||||
public void shutdown() {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2006-2007 the original author or authors.
|
||||
* Copyright 2006-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.
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
package org.springframework.batch.core;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.core.job.JobSupport;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.support.ChildBeanDefinition;
|
||||
@@ -25,34 +27,42 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
|
||||
public class SpringBeanJobTests extends TestCase {
|
||||
public class SpringBeanJobTests {
|
||||
|
||||
@Test
|
||||
public void testBeanName() throws Exception {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
JobSupport configuration = new JobSupport();
|
||||
context.getAutowireCapableBeanFactory().initializeBean(configuration,
|
||||
"bean");
|
||||
context.refresh();
|
||||
assertNotNull(configuration.getName());
|
||||
configuration.setBeanName("foo");
|
||||
context.getAutowireCapableBeanFactory().initializeBean(configuration,
|
||||
"bean");
|
||||
assertEquals("bean", configuration.getName());
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBeanNameWithBeanDefinition() throws Exception {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
ConstructorArgumentValues args = new ConstructorArgumentValues();
|
||||
args.addGenericArgumentValue("foo");
|
||||
context.registerBeanDefinition("bean", new RootBeanDefinition(
|
||||
JobSupport.class, args, null));
|
||||
|
||||
context.refresh();
|
||||
JobSupport configuration = (JobSupport) context
|
||||
.getBean("bean");
|
||||
assertNotNull(configuration.getName());
|
||||
assertEquals("foo", configuration.getName());
|
||||
configuration.setBeanName("bar");
|
||||
assertEquals("foo", configuration.getName());
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBeanNameWithParentBeanDefinition() throws Exception {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
ConstructorArgumentValues args = new ConstructorArgumentValues();
|
||||
@@ -60,6 +70,7 @@ public class SpringBeanJobTests extends TestCase {
|
||||
context.registerBeanDefinition("parent", new RootBeanDefinition(
|
||||
JobSupport.class, args, null));
|
||||
context.registerBeanDefinition("bean", new ChildBeanDefinition("parent"));
|
||||
context.refresh();
|
||||
JobSupport configuration = (JobSupport) context
|
||||
.getBean("bean");
|
||||
assertNotNull(configuration.getName());
|
||||
@@ -68,5 +79,6 @@ public class SpringBeanJobTests extends TestCase {
|
||||
assertEquals("bar", configuration.getName());
|
||||
configuration.setName("foo");
|
||||
assertEquals("foo", configuration.getName());
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
* Copyright 2012-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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.batch.core.configuration.annotation;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.batch.core.PooledEmbeddedDataSource;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -31,13 +32,13 @@ import org.springframework.util.ClassUtils;
|
||||
|
||||
@Configuration
|
||||
public class DataSourceConfiguration {
|
||||
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
|
||||
@Autowired
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
protected void initialize() {
|
||||
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
|
||||
@@ -45,10 +46,10 @@ public class DataSourceConfiguration {
|
||||
populator.setContinueOnError(true);
|
||||
DatabasePopulatorUtils.execute(populator, dataSource());
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseFactory().getDatabase();
|
||||
return new PooledEmbeddedDataSource(new EmbeddedDatabaseFactory().getDatabase());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class JobBuilderConfigurationTests {
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.core.JobParameters;
|
||||
import org.springframework.batch.core.JobParametersBuilder;
|
||||
import org.springframework.batch.core.PooledEmbeddedDataSource;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
|
||||
public class JsrJobParametersConverterTests {
|
||||
@@ -24,10 +25,10 @@ public class JsrJobParametersConverterTests {
|
||||
|
||||
@BeforeClass
|
||||
public static void setupDatabase() {
|
||||
dataSource = new EmbeddedDatabaseBuilder().
|
||||
dataSource = new PooledEmbeddedDataSource(new EmbeddedDatabaseBuilder().
|
||||
addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql").
|
||||
addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql").
|
||||
build();
|
||||
build());
|
||||
}
|
||||
|
||||
@Before
|
||||
|
||||
@@ -23,6 +23,7 @@ import javax.sql.DataSource;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.core.PooledEmbeddedDataSource;
|
||||
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
|
||||
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
|
||||
import org.springframework.batch.core.configuration.xml.DummyItemProcessor;
|
||||
@@ -101,10 +102,10 @@ public class BatchParserTests {
|
||||
|
||||
@Bean
|
||||
DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder().
|
||||
return new PooledEmbeddedDataSource(new EmbeddedDatabaseBuilder().
|
||||
addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql").
|
||||
addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql").
|
||||
build();
|
||||
build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,22 @@ public class JdbcExecutionContextDaoTests extends AbstractExecutionContextDaoTes
|
||||
|
||||
@Override
|
||||
protected JobInstanceDao getJobInstanceDao() {
|
||||
return (JobInstanceDao) applicationContext.getBean("jobInstanceDao", JobInstanceDao.class);
|
||||
return applicationContext.getBean("jobInstanceDao", JobInstanceDao.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JobExecutionDao getJobExecutionDao() {
|
||||
return (JobExecutionDao) applicationContext.getBean("jobExecutionDao", JdbcJobExecutionDao.class);
|
||||
return applicationContext.getBean("jobExecutionDao", JdbcJobExecutionDao.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StepExecutionDao getStepExecutionDao() {
|
||||
return (StepExecutionDao) applicationContext.getBean("stepExecutionDao", StepExecutionDao.class);
|
||||
return applicationContext.getBean("stepExecutionDao", StepExecutionDao.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExecutionContextDao getExecutionContextDao() {
|
||||
return (ExecutionContextDao) applicationContext.getBean("executionContextDao", JdbcExecutionContextDao.class);
|
||||
return applicationContext.getBean("executionContextDao", JdbcExecutionContextDao.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package org.springframework.batch.core.repository.dao;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.batch.core.ExitStatus;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -30,6 +31,15 @@ public class XStreamExecutionContextStringSerializerTests {
|
||||
serializer = serializerDeserializer;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializePropertiesArrayContainer() throws Exception {
|
||||
PropertiesArrayContainer container = new PropertiesArrayContainer();
|
||||
container.setProps(new Properties[3]);
|
||||
|
||||
PropertiesArrayContainer result = (PropertiesArrayContainer) serializationRoundTrip(container);
|
||||
assertEquals(3, result.getProps().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeAMap() throws Exception {
|
||||
Map<String, Object> m1 = new HashMap<String, Object>();
|
||||
@@ -39,7 +49,7 @@ public class XStreamExecutionContextStringSerializerTests {
|
||||
m1.put("object3", new Date(123456790123L));
|
||||
m1.put("object4", new Double(1234567.1234D));
|
||||
|
||||
Map<String, Object> m2 = serializationRoundTrip(m1);
|
||||
Map<String, Object> m2 = (Map<String, Object>) serializationRoundTrip(m1);
|
||||
|
||||
compareContexts(m1, m2);
|
||||
}
|
||||
@@ -61,7 +71,7 @@ public class XStreamExecutionContextStringSerializerTests {
|
||||
o1.setObj(o2);
|
||||
m1.put("co", o1);
|
||||
|
||||
Map<String, Object> m2 = serializationRoundTrip(m1);
|
||||
Map<String, Object> m2 = (Map<String, Object>) serializationRoundTrip(m1);
|
||||
|
||||
compareContexts(m1, m2);
|
||||
}
|
||||
@@ -80,14 +90,14 @@ public class XStreamExecutionContextStringSerializerTests {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> serializationRoundTrip(Map<String, Object> m1) throws IOException {
|
||||
private Object serializationRoundTrip(Object m1) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
serializer.serialize(m1, out);
|
||||
|
||||
String s = out.toString();
|
||||
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(s.getBytes());
|
||||
Map<String, Object> m2 = (Map<String, Object>) serializer.deserialize(in);
|
||||
Object m2 = serializer.deserialize(in);
|
||||
return m2;
|
||||
}
|
||||
|
||||
@@ -133,15 +143,27 @@ public class XStreamExecutionContextStringSerializerTests {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ComplexObject that = (ComplexObject) o;
|
||||
|
||||
if (map != null ? !map.equals(that.map) : that.map != null) return false;
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) return false;
|
||||
if (number != null ? !number.equals(that.number) : that.number != null) return false;
|
||||
if (obj != null ? !obj.equals(that.obj) : that.obj != null) return false;
|
||||
if (map != null ? !map.equals(that.map) : that.map != null) {
|
||||
return false;
|
||||
}
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) {
|
||||
return false;
|
||||
}
|
||||
if (number != null ? !number.equals(that.number) : that.number != null) {
|
||||
return false;
|
||||
}
|
||||
if (obj != null ? !obj.equals(that.obj) : that.obj != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -160,6 +182,17 @@ public class XStreamExecutionContextStringSerializerTests {
|
||||
public String toString() {
|
||||
return "ComplexObject [name=" + name + ", number=" + number + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static class PropertiesArrayContainer {
|
||||
private Properties[] props;
|
||||
|
||||
public Properties[] getProps() {
|
||||
return props;
|
||||
}
|
||||
|
||||
public void setProps(Properties[] props) {
|
||||
this.props = props;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Repository tests using JDBC DAOs (rather than mocks).
|
||||
*
|
||||
*
|
||||
* @author Robert Kasanicky
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -186,4 +186,4 @@ public class SimpleJobRepositoryIntegrationTests {
|
||||
assertEquals(jobExecution, jobRepository.getLastJobExecution(job.getName(), jobParameters));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.batch.core.ItemWriteListener;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobParameters;
|
||||
import org.springframework.batch.core.PooledEmbeddedDataSource;
|
||||
import org.springframework.batch.core.SkipListener;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.StepExecution;
|
||||
@@ -184,11 +185,11 @@ public class RegisterMultiListenerTests {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource(){
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
return new PooledEmbeddedDataSource(new EmbeddedDatabaseBuilder()
|
||||
.addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql")
|
||||
.addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql")
|
||||
.setType(EmbeddedDatabaseType.HSQL)
|
||||
.build();
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,11 +215,11 @@ public class RegisterMultiListenerTests {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource(){
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
return new PooledEmbeddedDataSource(new EmbeddedDatabaseBuilder()
|
||||
.addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql")
|
||||
.addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql")
|
||||
.setType(EmbeddedDatabaseType.HSQL)
|
||||
.build();
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(locations = "/org/springframework/batch/core/repository/dao/sql-dao-test.xml")
|
||||
@@ -138,12 +138,12 @@ public class AsyncChunkOrientedStepIntegrationTests {
|
||||
assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus());
|
||||
// Need a transaction so one connection is enough to get job execution and its parameters
|
||||
StepExecution lastStepExecution = new TransactionTemplate(transactionManager)
|
||||
.execute(new TransactionCallback<StepExecution>() {
|
||||
@Override
|
||||
public StepExecution doInTransaction(TransactionStatus status) {
|
||||
return jobRepository.getLastStepExecution(jobExecution.getJobInstance(), step.getName());
|
||||
}
|
||||
});
|
||||
.execute(new TransactionCallback<StepExecution>() {
|
||||
@Override
|
||||
public StepExecution doInTransaction(TransactionStatus status) {
|
||||
return jobRepository.getLastStepExecution(jobExecution.getJobInstance(), step.getName());
|
||||
}
|
||||
});
|
||||
assertEquals(lastStepExecution, stepExecution);
|
||||
assertFalse(lastStepExecution == stepExecution);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user