Support getNextJobParameters from JobParametersBuilder.
resolves BATCH-2640 Cleanup and added tests to JobParametersBuilder.
This commit is contained in:
committed by
Michael Minella
parent
96a317c52f
commit
a35784dbd6
@@ -16,10 +16,13 @@
|
||||
|
||||
package org.springframework.batch.core;
|
||||
|
||||
import org.springframework.batch.core.explore.JobExplorer;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
@@ -36,20 +39,21 @@ import java.util.Properties;
|
||||
*
|
||||
* @author Lucas Ward
|
||||
* @author Michael Minella
|
||||
* @author Glenn Renfro
|
||||
* @since 1.0
|
||||
* @see JobParameters
|
||||
* @see JobParameter
|
||||
*/
|
||||
public class JobParametersBuilder {
|
||||
|
||||
private final Map<String, JobParameter> parameterMap;
|
||||
private Map<String, JobParameter> parameterMap;
|
||||
|
||||
/**
|
||||
* Default constructor. Initializes the builder with empty parameters.
|
||||
*/
|
||||
public JobParametersBuilder() {
|
||||
|
||||
this.parameterMap = new LinkedHashMap<String, JobParameter>();
|
||||
this.parameterMap = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +61,7 @@ public class JobParametersBuilder {
|
||||
* @param jobParameters {@link JobParameters} instance used to initialize the builder.
|
||||
*/
|
||||
public JobParametersBuilder(JobParameters jobParameters) {
|
||||
this.parameterMap = new LinkedHashMap<String, JobParameter>(jobParameters.getParameters());
|
||||
this.parameterMap = new LinkedHashMap<>(jobParameters.getParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +71,7 @@ public class JobParametersBuilder {
|
||||
* @param properties the job parameters to be used
|
||||
*/
|
||||
public JobParametersBuilder(Properties properties) {
|
||||
this.parameterMap = new LinkedHashMap<String, JobParameter>();
|
||||
this.parameterMap = new LinkedHashMap<>();
|
||||
|
||||
if(properties != null) {
|
||||
for (Map.Entry<Object, Object> curProperty : properties.entrySet()) {
|
||||
@@ -84,7 +88,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addString(String key, String parameter) {
|
||||
parameterMap.put(key, new JobParameter(parameter, true));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -97,7 +101,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addString(String key, String parameter, boolean identifying) {
|
||||
parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -109,7 +113,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addDate(String key, Date parameter) {
|
||||
parameterMap.put(key, new JobParameter(parameter, true));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -122,7 +126,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addDate(String key, Date parameter, boolean identifying) {
|
||||
parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -134,7 +138,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addLong(String key, Long parameter) {
|
||||
parameterMap.put(key, new JobParameter(parameter, true));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -147,7 +151,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addLong(String key, Long parameter, boolean identifying) {
|
||||
parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -159,7 +163,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addDouble(String key, Double parameter) {
|
||||
parameterMap.put(key, new JobParameter(parameter, true));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -172,7 +176,7 @@ public class JobParametersBuilder {
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public JobParametersBuilder addDouble(String key, Double parameter, boolean identifying) {
|
||||
parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
this.parameterMap.put(key, new JobParameter(parameter, identifying));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -183,7 +187,7 @@ public class JobParametersBuilder {
|
||||
* @return a valid {@link JobParameters} object.
|
||||
*/
|
||||
public JobParameters toJobParameters() {
|
||||
return new JobParameters(parameterMap);
|
||||
return new JobParameters(this.parameterMap);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +199,75 @@ public class JobParametersBuilder {
|
||||
*/
|
||||
public JobParametersBuilder addParameter(String key, JobParameter jobParameter) {
|
||||
Assert.notNull(jobParameter, "JobParameter must not be null");
|
||||
parameterMap.put(key, jobParameter);
|
||||
this.parameterMap.put(key, jobParameter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the {@link JobParameters} based on the state of the {@link Job}. This
|
||||
* should be called after all parameters have been entered into the builder.
|
||||
*
|
||||
* @param job the job for which the {@link JobParameters} are being constructed.
|
||||
* @param jobExplorer instance to a {@link JobExplorer}
|
||||
* @return a reference to this object.
|
||||
*
|
||||
* @since 4.0
|
||||
*/
|
||||
public JobParametersBuilder getNextJobParameters(Job job, JobExplorer jobExplorer) {
|
||||
String name = job.getName();
|
||||
JobParameters nextParameters = new JobParameters();
|
||||
List<JobInstance> lastInstances = jobExplorer.getJobInstances(name, 0, 1);
|
||||
JobParametersIncrementer incrementer = job.getJobParametersIncrementer();
|
||||
if (lastInstances.isEmpty()) {
|
||||
// Start from a completely clean sheet
|
||||
if (incrementer != null) {
|
||||
nextParameters = incrementer.getNext(new JobParameters());
|
||||
}
|
||||
}
|
||||
else {
|
||||
List<JobExecution> previousExecutions = jobExplorer
|
||||
.getJobExecutions(lastInstances.get(0));
|
||||
JobExecution previousExecution = previousExecutions.get(0);
|
||||
if (previousExecution == null) {
|
||||
// Normally this will not happen - an instance exists with no executions
|
||||
if (incrementer != null) {
|
||||
nextParameters = incrementer.getNext(new JobParameters());
|
||||
}
|
||||
}
|
||||
else if (isStoppedOrFailed(previousExecution) && job.isRestartable()) {
|
||||
// Retry a failed or stopped execution
|
||||
nextParameters = previousExecution.getJobParameters();
|
||||
// Non-identifying additional parameters can be removed to a retry
|
||||
removeNonIdentifying(this.parameterMap);
|
||||
}
|
||||
else if (incrementer != null) {
|
||||
// New instance so increment the parameters if we can
|
||||
nextParameters = incrementer.getNext(previousExecution.getJobParameters());
|
||||
}
|
||||
}
|
||||
this.parameterMap = merge(nextParameters, this.parameterMap);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void removeNonIdentifying(Map<String, JobParameter> parameters) {
|
||||
HashMap<String, JobParameter> copy = new HashMap<>(parameters);
|
||||
for (Map.Entry<String, JobParameter> parameter : copy.entrySet()) {
|
||||
if (!parameter.getValue().isIdentifying()) {
|
||||
parameters.remove(parameter.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LinkedHashMap<String, JobParameter> merge(JobParameters parameters,
|
||||
Map<String, JobParameter> additionals) {
|
||||
Map<String, JobParameter> merged = new HashMap<>();
|
||||
merged.putAll(parameters.getParameters());
|
||||
merged.putAll(additionals);
|
||||
return new LinkedHashMap<>(merged);
|
||||
}
|
||||
|
||||
private boolean isStoppedOrFailed(JobExecution execution) {
|
||||
BatchStatus status = execution.getStatus();
|
||||
return (status == BatchStatus.STOPPED || status == BatchStatus.FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,76 +15,114 @@
|
||||
*/
|
||||
package org.springframework.batch.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.batch.core.explore.JobExplorer;
|
||||
import org.springframework.batch.core.job.SimpleJob;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Lucas Ward
|
||||
* @author Michael Minella
|
||||
* @author Glenn Renfro
|
||||
*
|
||||
*/
|
||||
public class JobParametersBuilderTests {
|
||||
|
||||
JobParametersBuilder parametersBuilder = new JobParametersBuilder();
|
||||
private JobParametersBuilder parametersBuilder;
|
||||
|
||||
Date date = new Date(System.currentTimeMillis());
|
||||
private SimpleJob job;
|
||||
|
||||
private JobExplorer jobExplorer;
|
||||
|
||||
private List<JobInstance> jobInstanceList;
|
||||
|
||||
private List<JobExecution> jobExecutionList;
|
||||
|
||||
private Date date = new Date(System.currentTimeMillis());
|
||||
|
||||
@Before
|
||||
public void initialize() {
|
||||
this.parametersBuilder = new JobParametersBuilder();
|
||||
this.job = new SimpleJob("simpleJob");
|
||||
this.jobExplorer = mock(JobExplorer.class);
|
||||
this.jobInstanceList = new ArrayList<>(1);
|
||||
this.jobExecutionList = new ArrayList<>(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonIdentifyingParameters() {
|
||||
parametersBuilder.addDate("SCHEDULE_DATE", date, false);
|
||||
parametersBuilder.addLong("LONG", new Long(1), false);
|
||||
parametersBuilder.addString("STRING", "string value", false);
|
||||
JobParameters parameters = parametersBuilder.toJobParameters();
|
||||
this.parametersBuilder.addDate("SCHEDULE_DATE", date, false);
|
||||
this.parametersBuilder.addLong("LONG", new Long(1), false);
|
||||
this.parametersBuilder.addString("STRING", "string value", false);
|
||||
this.parametersBuilder.addDouble("DOUBLE", new Double(1), false);
|
||||
|
||||
JobParameters parameters = this.parametersBuilder.toJobParameters();
|
||||
assertEquals(date, parameters.getDate("SCHEDULE_DATE"));
|
||||
assertEquals(1L, parameters.getLong("LONG").longValue());
|
||||
assertEquals("string value", parameters.getString("STRING"));
|
||||
assertEquals(1, parameters.getDouble("DOUBLE").doubleValue(), 1e-15);
|
||||
assertFalse(parameters.getParameters().get("SCHEDULE_DATE").isIdentifying());
|
||||
assertFalse(parameters.getParameters().get("LONG").isIdentifying());
|
||||
assertFalse(parameters.getParameters().get("STRING").isIdentifying());
|
||||
assertFalse(parameters.getParameters().get("DOUBLE").isIdentifying());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToJobRuntimeParamters(){
|
||||
parametersBuilder.addDate("SCHEDULE_DATE", date);
|
||||
parametersBuilder.addLong("LONG", new Long(1));
|
||||
parametersBuilder.addString("STRING", "string value");
|
||||
JobParameters parameters = parametersBuilder.toJobParameters();
|
||||
public void testToJobRuntimeParameters(){
|
||||
this.parametersBuilder.addDate("SCHEDULE_DATE", date);
|
||||
this.parametersBuilder.addLong("LONG", new Long(1));
|
||||
this.parametersBuilder.addString("STRING", "string value");
|
||||
this.parametersBuilder.addDouble("DOUBLE", new Double(1));
|
||||
JobParameters parameters = this.parametersBuilder.toJobParameters();
|
||||
assertEquals(date, parameters.getDate("SCHEDULE_DATE"));
|
||||
assertEquals(1L, parameters.getLong("LONG").longValue());
|
||||
assertEquals(1, parameters.getDouble("DOUBLE").doubleValue(), 1e-15);
|
||||
assertEquals("string value", parameters.getString("STRING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullRuntimeParamters(){
|
||||
parametersBuilder.addDate("SCHEDULE_DATE", null);
|
||||
parametersBuilder.addLong("LONG", null);
|
||||
parametersBuilder.addString("STRING", null);
|
||||
JobParameters parameters = parametersBuilder.toJobParameters();
|
||||
public void testNullRuntimeParameters(){
|
||||
this.parametersBuilder.addDate("SCHEDULE_DATE", null);
|
||||
this.parametersBuilder.addLong("LONG", null);
|
||||
this.parametersBuilder.addString("STRING", null);
|
||||
this.parametersBuilder.addDouble("DOUBLE", null);
|
||||
|
||||
JobParameters parameters = this.parametersBuilder.toJobParameters();
|
||||
assertEquals(null, parameters.getDate("SCHEDULE_DATE"));
|
||||
assertEquals(0L, parameters.getLong("LONG").longValue());
|
||||
assertEquals(null, parameters.getString("STRING"));
|
||||
assertEquals(0, parameters.getLong("DOUBLE").doubleValue(), 1e-15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopy(){
|
||||
parametersBuilder.addString("STRING", "string value");
|
||||
parametersBuilder = new JobParametersBuilder(parametersBuilder.toJobParameters());
|
||||
Iterator<String> parameters = parametersBuilder.toJobParameters().getParameters().keySet().iterator();
|
||||
this.parametersBuilder.addString("STRING", "string value");
|
||||
this.parametersBuilder = new JobParametersBuilder(this.parametersBuilder.toJobParameters());
|
||||
Iterator<String> parameters = this.parametersBuilder.toJobParameters().getParameters().keySet().iterator();
|
||||
assertEquals("STRING", parameters.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrderedTypes(){
|
||||
parametersBuilder.addDate("SCHEDULE_DATE", date);
|
||||
parametersBuilder.addLong("LONG", new Long(1));
|
||||
parametersBuilder.addString("STRING", "string value");
|
||||
Iterator<String> parameters = parametersBuilder.toJobParameters().getParameters().keySet().iterator();
|
||||
this.parametersBuilder.addDate("SCHEDULE_DATE", date);
|
||||
this.parametersBuilder.addLong("LONG", new Long(1));
|
||||
this.parametersBuilder.addString("STRING", "string value");
|
||||
Iterator<String> parameters = this.parametersBuilder.toJobParameters().getParameters().keySet().iterator();
|
||||
assertEquals("SCHEDULE_DATE", parameters.next());
|
||||
assertEquals("LONG", parameters.next());
|
||||
assertEquals("STRING", parameters.next());
|
||||
@@ -92,10 +130,10 @@ public class JobParametersBuilderTests {
|
||||
|
||||
@Test
|
||||
public void testOrderedStrings(){
|
||||
parametersBuilder.addString("foo", "value foo");
|
||||
parametersBuilder.addString("bar", "value bar");
|
||||
parametersBuilder.addString("spam", "value spam");
|
||||
Iterator<String> parameters = parametersBuilder.toJobParameters().getParameters().keySet().iterator();
|
||||
this.parametersBuilder.addString("foo", "value foo");
|
||||
this.parametersBuilder.addString("bar", "value bar");
|
||||
this.parametersBuilder.addString("spam", "value spam");
|
||||
Iterator<String> parameters = this.parametersBuilder.toJobParameters().getParameters().keySet().iterator();
|
||||
assertEquals("foo", parameters.next());
|
||||
assertEquals("bar", parameters.next());
|
||||
assertEquals("spam", parameters.next());
|
||||
@@ -104,9 +142,105 @@ public class JobParametersBuilderTests {
|
||||
@Test
|
||||
public void testAddJobParameter(){
|
||||
JobParameter jobParameter = new JobParameter("bar");
|
||||
parametersBuilder.addParameter("foo", jobParameter);
|
||||
Map<String,JobParameter> parameters = parametersBuilder.toJobParameters().getParameters();
|
||||
this.parametersBuilder.addParameter("foo", jobParameter);
|
||||
Map<String,JobParameter> parameters = this.parametersBuilder.toJobParameters().getParameters();
|
||||
assertEquals(1, parameters.size());
|
||||
assertEquals("bar", parameters.get("foo").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPropreties() {
|
||||
Properties props = new Properties();
|
||||
props.setProperty("SCHEDULE_DATE", "A DATE");
|
||||
props.setProperty("LONG", "1");
|
||||
props.setProperty("STRING", "string value");
|
||||
this.parametersBuilder = new JobParametersBuilder(props);
|
||||
JobParameters parameters = this.parametersBuilder.toJobParameters();
|
||||
assertEquals("A DATE", parameters.getString("SCHEDULE_DATE"));
|
||||
assertEquals("1", parameters.getString("LONG"));
|
||||
assertEquals("string value", parameters.getString("STRING"));
|
||||
assertFalse(parameters.getParameters().get("SCHEDULE_DATE").isIdentifying());
|
||||
assertFalse(parameters.getParameters().get("LONG").isIdentifying());
|
||||
assertFalse(parameters.getParameters().get("STRING").isIdentifying());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetNextJobParametersFirstRun(){
|
||||
job.setJobParametersIncrementer(new RunIdIncrementer());
|
||||
initializeForNextJobParameters();
|
||||
this.parametersBuilder.getNextJobParameters(this.job, this.jobExplorer);
|
||||
defaultNextJobParametersVerify(this.parametersBuilder.toJobParameters(), 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextJobParametersNoIncrementer(){
|
||||
initializeForNextJobParameters();
|
||||
this.parametersBuilder.getNextJobParameters(this.job, this.jobExplorer);
|
||||
baseJobParametersVerify(this.parametersBuilder.toJobParameters(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextJobParameters(){
|
||||
this.job.setJobParametersIncrementer(new RunIdIncrementer());
|
||||
this.jobInstanceList.add(new JobInstance(1L, "simpleJobInstance"));
|
||||
this.jobExecutionList.add(getJobExecution(this.jobInstanceList.get(0), null));
|
||||
when(this.jobExplorer.getJobInstances("simpleJob",0,1)).thenReturn(this.jobInstanceList);
|
||||
when(this.jobExplorer.getJobExecutions(any())).thenReturn(this.jobExecutionList);
|
||||
initializeForNextJobParameters();
|
||||
this.parametersBuilder.getNextJobParameters(this.job, this.jobExplorer);
|
||||
defaultNextJobParametersVerify(this.parametersBuilder.toJobParameters(), 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextJobParametersRestartable(){
|
||||
this.job.setRestartable(true);
|
||||
this.job.setJobParametersIncrementer(new RunIdIncrementer());
|
||||
this.jobInstanceList.add(new JobInstance(1L, "simpleJobInstance"));
|
||||
this.jobExecutionList.add(getJobExecution(this.jobInstanceList.get(0), BatchStatus.FAILED));
|
||||
when(this.jobExplorer.getJobInstances("simpleJob",0,1)).thenReturn(this.jobInstanceList);
|
||||
when(this.jobExplorer.getJobExecutions(any())).thenReturn(this.jobExecutionList);
|
||||
initializeForNextJobParameters();
|
||||
this.parametersBuilder.addLong("NON_IDENTIFYING_LONG", new Long(1), false);
|
||||
this.parametersBuilder.getNextJobParameters(this.job, this.jobExplorer);
|
||||
baseJobParametersVerify(this.parametersBuilder.toJobParameters(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextJobParametersNoPreviousExecution(){
|
||||
this.job.setJobParametersIncrementer(new RunIdIncrementer());
|
||||
this.jobInstanceList.add(new JobInstance(1L, "simpleJobInstance"));
|
||||
this.jobExecutionList.add(null);
|
||||
when(this.jobExplorer.getJobInstances("simpleJob",0,1)).thenReturn(this.jobInstanceList);
|
||||
when(this.jobExplorer.getJobExecutions(any())).thenReturn(this.jobExecutionList);
|
||||
initializeForNextJobParameters();
|
||||
this.parametersBuilder.getNextJobParameters(this.job, this.jobExplorer);
|
||||
baseJobParametersVerify(this.parametersBuilder.toJobParameters(), 4);
|
||||
}
|
||||
|
||||
private void initializeForNextJobParameters() {
|
||||
this.parametersBuilder.addDate("SCHEDULE_DATE", date);
|
||||
this.parametersBuilder.addLong("LONG", new Long(1));
|
||||
this.parametersBuilder.addString("STRING", "string value");
|
||||
}
|
||||
|
||||
private void defaultNextJobParametersVerify(JobParameters parameters, int paramCount) {
|
||||
baseJobParametersVerify(parameters, paramCount);
|
||||
assertEquals("1", parameters.getString("run.id"));
|
||||
}
|
||||
private void baseJobParametersVerify(JobParameters parameters, int paramCount) {
|
||||
assertEquals(date, parameters.getDate("SCHEDULE_DATE"));
|
||||
assertEquals(1L, parameters.getLong("LONG").longValue());
|
||||
assertEquals("string value", parameters.getString("STRING"));
|
||||
assertEquals(paramCount, parameters.getParameters().size());
|
||||
}
|
||||
|
||||
private JobExecution getJobExecution(JobInstance jobInstance, BatchStatus batchStatus) {
|
||||
JobExecution jobExecution = new JobExecution(jobInstance, 1L, null, "TestConfig");
|
||||
if(batchStatus != null) {
|
||||
jobExecution.setStatus(batchStatus);
|
||||
}
|
||||
return jobExecution;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user