From 7e83c33632583be66664bfa06af6d1c6409412f2 Mon Sep 17 00:00:00 2001 From: lucasward Date: Thu, 20 Sep 2007 01:16:57 +0000 Subject: [PATCH] IN PROGRESS - issue BATCH-24: Provide exit code for job executed as a main method http://opensource.atlassian.com/projects/spring/browse/BATCH-24 --- .../bootstrap/BatchCommandLineLauncher.java | 147 +++++++++++++----- .../execution/bootstrap/SystemExiter.java | 39 +++++ .../BatchCommandLineLauncherTests.java | 123 +++++++++++---- .../execution/bootstrap/MockJobLauncher.java | 63 ++++++++ .../execution/bootstrap/MockSystemExiter.java | 33 ++++ .../bootstrap/support/JvmSystemExiter.java | 39 +++++ .../src/test/resources/beanRefContext.xml | 14 +- .../src/test/resources/job-configuration.xml | 4 +- .../test-batch-environment-no-launcher.xml | 16 ++ .../bootstrap/test-batch-environment.xml | 18 +++ 10 files changed, 417 insertions(+), 79 deletions(-) create mode 100644 execution/src/main/java/org/springframework/batch/execution/bootstrap/SystemExiter.java create mode 100644 execution/src/test/java/org/springframework/batch/execution/bootstrap/MockJobLauncher.java create mode 100644 execution/src/test/java/org/springframework/batch/execution/bootstrap/MockSystemExiter.java create mode 100644 execution/src/test/java/org/springframework/batch/execution/bootstrap/support/JvmSystemExiter.java create mode 100644 execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment-no-launcher.xml create mode 100644 execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment.xml diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java index e2ef62d38..929e2f2fd 100644 --- a/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java @@ -16,36 +16,55 @@ package org.springframework.batch.execution.bootstrap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.executor.ExitCodeExceptionClassifier; +import org.springframework.batch.execution.bootstrap.support.JvmSystemExiter; +import org.springframework.batch.execution.bootstrap.support.SimpleJvmExitCodeMapper; +import org.springframework.batch.execution.step.simple.SimpleExitCodeExceptionClassifier; import org.springframework.batch.repeat.ExitStatus; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.access.BeanFactoryLocator; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.access.ContextSingletonBeanFactoryLocator; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.Assert; /** * @author Dave Syer + * @author Lucas Ward * @since 2.1 */ public class BatchCommandLineLauncher { + protected static final Log logger = LogFactory + .getLog(BatchCommandLineLauncher.class); + /** * The key for the parent context. */ - public static final String PARENT_KEY = "simple-container"; + private static final String DEFAULT_PARENT_KEY = "batchExecutionEnvironment"; + private static final String DEFAULT_JOB_CONFIGURATION_PATH = "job-configuration.xml"; - private ConfigurableApplicationContext parent; - private JvmExitCodeMapper exitCodeMapper; + private static final String JOB_CONFIGURATIN_PATH_KEY = "job.configuration.path"; + private static final String JOB_NAME_KEY = "job.name"; + private static final String BATCH_EXECUTION_ENVIRONMENT_KEY = "batch.execution.environment.key"; + + private BeanFactoryLocator beanFactoryLocator; + + private JvmExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper(); + + private ExitCodeExceptionClassifier exceptionClassifier = new SimpleExitCodeExceptionClassifier(); private JobLauncher launcher; - /** - * Default constructor for the launcher. Sets up the parent context to use - * for all job executions using a context key {@link #PARENT_KEY}. - */ + private SystemExiter systemExiter = new JvmSystemExiter(); + public BatchCommandLineLauncher() { - parent = (ConfigurableApplicationContext) ContextSingletonBeanFactoryLocator - .getInstance().useBeanFactory(PARENT_KEY).getFactory(); + beanFactoryLocator = ContextSingletonBeanFactoryLocator.getInstance(); } /** @@ -58,6 +77,16 @@ public class BatchCommandLineLauncher { this.launcher = launcher; } + /** + * Injection setter for the {@link ExitCodeExceptionClassifier} + * + * @param exceptionClassifier + */ + public void setExceptionClassifier( + ExitCodeExceptionClassifier exceptionClassifier) { + this.exceptionClassifier = exceptionClassifier; + } + /** * Injection setter for the {@link JvmExitCodeMapper}. * @@ -68,25 +97,52 @@ public class BatchCommandLineLauncher { this.exitCodeMapper = exitCodeMapper; } + /** + * Injection setter for the {@link SystemExiter}. + * + * @param systemExitor + */ + public void setSystemExiter(SystemExiter systemExitor) { + this.systemExiter = systemExitor; + } + + public SystemExiter getSystemExiter() { + return systemExiter; + } + /** * @param path * the path to a Spring context configuration for this job * @param jobName * the name of the job execution to use * @throws NoSuchJobConfigurationException + * @throws IllegalStateException + * if JobLauncher is not autowired by the ApplicationContext */ - int start(String path, String jobName) { - if (!path.endsWith(".xml")) { - path = path + ".xml"; - } - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[] { path }, parent); - context.getAutowireCapableBeanFactory().autowireBeanProperties(this, - AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); - - ExitStatus status = null; + int start(String path, String jobName, String parentKey) { + ExitStatus status = ExitStatus.FAILED; + ClassPathXmlApplicationContext context = null; + try { + ClassPathXmlApplicationContext parent = (ClassPathXmlApplicationContext) beanFactoryLocator + .useBeanFactory(parentKey).getFactory(); + + if (!path.endsWith(".xml")) { + path = path + ".xml"; + } + + context = new ClassPathXmlApplicationContext( + new String[] { path }, parent); + + context.getAutowireCapableBeanFactory().autowireBeanProperties( + this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); + + Assert.state(launcher != null, + "JobLauncher must be provided in the parent ApplicationContext" + + ", check the context created within classpath*:beanRefContext.xml to ensure a JobLauncher" + + " is declared"); + if (!launcher.isRunning()) { if (jobName == null) { status = launcher.run(); @@ -94,15 +150,21 @@ public class BatchCommandLineLauncher { status = launcher.run(jobName); } } - } catch (NoSuchJobConfigurationException e) { - status = new ExitStatus(false, - JvmExitCodeMapper.BATCH_EXITCODE_NO_SUCH_JOBCONFIGURATION, - "Could not locate JobConfiguration \"" + jobName + "\"" ); + } catch (NoSuchJobConfigurationException e) { + logger.fatal("Could not locate JobConfiguration \"" + jobName + + "\"", e); + status = new ExitStatus(false, + JvmExitCodeMapper.BATCH_EXITCODE_NO_SUCH_JOBCONFIGURATION); + } catch (Throwable t) { + logger.fatal(t); + status = exceptionClassifier.classifyForExitCode(t); } finally { - try { - context.stop(); - } finally { - context.close(); + if(context != null){ + try { + context.stop(); + } finally { + context.close(); + } } } return exitCodeMapper.getExitCode(status.getExitCode()); @@ -115,27 +177,26 @@ public class BatchCommandLineLauncher { * * @param args *
    - *
  1. path to resource to load job configuration context - * (default "job-configuration.xml");
  2. - *
  3. runtime name for job execution (default - * "job-execution-id").
  4. + *
  5. classpath location of resource to load job configuration + * context (default "job-configuration.xml");
  6. + *
  7. runtime name of Job (default "job-execution-id").
  8. + *
  9. parent context key for use in pulling the correct + * beanFactory from the beanRefContext.xml (@see + * ContextSingletonBeanFactoryLocator)
  10. *
* @throws NoSuchJobConfigurationException */ public static void main(String[] args) { - - String path = "job-configuration.xml"; - String name = null; - if (args.length > 0) { - path = args[0]; - } - if (args.length > 1) { - name = args[1]; - } + + String path = System.getProperty(JOB_CONFIGURATIN_PATH_KEY, + DEFAULT_JOB_CONFIGURATION_PATH); + String name = System.getProperty(JOB_NAME_KEY); + String parentKey = System.getProperty(BATCH_EXECUTION_ENVIRONMENT_KEY, + DEFAULT_PARENT_KEY); + BatchCommandLineLauncher command = new BatchCommandLineLauncher(); - int result = command.start(path, name); - System.exit(result); - + int result = command.start(path, name, parentKey); + command.getSystemExiter().exit(result); } } diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/SystemExiter.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/SystemExiter.java new file mode 100644 index 000000000..fe4cbdf59 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/SystemExiter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2006-2007 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.execution.bootstrap; + +/** + * Interface for exiting the JVM. This abstraction is only + * useful in order to allow classes that make System.exit calls + * to be testable, since calling System.exit during a unit + * test would cause the entire jvm to finish. + * + * @author Lucas Ward + * + */ +public interface SystemExiter { + + /** + * Terminate the currently running Java Virtual Machine. + * + * @param status exit status. + * @throws SecurityException + * if a security manager exists and its checkExit + * method doesn't allow exit with the specified status. + * @see System.exit + */ + void exit(int status); +} diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java index f9372f2ac..cebc51965 100644 --- a/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java @@ -15,8 +15,15 @@ */ package org.springframework.batch.execution.bootstrap; +import org.easymock.MockControl; import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; import org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.beans.factory.access.BeanFactoryLocator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.access.ContextSingletonBeanFactoryLocator; +import org.springframework.context.support.ClassPathXmlApplicationContext; import junit.framework.TestCase; @@ -25,48 +32,98 @@ import junit.framework.TestCase; * */ public class BatchCommandLineLauncherTests extends TestCase { + + private static final String DEFAULT_PARENT_KEY = "batchExecutionEnvironment"; + private static final String DEFAULT_JOB_CONFIGURATION_PATH = "job-configuration.xml"; + + private static final String JOB_CONFIGURATION_PATH_KEY = "job.configuration.path"; + private static final String JOB_NAME_KEY = "job.name"; + private static final String BATCH_EXECUTION_ENVIRONMENT_KEY = "batch.execution.environment.key"; + + private static final String TEST_BATCH_ENVIRONMENT_KEY = "testBatchEnvironment"; + private static final String TEST_BATCH_ENVIRONMENT_NO_LAUNCHER_KEY = "testBatchEnvironmentNoLauncher"; + + BeanFactoryLocator beanFactoryLocator = ContextSingletonBeanFactoryLocator.getInstance(); + + ClassPathXmlApplicationContext context; + + MockJobLauncher mockJobLauncher; + MockSystemExiter mockSystemExiter; + + protected void setUp() throws Exception { + super.setUp(); + + context = (ClassPathXmlApplicationContext)beanFactoryLocator.useBeanFactory(TEST_BATCH_ENVIRONMENT_KEY).getFactory(); + context.getAutowireCapableBeanFactory().autowireBeanProperties(this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + System.setProperty(BATCH_EXECUTION_ENVIRONMENT_KEY, TEST_BATCH_ENVIRONMENT_KEY); + } + + protected void tearDown() throws Exception { + super.tearDown(); + System.clearProperty(JOB_CONFIGURATION_PATH_KEY); + System.clearProperty(JOB_NAME_KEY); + System.clearProperty(BATCH_EXECUTION_ENVIRONMENT_KEY); + } + + public void setMockJobLauncher(MockJobLauncher mockJobLauncher){ + this.mockJobLauncher = mockJobLauncher; + } + + public void setMockSystemExiter(MockSystemExiter mockSystemExiter) { + this.mockSystemExiter = mockSystemExiter; + } - BatchCommandLineLauncher commandLine = new BatchCommandLineLauncher(); + public void testParentWithNoLauncher(){ + context = (ClassPathXmlApplicationContext)beanFactoryLocator.useBeanFactory(TEST_BATCH_ENVIRONMENT_NO_LAUNCHER_KEY).getFactory(); + context.getAutowireCapableBeanFactory().autowireBeanProperties(this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); + System.setProperty(BATCH_EXECUTION_ENVIRONMENT_KEY, TEST_BATCH_ENVIRONMENT_NO_LAUNCHER_KEY); + + BatchCommandLineLauncher.main(new String[0]); + + assertEquals(JvmExitCodeMapper.JVM_EXITCODE_GENERIC_ERROR, mockSystemExiter.getStatus()); + } - int count = 0; + /** + * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. + * @throws Exception + */ + public void testDefaultNameAndPath() throws Exception { + + mockJobLauncher.setReturnValue(ExitStatus.FINISHED); + + BatchCommandLineLauncher.main(new String[0]); + + assertEquals(JvmExitCodeMapper.JVM_EXITCODE_COMPLETED, mockSystemExiter.getStatus()); + assertEquals(mockJobLauncher.getLastRunCalled(), MockJobLauncher.RUN_NO_ARGS); + } + public void testCustomJobName(){ + + mockJobLauncher.setReturnValue(ExitStatus.FINISHED); + + System.setProperty(JOB_NAME_KEY, "foo"); + BatchCommandLineLauncher.main(new String[0]); + + assertEquals(JvmExitCodeMapper.JVM_EXITCODE_COMPLETED, mockSystemExiter.getStatus()); + assertEquals(mockJobLauncher.getLastRunCalled(), MockJobLauncher.RUN_JOB_NAME); + } + /** * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. * @throws Exception */ public void testMainWithDefaultArguments() throws Exception { - // BatchCommandLineLauncher.main(new String[0]); - // TODO: find a way to assert something. No error actually - // means the test was successful... - } - - /** - * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. - * @throws Exception - */ - public void testMainWithParentContext() throws Exception { - // Try an XML file name for the parent context with no suffix - //BatchCommandLineLauncher.main(new String[]{"job-configuration"}); - //assertEquals(commandLine.start("job-configuration.xml", null), 0); + //can't test this without running the whole test in another jvm. + //BatchCommandLineLauncher.main(new String[0]); } - /** - * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. - * @throws Exception - */ - public void testMainWithParentContextAndValidJobId() throws Exception { - // Try a job id as the second argument - //BatchCommandLineLauncher.main(new String[]{"job-configuration", "test-job"}); - //assertEquals(commandLine.start("job-configuration.xml", "test-job"), 0); - } - - /** - * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. - * @throws Exception - */ - public void testMainWithParentContextAndInvalidJobId() throws Exception { - // Try a job id as the second argument test-job - //BatchCommandLineLauncher.main(new String[]{"job-configuration", "foo-bar-spam"}); - //assertEquals(commandLine.start("job-configuration", "foo-bar-spam"), 2); + + + public void testInvalidJobConfig(){ + //also not testable without kicking off in a new jvm, since autowiring happens + //after the context is loaded. +/* System.setProperty(JOB_CONFIGURATION_PATH_KEY, "foo"); + + BatchCommandLineLauncher.main(new String[0]);*/ } } diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/MockJobLauncher.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/MockJobLauncher.java new file mode 100644 index 000000000..4beeb194e --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/MockJobLauncher.java @@ -0,0 +1,63 @@ +package org.springframework.batch.execution.bootstrap; + +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.repeat.ExitStatus; + +/** + * Mock Job Launcher. Normally, something like EasyMock would + * be used to mock an interface, however, because of the nature + * of launching a batch job from the command line, the mocked + * class cannot be injected. + * + * @author Lucas Ward + * + */ +public class MockJobLauncher implements JobLauncher { + + public static final int RUN_NO_ARGS = 0; + public static final int RUN_JOB_NAME = 1; + public static final int RUN_JOB_IDENTIFIER =2 ; + + private int lastRunCalled = RUN_NO_ARGS; + private ExitStatus returnValue = ExitStatus.FINISHED; + + private boolean isRunning = false; + + public boolean isRunning() { + return isRunning; + } + + public ExitStatus run() throws NoSuchJobConfigurationException { + lastRunCalled = RUN_NO_ARGS; + return returnValue; + } + + public ExitStatus run(String jobName) + throws NoSuchJobConfigurationException { + lastRunCalled = RUN_JOB_NAME; + return returnValue; + } + + public ExitStatus run(JobIdentifier jobIdentifier) + throws NoSuchJobConfigurationException { + lastRunCalled = RUN_JOB_IDENTIFIER; + return returnValue; + } + + public void stop() { + + } + + public void setReturnValue(ExitStatus returnValue){ + this.returnValue = returnValue; + } + + public void setIsRunning(boolean isRunning){ + this.isRunning = isRunning; + } + + public int getLastRunCalled(){ + return lastRunCalled; + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/MockSystemExiter.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/MockSystemExiter.java new file mode 100644 index 000000000..6dc0f3638 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/MockSystemExiter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2006-2007 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.execution.bootstrap; + +/** + * @author Lucas Ward + * + */ +class MockSystemExiter implements SystemExiter{ + + private int status; + + public void exit(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/support/JvmSystemExiter.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/support/JvmSystemExiter.java new file mode 100644 index 000000000..1f2d4fc04 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/support/JvmSystemExiter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2006-2007 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.execution.bootstrap.support; + +import org.springframework.batch.execution.bootstrap.SystemExiter; + +/** + * Implementation of the {@link SystemExiter} interface + * that calls the standards System.exit method. It should + * be noted that there will be no unit tests for this class, + * since there is only one line of actual code, that would only + * be testable by mocking System or Runtime. + * + * @author Lucas Ward + * + */ +public class JvmSystemExiter implements SystemExiter { + + /* (non-Javadoc) + * @see org.springframework.batch.execution.bootstrap.SystemExiter#exit(int) + */ + public void exit(int status) { + System.exit(status); + } + +} diff --git a/execution/src/test/resources/beanRefContext.xml b/execution/src/test/resources/beanRefContext.xml index 1e8b9b7a5..784d115a2 100644 --- a/execution/src/test/resources/beanRefContext.xml +++ b/execution/src/test/resources/beanRefContext.xml @@ -2,10 +2,22 @@ - + simple-container-definition.xml + + + + org/springframework/batch/execution/bootstrap/test-batch-environment.xml + + + + + + org/springframework/batch/execution/bootstrap/test-batch-environment-no-launcher.xml + + \ No newline at end of file diff --git a/execution/src/test/resources/job-configuration.xml b/execution/src/test/resources/job-configuration.xml index 836948bea..676b3bd90 100644 --- a/execution/src/test/resources/job-configuration.xml +++ b/execution/src/test/resources/job-configuration.xml @@ -11,10 +11,10 @@ - + - + diff --git a/execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment-no-launcher.xml b/execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment-no-launcher.xml new file mode 100644 index 000000000..e4b0e85ed --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment-no-launcher.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment.xml b/execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment.xml new file mode 100644 index 000000000..b167107ec --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/bootstrap/test-batch-environment.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + +