You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@batchee.apache.org by rm...@apache.org on 2015/12/02 17:39:10 UTC

incubator-batchee git commit: adding StepExecutions command and handling required @Options

Repository: incubator-batchee
Updated Branches:
  refs/heads/master 2d63aa14b -> d24d2ef09


adding StepExecutions command and handling required @Options


Project: http://git-wip-us.apache.org/repos/asf/incubator-batchee/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-batchee/commit/d24d2ef0
Tree: http://git-wip-us.apache.org/repos/asf/incubator-batchee/tree/d24d2ef0
Diff: http://git-wip-us.apache.org/repos/asf/incubator-batchee/diff/d24d2ef0

Branch: refs/heads/master
Commit: d24d2ef09358e9c42bb049e820bf271deccd47c8
Parents: 2d63aa1
Author: Romain Manni-Bucau <rm...@gmail.com>
Authored: Wed Dec 2 17:39:03 2015 +0100
Committer: Romain Manni-Bucau <rm...@gmail.com>
Committed: Wed Dec 2 17:39:03 2015 +0100

----------------------------------------------------------------------
 .../java/org/apache/batchee/cli/BatchEECLI.java | 168 ++++++-------------
 .../batchee/cli/command/CliConfiguration.java   |  30 ----
 .../apache/batchee/cli/command/Executions.java  |  11 +-
 .../batchee/cli/command/JobOperatorCommand.java |  10 +-
 .../batchee/cli/command/StepExecutions.java     |  93 ++++++++++
 .../cli/command/api/CliConfiguration.java       |  29 ++++
 .../internal/DefaultCliConfiguration.java       | 122 ++++++++++++++
 .../java/org/apache/batchee/cli/MainTest.java   |  28 +++-
 8 files changed, 338 insertions(+), 153 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java b/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
index 55f084b..6fb656e 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
@@ -17,19 +17,21 @@
 package org.apache.batchee.cli;
 
 import org.apache.batchee.cli.command.Abandon;
-import org.apache.batchee.cli.command.CliConfiguration;
 import org.apache.batchee.cli.command.Eviction;
 import org.apache.batchee.cli.command.Executions;
-import org.apache.batchee.cli.command.api.Exit;
 import org.apache.batchee.cli.command.Instances;
 import org.apache.batchee.cli.command.Names;
 import org.apache.batchee.cli.command.Restart;
 import org.apache.batchee.cli.command.Running;
 import org.apache.batchee.cli.command.Start;
 import org.apache.batchee.cli.command.Status;
+import org.apache.batchee.cli.command.StepExecutions;
 import org.apache.batchee.cli.command.Stop;
+import org.apache.batchee.cli.command.api.CliConfiguration;
 import org.apache.batchee.cli.command.api.Command;
+import org.apache.batchee.cli.command.api.Exit;
 import org.apache.batchee.cli.command.api.UserCommand;
+import org.apache.batchee.cli.command.internal.DefaultCliConfiguration;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.CommandLineParser;
 import org.apache.commons.cli.DefaultParser;
@@ -38,17 +40,12 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
 import org.apache.commons.cli.ParseException;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -56,68 +53,12 @@ import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.TreeMap;
 
-import static java.lang.ClassLoader.getSystemClassLoader;
 import static java.util.Arrays.asList;
 
 public class BatchEECLI {
     public static void main(final String[] args) {
         final Iterator<CliConfiguration> configuration = ServiceLoader.load(CliConfiguration.class).iterator();
-        final CliConfiguration cliConfiguration = configuration.hasNext() ? configuration.next() : new CliConfiguration() {
-            @Override
-            public String name() {
-                return "batchee";
-            }
-
-            @Override
-            public String description() {
-                return "BatchEE CLI";
-            }
-
-            @Override
-            public boolean addDefaultCommands() {
-                return true;
-            }
-
-            @Override
-            public Iterator<Class<? extends UserCommand>> userCommands() {
-                final Collection<Class<? extends UserCommand>> classes = new ArrayList<Class<? extends UserCommand>>();
-                try { // read manually cause we dont want to instantiate them there, so no ServiceLoader
-                    final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
-                    final ClassLoader loader = tccl != null ? tccl : getSystemClassLoader();
-                    final Enumeration<URL> uc = loader.getResources("META-INF/services/org.apache.batchee.cli.command.UserCommand");
-                    while (uc.hasMoreElements()) {
-                        final URL url = uc.nextElement();
-                        BufferedReader r = null;
-                        try {
-                            r = new BufferedReader(new InputStreamReader(url.openStream()));
-                            String line;
-                            while ((line = r.readLine()) != null) {
-                                if (line.startsWith("#") || line.trim().isEmpty()) {
-                                    continue;
-                                }
-                                classes.add(Class.class.cast(loader.loadClass(line.trim())));
-                            }
-                        } catch (final IOException ioe) {
-                            throw new IllegalStateException(ioe);
-                        } catch (final ClassNotFoundException cnfe) {
-                            throw new IllegalArgumentException(cnfe);
-                        } finally {
-                            if (r != null) {
-                                r.close();
-                            }
-                        }
-                    }
-                } catch (final IOException e) {
-                    throw new IllegalStateException(e);
-                }
-                return classes.iterator();
-            }
-
-            @Override
-            public Runnable decorate(final Runnable task) {
-                return task;
-            }
-        };
+        final CliConfiguration cliConfiguration = configuration.hasNext() ? configuration.next() : new DefaultCliConfiguration();
 
         final Map<String, Class<? extends Runnable>> commands = new TreeMap<String, Class<? extends Runnable>>();
         if (cliConfiguration.addDefaultCommands()) {
@@ -127,7 +68,7 @@ public class BatchEECLI {
                     Status.class, Running.class,
                     Stop.class, Abandon.class,
                     Instances.class, Executions.class,
-                    Eviction.class)) {
+                    StepExecutions.class, Eviction.class)) {
                 addCommand(commands, type);
             }
         }
@@ -174,58 +115,7 @@ public class BatchEECLI {
         final CommandLineParser parser = new DefaultParser();
         try {
             final CommandLine line = parser.parse(options, newArgs.toArray(new String[newArgs.size()]));
-
-            final Runnable commandInstance = cmd.newInstance();
-            if (!newArgs.isEmpty()) { // we have few commands we can execute without args even if we have a bunch of config
-                for (final Map.Entry<String, Field> option : fields.entrySet()) {
-                    final String key = option.getKey();
-                    if (key.isEmpty()) { // arguments, not an option
-                        final List<String> list = line.getArgList();
-                        if (list != null) {
-                            final Field field = option.getValue();
-                            final Type expectedType = field.getGenericType();
-                            if (ParameterizedType.class.isInstance(expectedType)) {
-                                final ParameterizedType pt = ParameterizedType.class.cast(expectedType);
-                                if ((pt.getRawType() == List.class || pt.getRawType() == Collection.class)
-                                    && pt.getActualTypeArguments().length == 1 && pt.getActualTypeArguments()[0] == String.class) {
-                                    field.set(commandInstance, list);
-                                } else {
-                                    throw new IllegalArgumentException("@Arguments only supports List<String>");
-                                }
-                            } else {
-                                throw new IllegalArgumentException("@Arguments only supports List<String>");
-                            }
-                        }
-                    } else {
-                        final String value = line.getOptionValue(key);
-                        if (value != null) {
-                            final Field field = option.getValue();
-                            final Class<?> expectedType = field.getType();
-                            if (String.class == expectedType) {
-                                field.set(commandInstance, value);
-                            } else if (long.class == expectedType) {
-                                field.set(commandInstance, Long.parseLong(value));
-                            } else if (int.class == expectedType) {
-                                field.set(commandInstance, Integer.parseInt(value));
-                            } else if (boolean.class == expectedType) {
-                                field.set(commandInstance, Boolean.parseBoolean(value));
-                            } else if (short.class == expectedType) {
-                                field.set(commandInstance, Short.parseShort(value));
-                            } else if (byte.class == expectedType) {
-                                field.set(commandInstance, Byte.parseByte(value));
-                            } else {
-                                try {
-                                    field.set(commandInstance, expectedType.getMethod("fromString", String.class)
-                                        .invoke(null, value));
-                                } catch (final Exception e) {
-                                    throw new IllegalArgumentException(expectedType + " not supported as option with value '" + value + "'");
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            cliConfiguration.decorate(commandInstance).run();
+            cliConfiguration.decorate(instantiate(cmd, cliConfiguration, fields, !newArgs.isEmpty(), line)).run();
         } catch (final ParseException e) {
             printHelp(command, options);
         } catch (final RuntimeException e) {
@@ -245,6 +135,44 @@ public class BatchEECLI {
         }
     }
 
+    private static Runnable instantiate(final Class<? extends Runnable> cmd,
+                                        final CliConfiguration configuration,
+                                        final Map<String, Field> fields,
+                                        final boolean hasArgs,
+                                        final CommandLine line) throws InstantiationException, IllegalAccessException {
+        final Runnable commandInstance = cmd.newInstance();
+        if (hasArgs) { // we have few commands we can execute without args even if we have a bunch of config
+            for (final Map.Entry<String, Field> option : fields.entrySet()) {
+                final String key = option.getKey();
+                if (key.isEmpty()) { // arguments, not an option
+                    final List<String> list = line.getArgList();
+                    if (list != null) {
+                        final Field field = option.getValue();
+                        final Type expectedType = field.getGenericType();
+                        if (ParameterizedType.class.isInstance(expectedType)) {
+                            final ParameterizedType pt = ParameterizedType.class.cast(expectedType);
+                            if ((pt.getRawType() == List.class || pt.getRawType() == Collection.class)
+                                && pt.getActualTypeArguments().length == 1 && pt.getActualTypeArguments()[0] == String.class) {
+                                field.set(commandInstance, list);
+                            } else {
+                                throw new IllegalArgumentException("@Arguments only supports List<String>");
+                            }
+                        } else {
+                            throw new IllegalArgumentException("@Arguments only supports List<String>");
+                        }
+                    }
+                } else {
+                    final String value = line.getOptionValue(key);
+                    if (value != null) {
+                        final Field field = option.getValue();
+                        field.set(commandInstance, configuration.coerce(value, field.getGenericType()));
+                    }
+                }
+            }
+        }
+        return commandInstance;
+    }
+
     private static void printHelp(final Command command, final Options options) {
         new HelpFormatter().printHelp(command.name(), '\n' + command.description() + "\n\n", options, null, true);
     }
@@ -262,7 +190,11 @@ public class BatchEECLI {
 
                 if (option != null) {
                     final String name = option.name();
-                    options.addOption(Option.builder(name).desc(option.description()).hasArg().build());
+                    final Option.Builder builder = Option.builder(name).desc(option.description()).hasArg();
+                    if (option.required()) {
+                        builder.required();
+                    }
+                    options.addOption(builder.build());
                     fields.put(name, f);
                     f.setAccessible(true);
                 } else if (arguments != null) {

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/command/CliConfiguration.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/CliConfiguration.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/CliConfiguration.java
deleted file mode 100644
index b8a8522..0000000
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/CliConfiguration.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.batchee.cli.command;
-
-import org.apache.batchee.cli.command.api.UserCommand;
-
-import java.util.Iterator;
-
-public interface CliConfiguration {
-    String name();
-    String description();
-    boolean addDefaultCommands();
-    Iterator<Class<? extends UserCommand>> userCommands();
-    Runnable decorate(Runnable task);
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
index dcb4f44..e8ba4c1 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
@@ -21,6 +21,7 @@ import org.apache.batchee.cli.command.api.Option;
 import org.apache.batchee.container.impl.JobInstanceImpl;
 import org.apache.commons.lang3.StringUtils;
 
+import javax.batch.operations.JobOperator;
 import javax.batch.runtime.JobExecution;
 import java.util.List;
 
@@ -29,9 +30,13 @@ public class Executions extends JobOperatorCommand {
     @Option(name = "id", description = "instance id", required = true)
     private long id;
 
+    @Option(name = "showSteps", description = "if steps should be dumped as well")
+    private boolean steps;
+
     @Override
     public void doRun() {
-        final List<JobExecution> executions = operator().getJobExecutions(new JobInstanceImpl(id));
+        final JobOperator operator = operator();
+        final List<JobExecution> executions = operator.getJobExecutions(new JobInstanceImpl(id));
         if (!executions.isEmpty()) {
             info("Executions of " + executions.iterator().next().getJobName() + " for instance " + id);
         }
@@ -43,5 +48,9 @@ public class Executions extends JobOperatorCommand {
                     StringUtils.leftPad(exec.getBatchStatus() != null ? exec.getBatchStatus().toString() : "null", 12),
                     StringUtils.leftPad(exec.getExitStatus(), 11), exec.getStartTime(), exec.getEndTime()));
         }
+
+        if (steps) {
+            new StepExecutions().withOperator(operator).withId(id).run();
+        }
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
index 7357e18..27c9891 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
@@ -113,9 +113,15 @@ public abstract class JobOperatorCommand implements Runnable {
     @Option(name = "addFolderToLoader", description = "force shared lib and libs folders to be added to the classloader")
     private boolean addFolderToLoader = false;
 
+    protected JobOperator operator;
+
     protected JobOperator operator() {
+        if (operator != null) {
+            return operator;
+        }
+
         if (baseUrl == null) {
-            return BatchRuntime.getJobOperator();
+            return operator = BatchRuntime.getJobOperator();
         }
 
         final ClientConfiguration configuration = new ClientConfiguration();
@@ -140,7 +146,7 @@ public abstract class JobOperatorCommand implements Runnable {
         security.setPassword(password);
         security.setType(type);
 
-        return BatchEEJAXRSClientFactory.newClient(configuration);
+        return operator = BatchEEJAXRSClientFactory.newClient(configuration);
     }
 
     protected void info(final String text) {

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/command/StepExecutions.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/StepExecutions.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/StepExecutions.java
new file mode 100644
index 0000000..45a0212
--- /dev/null
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/StepExecutions.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.batchee.cli.command;
+
+import org.apache.batchee.cli.command.api.Command;
+import org.apache.batchee.cli.command.api.Option;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.batch.operations.JobOperator;
+import javax.batch.runtime.Metric;
+import javax.batch.runtime.StepExecution;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Command(name = "stepExecutions", description = "list step executions for a particular execution")
+public class StepExecutions extends JobOperatorCommand {
+    @Option(name = "id", description = "execution id", required = true)
+    private long id;
+
+    public StepExecutions withId(final long id) {
+        this.id = id;
+        return this;
+    }
+    public StepExecutions withOperator(final JobOperator operator) {
+        this.operator = operator;
+        return this;
+    }
+
+    @Override
+    public void doRun() {
+        final JobOperator operator = operator();
+        final List<StepExecution> executions = operator.getStepExecutions(id);
+        if (executions == null || executions.isEmpty()) {
+            info("Executions of " + id + " not found");
+            return;
+        }
+
+        info("Step executions of " + id);
+
+        final List<Metric.MetricType> metricsOrder = new ArrayList<Metric.MetricType>();
+        final StringBuilder metrics = new StringBuilder();
+        for (final Metric.MetricType type : Metric.MetricType.values()) {
+            metrics.append("\t|\t").append(type.name());
+            metricsOrder.add(type);
+        }
+
+        final DateFormat format = new SimpleDateFormat("YYYYMMdd hh:mm:ss");
+        info("   step id\t|\t step name\t|\t    start time   \t|\t     end time    \t|\texit status\t|\tbatch status" + metrics.toString());
+        for (final StepExecution exec : executions) {
+            final StringBuilder builder = new StringBuilder(String.format("%10d\t|\t%s\t|\t%s\t|\t%s\t|\t%s\t|\t%s",
+                exec.getStepExecutionId(),
+                StringUtils.center(exec.getStepName(), 10),
+                format.format(exec.getStartTime()),
+                exec.getEndTime() != null ? format.format(exec.getEndTime()) : "-",
+                StringUtils.center(exec.getExitStatus() == null ? "-" : exec.getExitStatus(), 11),
+                StringUtils.center(String.valueOf(exec.getBatchStatus()), 12)));
+            final Map<Metric.MetricType, Long> stepMetrics = new HashMap<Metric.MetricType, Long>();
+            if (exec.getMetrics() != null) {
+                for (final Metric m : exec.getMetrics()) {
+                    stepMetrics.put(m.getType(), m.getValue());
+                }
+            }
+            for (final Metric.MetricType type : metricsOrder) {
+                final Long value = stepMetrics.get(type);
+                builder.append("\t|\t");
+                if (value != null) {
+                    builder.append(StringUtils.center(Long.toString(value), type.name().length()));
+                } else {
+                    builder.append("-");
+                }
+            }
+            info(builder.toString());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/command/api/CliConfiguration.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/api/CliConfiguration.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/api/CliConfiguration.java
new file mode 100644
index 0000000..96d09f2
--- /dev/null
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/api/CliConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.batchee.cli.command.api;
+
+import java.lang.reflect.Type;
+import java.util.Iterator;
+
+public interface CliConfiguration {
+    String name();
+    String description();
+    boolean addDefaultCommands();
+    Iterator<Class<? extends UserCommand>> userCommands();
+    Runnable decorate(Runnable task);
+    Object coerce(String value, Type expected);
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/main/java/org/apache/batchee/cli/command/internal/DefaultCliConfiguration.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/internal/DefaultCliConfiguration.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/internal/DefaultCliConfiguration.java
new file mode 100644
index 0000000..08060ac
--- /dev/null
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/internal/DefaultCliConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.batchee.cli.command.internal;
+
+import org.apache.batchee.cli.command.api.CliConfiguration;
+import org.apache.batchee.cli.command.api.UserCommand;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import static java.lang.ClassLoader.getSystemClassLoader;
+
+public class DefaultCliConfiguration implements CliConfiguration {
+    @Override
+    public String name() {
+        return "batchee";
+    }
+
+    @Override
+    public String description() {
+        return "BatchEE CLI";
+    }
+
+    @Override
+    public boolean addDefaultCommands() {
+        return true;
+    }
+
+    @Override
+    public Iterator<Class<? extends UserCommand>> userCommands() {
+        final Collection<Class<? extends UserCommand>> classes = new ArrayList<Class<? extends UserCommand>>();
+        try { // read manually cause we dont want to instantiate them there, so no ServiceLoader
+            final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+            final ClassLoader loader = tccl != null ? tccl : getSystemClassLoader();
+            final Enumeration<URL> uc = loader.getResources("META-INF/services/org.apache.batchee.cli.command.UserCommand");
+            while (uc.hasMoreElements()) {
+                final URL url = uc.nextElement();
+                BufferedReader r = null;
+                try {
+                    r = new BufferedReader(new InputStreamReader(url.openStream()));
+                    String line;
+                    while ((line = r.readLine()) != null) {
+                        if (line.startsWith("#") || line.trim().isEmpty()) {
+                            continue;
+                        }
+                        classes.add(Class.class.cast(loader.loadClass(line.trim())));
+                    }
+                } catch (final IOException ioe) {
+                    throw new IllegalStateException(ioe);
+                } catch (final ClassNotFoundException cnfe) {
+                    throw new IllegalArgumentException(cnfe);
+                } finally {
+                    if (r != null) {
+                        r.close();
+                    }
+                }
+            }
+        } catch (final IOException e) {
+            throw new IllegalStateException(e);
+        }
+        return classes.iterator();
+    }
+
+    @Override
+    public Runnable decorate(final Runnable task) {
+        return task;
+    }
+
+    @Override
+    public Object coerce(String value, Type expected) {
+        if (String.class == expected) {
+            return value;
+        }
+        if (long.class == expected) {
+            return Long.parseLong(value);
+        }
+        if (int.class == expected) {
+            return Integer.parseInt(value);
+        }
+        if (boolean.class == expected) {
+            return Boolean.parseBoolean(value);
+        }
+        if (short.class == expected) {
+            return Short.parseShort(value);
+        }
+        if (byte.class == expected) {
+            return Byte.parseByte(value);
+        }
+        if (char.class == expected) {
+            return value.charAt(0);
+        }
+        if (Class.class.isInstance(expected)) {
+            try {
+                return Class.class.cast(expected).getMethod("fromString", String.class).invoke(null, value);
+            } catch (final Exception e) {
+                // no-op
+            }
+        }
+        throw new IllegalArgumentException(expected + " not supported as option with value '" + value + "'");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/d24d2ef0/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java b/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
index 6139a09..957ab3b 100644
--- a/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
+++ b/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
@@ -38,6 +38,7 @@ import static org.junit.Assert.assertThat;
 public class MainTest {
     @Rule
     public final StandardOutputStreamLog stdout = new StandardOutputStreamLog();
+
     @Rule
     public final StandardErrorStreamLog stderr = new StandardErrorStreamLog();
 
@@ -54,6 +55,7 @@ public class MainTest {
             "running: list running batches\n" +
             "start: start a batch\n" +
             "status: list last batches statuses\n" +
+            "stepExecutions: list step executions for a particular execution\n" +
             "stop: stop a batch from its id\n" +
             "user1\n" +
             "user2\n", stderr.getLog().replace(System.getProperty("line.separator"), "\n"));
@@ -63,8 +65,10 @@ public class MainTest {
     public void helpCommand() {
         main(new String[] { "help", "evict" }); // using a simple command to avoid a big block for nothing
         assertEquals(
-            "usage: evict [-until <arg>]\n\n" +
-            "remove old data, uses embedded configuration (no JAXRS support yet)\n\n" +
+            "usage: evict -until <arg>\n" +
+            "\n" +
+            "remove old data, uses embedded configuration (no JAXRS support yet)\n" +
+            "\n" +
             " -until <arg>   date until when the eviction will occur (excluded),\n" +
             "                YYYYMMDD format\n", stdout.getLog().replace(System.getProperty("line.separator"), "\n"));
     }
@@ -208,6 +212,26 @@ public class MainTest {
     }
 
     @Test
+    public void stepExecutions() {
+        // ensure we have at least one thing to print
+        final JobOperator jobOperator = BatchRuntime.getJobOperator();
+        final long id = jobOperator.start("sample", null);
+
+        Batches.waitForEnd(jobOperator, id);
+        main(new String[]{"stepExecutions", "-id", Long.toString(id)});
+
+        System.out.println(stdout.getLog());
+        assertThat(stdout.getLog(), containsString(
+            "step id\t|\t step name\t|\t    start time   \t|\t     end time    \t|\t" +
+            "exit status\t|\tbatch status\t|\t" +
+            "READ_COUNT\t|\tWRITE_COUNT\t|\tCOMMIT_COUNT\t|\tROLLBACK_COUNT\t|\tREAD_SKIP_COUNT\t|\t" +
+            "PROCESS_SKIP_COUNT\t|\tFILTER_COUNT\t|\tWRITE_SKIP_COUNT"));
+        assertThat(stdout.getLog(), containsString("OK"));
+        assertThat(stdout.getLog(), containsString("sample-step"));
+        assertThat(stdout.getLog(), containsString("COMPLETED"));
+    }
+
+    @Test
     public void lifecycle() {
         // whatever child of JobOperatorCommand so using running which is simple
         main(new String[]{ "running", "-lifecycle", MyLifecycle.class.getName() });