OWB-1284 add a default main for CDI SE API

git-svn-id: https://svn.apache.org/repos/asf/openwebbeans/trunk@1857523 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webbeans-se/README.adoc b/webbeans-se/README.adoc
new file mode 100644
index 0000000..146584c
--- /dev/null
+++ b/webbeans-se/README.adoc
@@ -0,0 +1,71 @@
+= OpenWebBeans CDI SE implementation
+
+It is a plain CDI SE implementation but with a few specific features:
+
+. It enables to be extended through `org.apache.openwebbeans.se.SeContainerSelector`,
+. It has specific properties intended to be used to configured the container,
+. It provides a generic `main(String[])`
+
+== `SeContainer` OpenWebBeans properties
+
+[opts="header"]
+|===
+| Name | Description
+| `openwebbeans.disableDiscovery`|`true` to disable the automatic discovery, false otherwise
+| `openwebbeans.classes`|Comma separated values of fully qualified classes names to register in the container
+| `openwebbeans.packages`|Comma separated values of fully qualified packages (either direct packages if `package-info` exists or a class of this package if not) names to register in the container
+| `openwebbeans.packages.recursive`|Comma separated values of fully qualified packages (either direct packages if `package-info` exists or a class of this package if not) names to register in the container recursively
+| `openwebbeans.decorators`|List of decorators to enable
+| `openwebbeans.interceptors`|List of interceptors to enable
+| `openwebbeans.alternatives`|List of alternatives to enable
+| `openwebbeans.stereotypes`|List of alternative stereotypes to enable
+| `openwebbeans.properties`|Properties to enable - in `java.util.Properties` format
+| `openwebbeans.property.${key}`|A property to enable, this is not useful by itself in programmatic mode but `CDILauncher` can rely on it.
+|===
+
+== Generic Launcher
+
+`org.apache.openwebbeans.se.CDILauncher` is a reusable main to launch any CDI application.
+In other words you can launch. It reuses the previous part properties prefixed with `--`
+and getting their value using a space as separator.
+This main also takes a new property `--openwebbeans.main` to specify an optional
+main. The value is either a bean name or a fully qualified class name using `@Default` qualifier.
+The method executed is either `Runnable#run` if the bean implements it
+or a `run()` method or a `main()` method or a `main(String...)̀  method taking
+the cleaned up arguments.
+
+Example:
+
+[source,sh]
+----
+java -cp .... \
+    org.apache.openwebbeans.se.CDILauncher \
+    --openwebbeans.main myNamedMain \
+    --openwebbeans.disableDiscovery true \
+    --openwebbeans.packaged com.company.myapp
+    --cli-opt opt-value \
+    other thing
+----
+
+If the bean named `myNamedMain` is the following one:
+
+[source,java]
+----
+@Named("myNamedMain")
+@ApplicationScoped
+public class MyMain {
+  public void main(String... args) {
+      Stream.of(args).forEach(System.out::println);
+  }
+}
+----
+
+Then the execution will print:
+
+[source]
+----
+--cli-opt
+opt-value
+other
+thing
+----
diff --git a/webbeans-se/src/main/java/org/apache/openwebbeans/se/CDILauncher.java b/webbeans-se/src/main/java/org/apache/openwebbeans/se/CDILauncher.java
new file mode 100644
index 0000000..38ea857
--- /dev/null
+++ b/webbeans-se/src/main/java/org/apache/openwebbeans/se/CDILauncher.java
@@ -0,0 +1,195 @@
+/*
+ * 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.openwebbeans.se;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+
+import javax.enterprise.context.spi.CreationalContext;
+import javax.enterprise.inject.se.SeContainer;
+import javax.enterprise.inject.se.SeContainerInitializer;
+import javax.enterprise.inject.spi.Bean;
+import javax.enterprise.inject.spi.BeanManager;
+
+public final class CDILauncher
+{
+    private CDILauncher()
+    {
+        // no-op
+    }
+
+    public static void main(final String[] args)
+    {
+        final SeContainerInitializer initializer = SeContainerInitializer.newInstance();
+        final Config config = configure(initializer, args);
+        try (final SeContainer container = initializer.initialize())
+        {
+            if (config.main != null)
+            {
+                executeMain(config, container);
+            }
+            // else the app can use other ways to execute code like @PreDestroy of a startup bean
+        }
+    }
+
+    private static void executeMain(final Config config, final SeContainer container)
+    {
+        final BeanManager manager = container.getBeanManager();
+        Set<Bean<?>> beans = manager.getBeans(config.main);
+        if (beans == null || beans.isEmpty())
+        {
+            try
+            {
+                beans = manager.getBeans(Thread.currentThread().getContextClassLoader().loadClass(config.main));
+            }
+            catch (final ClassNotFoundException e)
+            {
+                throw new IllegalArgumentException("No bean '" + config.main + "' found");
+            }
+        }
+        final Bean<?> bean = manager.resolve(beans);
+        final CreationalContext<Object> context = manager.createCreationalContext(null);
+        final Object reference = manager.getReference(bean, selectProxyType(bean.getTypes()), context);
+        try
+        {
+            if (Runnable.class.isInstance(reference))
+            {
+                Runnable.class.cast(reference).run();
+            }
+            else // by reflection
+            {
+                // try a main method taking parameters
+                // then just a main or run method
+                Method method = null;
+                try
+                {
+                    method = bean.getBeanClass()
+                            .getMethod("main", String[].class);
+                }
+                catch (final Exception e)
+                {
+                    try
+                    {
+                        method = bean.getBeanClass()
+                                .getMethod("main");
+                    }
+                    catch (final Exception e2)
+                    {
+                        try
+                        {
+                            method = bean.getBeanClass()
+                                    .getMethod("main");
+                        }
+                        catch (final Exception e3)
+                        {
+                            throw new IllegalArgumentException(
+                                    bean + " does not implements Runnable or has a public main method");
+                        }
+                    }
+                }
+                try
+                {
+                    final Object output = method.invoke(
+                            Modifier.isStatic(method.getModifiers()) ? null : reference,
+                            method.getParameterCount() == 1 ? new Object[] { config.args } : new Object[0]);
+                    if (output != null)
+                    {
+                        System.out.println(output);
+                    }
+                }
+                catch (final IllegalAccessException e)
+                {
+                    throw new IllegalStateException(e);
+                }
+                catch (final InvocationTargetException e)
+                {
+                    throw new IllegalStateException(e.getTargetException());
+                }
+            }
+        }
+        finally
+        {
+            if (manager.isNormalScope(bean.getScope()))
+            {
+                context.release();
+            }
+        }
+    }
+
+    private static Class<?> selectProxyType(final Set<Type> types)
+    {
+        if (types.contains(Runnable.class))
+        {
+            return Runnable.class;
+        }
+        return Object.class;
+    }
+
+    private static Config configure(final SeContainerInitializer initializer, final String[] args)
+    {
+        String main = null;
+        final Collection<String> remaining = new ArrayList<>(args.length);
+        for (int i = 0; i < args.length; i++)
+        {
+            final String current = args[i];
+            if (current != null && current.startsWith("--openwebbeans."))
+            {
+                if (args.length <= i + 1)
+                {
+                    throw new IllegalArgumentException("Missing argument value for: '" + args[i] + "'");
+                }
+                if (current.equals("--openwebbeans.main"))
+                {
+                    if (main != null)
+                    {
+                        throw new IllegalArgumentException("Ambiguous main '" + main + "' vs '" + args[i + 1] + "'");
+                    }
+                    main = args[i + 1];
+                }
+                else
+                {
+                    initializer.addProperty(current.substring("--".length()), args[i + 1]);
+                }
+                i++;
+            }
+            else
+            {
+                remaining.add(current);
+            }
+        }
+        return new Config(remaining.toArray(new String[0]), main);
+    }
+
+    private static class Config
+    {
+        private final String[] args;
+        private final String main;
+
+        private Config(final String[] args, final String main)
+        {
+            this.args = args;
+            this.main = main;
+        }
+    }
+}
diff --git a/webbeans-se/src/main/java/org/apache/openwebbeans/se/OWBInitializer.java b/webbeans-se/src/main/java/org/apache/openwebbeans/se/OWBInitializer.java
index 8101a29..7563658 100644
--- a/webbeans-se/src/main/java/org/apache/openwebbeans/se/OWBInitializer.java
+++ b/webbeans-se/src/main/java/org/apache/openwebbeans/se/OWBInitializer.java
@@ -31,6 +31,9 @@
 import javax.enterprise.inject.se.SeContainer;
 import javax.enterprise.inject.se.SeContainerInitializer;
 import javax.enterprise.inject.spi.Extension;
+
+import java.io.IOException;
+import java.io.StringReader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
@@ -38,6 +41,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
+import java.util.function.Function;
 import java.util.stream.Stream;
 
 import static java.util.Arrays.asList;
@@ -203,15 +207,69 @@
     }
 
     @Override
-    public SeContainerInitializer addProperty(String key, Object value)
+    public SeContainerInitializer addProperty(final String key, final Object value)
     {
-        if (String.class.isInstance(value))
+        switch (key)
         {
-            properties.put(key, value);
-        }
-        else
-        {
-            services.put(key, value);
+            case "openwebbeans.disableDiscovery":
+                if ("true".equalsIgnoreCase(String.valueOf(value)))
+                {
+                    disableDiscovery();
+                }
+                break;
+            case "openwebbeans.classes":
+                addBeanClasses(list(value, this::loadClass).toArray(Class[]::new));
+                break;
+            case "openwebbeans.interceptors":
+                enableInterceptors(list(value, this::loadClass).toArray(Class[]::new));
+                break;
+            case "openwebbeans.decorators":
+                enableDecorators(list(value, this::loadClass).toArray(Class[]::new));
+                break;
+            case "openwebbeans.alternatives":
+                selectAlternatives(list(value, this::loadClass).toArray(Class[]::new));
+                break;
+            case "openwebbeans.stereotypes":
+                selectAlternativeStereotypes(list(value, this::loadClass).toArray(Class[]::new));
+                break;
+            case "openwebbeans.extensions":
+                addExtensions((Class<? extends Extension>[]) list(value, this::loadClass)
+                        .toArray(Class[]::new));
+                break;
+            case "openwebbeans.packages":
+                addPackages(list(value, this::loadPackage).toArray(Package[]::new));
+                break;
+            case "openwebbeans.packages.recursive":
+                addPackages(true, list(value, this::loadPackage).toArray(Package[]::new));
+                break;
+            case "openwebbeans.properties":
+            {
+                final Properties properties = new Properties();
+                try (final StringReader reader = new StringReader(String.valueOf(value)))
+                {
+                    properties.load(reader);
+                }
+                catch (final IOException e)
+                {
+                    throw new IllegalArgumentException(e);
+                }
+                properties.stringPropertyNames().forEach(k -> addProperty(k, properties.getProperty(k)));
+                break;
+            }
+            case "openwebbeans.property.":
+            {
+                addProperty(key.substring("openwebbeans.property.".length()), value);
+                break;
+            }
+            default:
+                if (String.class.isInstance(value))
+                {
+                    properties.put(key, value);
+                }
+                else
+                {
+                    services.put(key, value);
+                }
         }
         return this;
     }
@@ -237,4 +295,56 @@
         scannerService.loader(loader);
         return this;
     }
+
+    private <T> Stream<T> list(final Object list, final Function<Object, T> mapper)
+    {
+        if (Collection.class.isInstance(list))
+        {
+            return Collection.class.cast(list).stream().map(mapper);
+        }
+        return Stream.of(String.valueOf(list).split(","))
+                .map(String::trim)
+                .filter(it -> !it.isEmpty())
+                .map(mapper);
+    }
+
+    private Package loadPackage(final Object obj)
+    {
+        if (Package.class.isInstance(obj))
+        {
+            return Package.class.cast(obj);
+        }
+        final String name = String.valueOf(obj);
+        try
+        {
+            return loader.loadClass(name + ".package-info").getPackage();
+        }
+        catch (final ClassNotFoundException e)
+        {
+            try
+            {
+                return loader.loadClass(name).getPackage();
+            }
+            catch (final ClassNotFoundException e1)
+            {
+                throw new IllegalArgumentException(e);
+            }
+        }
+    }
+
+    private Class loadClass(final Object it)
+    {
+        if (Class.class.isInstance(it))
+        {
+            return Class.class.cast(it);
+        }
+        try
+        {
+            return loader.loadClass(String.valueOf(it));
+        }
+        catch (final ClassNotFoundException e)
+        {
+            throw new IllegalArgumentException(e);
+        }
+    }
 }
diff --git a/webbeans-se/src/test/java/org/apache/openwebbeans/se/CDILauncherTest.java b/webbeans-se/src/test/java/org/apache/openwebbeans/se/CDILauncherTest.java
new file mode 100644
index 0000000..6df3b2f
--- /dev/null
+++ b/webbeans-se/src/test/java/org/apache/openwebbeans/se/CDILauncherTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.openwebbeans.se;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Named;
+
+import org.junit.Test;
+
+public class CDILauncherTest
+{
+    @Test
+    public void namedRunnable()
+    {
+        assertFalse(MyRunnable.ran);
+        CDILauncher.main(new String[]{
+                "--openwebbeans.main", "main",
+                "--openwebbeans.disableDiscovery", "true",
+                "--openwebbeans.classes", MyRunnable.class.getName()
+        });
+        assertTrue(MyRunnable.ran);
+    }
+
+    @Test
+    public void typedRunnable()
+    {
+        assertFalse(MyRunnable2.ran);
+        CDILauncher.main(new String[]{
+                "--openwebbeans.main", MyRunnable2.class.getName(),
+                "--openwebbeans.disableDiscovery", "true",
+                "--openwebbeans.classes", MyRunnable2.class.getName()
+        });
+        assertTrue(MyRunnable2.ran);
+    }
+
+    @Test
+    public void mainArgs()
+    {
+        assertNull(MyMain.args);
+        CDILauncher.main(new String[]{
+                "--openwebbeans.main", MyMain.class.getName(),
+                "--openwebbeans.disableDiscovery", "true",
+                "--openwebbeans.classes", MyMain.class.getName(),
+                "--other", "yes",
+                "and", "args"
+        });
+        assertArrayEquals(new String[]{
+                "--other", "yes",
+                "and", "args"
+        }, MyMain.args);
+    }
+
+    @Named("main")
+    @ApplicationScoped
+    public static class MyRunnable implements Runnable
+    {
+        static boolean ran = false;
+
+        @Override
+        public void run()
+        {
+            ran = true;
+        }
+    }
+
+    @ApplicationScoped
+    public static class MyRunnable2 implements Runnable
+    {
+        static boolean ran = false;
+
+        @Override
+        public void run()
+        {
+            ran = true;
+        }
+    }
+
+    @ApplicationScoped
+    public static class MyMain
+    {
+        static String[] args;
+
+        public void main(final String... args)
+        {
+            MyMain.args = args;
+        }
+    }
+}