8047789: auth.login.LoginContext needs to be updated to work with modules

Reviewed-by: mullan, mchung, alanb
diff --git a/jdk/src/java.base/share/classes/javax/security/auth/login/LoginContext.java b/jdk/src/java.base/share/classes/javax/security/auth/login/LoginContext.java
index d86a914..e266c47 100644
--- a/jdk/src/java.base/share/classes/javax/security/auth/login/LoginContext.java
+++ b/jdk/src/java.base/share/classes/javax/security/auth/login/LoginContext.java
@@ -25,18 +25,18 @@
 
 package javax.security.auth.login;
 
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.lang.reflect.InvocationTargetException;
-import java.util.LinkedList;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.Map;
 import java.util.HashMap;
 import java.text.MessageFormat;
 import javax.security.auth.Subject;
 import javax.security.auth.AuthPermission;
 import javax.security.auth.callback.*;
-import java.security.AccessController;
+import javax.security.auth.spi.LoginModule;
 import java.security.AccessControlContext;
+import java.util.ServiceLoader;
+
 import sun.security.util.PendingException;
 import sun.security.util.ResourcesMgr;
 
@@ -192,7 +192,6 @@
  */
 public class LoginContext {
 
-    private static final String INIT_METHOD             = "initialize";
     private static final String LOGIN_METHOD            = "login";
     private static final String COMMIT_METHOD           = "commit";
     private static final String ABORT_METHOD            = "abort";
@@ -210,7 +209,6 @@
     private AccessControlContext creatorAcc = null;  // customized config only
     private ModuleInfo[] moduleStack;
     private ClassLoader contextClassLoader = null;
-    private static final Class<?>[] PARAMS = { };
 
     // state saved in the event a user-specified asynchronous exception
     // was specified and thrown
@@ -678,64 +676,64 @@
         for (int i = moduleIndex; i < moduleStack.length; i++, moduleIndex++) {
             try {
 
-                int mIndex = 0;
-                Method[] methods = null;
+                if (moduleStack[i].module == null) {
 
-                if (moduleStack[i].module != null) {
-                    methods = moduleStack[i].module.getClass().getMethods();
-                } else {
-
-                    // instantiate the LoginModule
+                    // locate and instantiate the LoginModule
                     //
-                    // Allow any object to be a LoginModule as long as it
-                    // conforms to the interface.
-                    Class<?> c = Class.forName(
-                                moduleStack[i].entry.getLoginModuleName(),
-                                true,
-                                contextClassLoader);
-
-                    Constructor<?> constructor = c.getConstructor(PARAMS);
-                    Object[] args = { };
-                    moduleStack[i].module = constructor.newInstance(args);
-
-                    // call the LoginModule's initialize method
-                    methods = moduleStack[i].module.getClass().getMethods();
-                    for (mIndex = 0; mIndex < methods.length; mIndex++) {
-                        if (methods[mIndex].getName().equals(INIT_METHOD)) {
+                    String name = moduleStack[i].entry.getLoginModuleName();
+                    ServiceLoader<LoginModule> sc = AccessController.doPrivileged(
+                            (PrivilegedAction<ServiceLoader<LoginModule>>)
+                                    () -> ServiceLoader.load(
+                                        LoginModule.class, contextClassLoader));
+                    for (LoginModule m: sc) {
+                        if (m.getClass().getName().equals(name)) {
+                            moduleStack[i].module = m;
+                            if (debug != null) {
+                                debug.println(name + " loaded as a service");
+                            }
                             break;
                         }
                     }
 
-                    Object[] initArgs = {subject,
-                                        callbackHandler,
-                                        state,
-                                        moduleStack[i].entry.getOptions() };
+                    if (moduleStack[i].module == null) {
+                        try {
+                            moduleStack[i].module = (LoginModule) Class.forName(
+                                    name, false, contextClassLoader).newInstance();
+                            if (debug != null) {
+                                debug.println(name + " loaded via reflection");
+                            }
+                        } catch (ClassNotFoundException e) {
+                            throw new LoginException("No LoginModule found for "
+                                    + name);
+                        }
+                    }
+
                     // invoke the LoginModule initialize method
-                    //
-                    // Throws ArrayIndexOutOfBoundsException if no such
-                    // method defined.  May improve to use LoginException in
-                    // the future.
-                    methods[mIndex].invoke(moduleStack[i].module, initArgs);
+                    moduleStack[i].module.initialize(subject,
+                            callbackHandler,
+                            state,
+                            moduleStack[i].entry.getOptions());
                 }
 
                 // find the requested method in the LoginModule
-                for (mIndex = 0; mIndex < methods.length; mIndex++) {
-                    if (methods[mIndex].getName().equals(methodName)) {
+                boolean status;
+                switch (methodName) {
+                    case LOGIN_METHOD:
+                        status = moduleStack[i].module.login();
                         break;
-                    }
+                    case COMMIT_METHOD:
+                        status = moduleStack[i].module.commit();
+                        break;
+                    case LOGOUT_METHOD:
+                        status = moduleStack[i].module.logout();
+                        break;
+                    case ABORT_METHOD:
+                        status = moduleStack[i].module.abort();
+                        break;
+                    default:
+                        throw new AssertionError("Unknown method " + methodName);
                 }
 
-                // set up the arguments to be passed to the LoginModule method
-                Object[] args = { };
-
-                // invoke the LoginModule method
-                //
-                // Throws ArrayIndexOutOfBoundsException if no such
-                // method defined.  May improve to use LoginException in
-                // the future.
-                boolean status = ((Boolean)methods[mIndex].invoke
-                                (moduleStack[i].module, args)).booleanValue();
-
                 if (status == true) {
 
                     // if SUFFICIENT, return if no prior REQUIRED errors
@@ -760,31 +758,12 @@
                     if (debug != null)
                         debug.println(methodName + " ignored");
                 }
-
-            } catch (NoSuchMethodException nsme) {
-                MessageFormat form = new MessageFormat(ResourcesMgr.getString
-                        ("unable.to.instantiate.LoginModule.module.because.it.does.not.provide.a.no.argument.constructor"));
-                Object[] source = {moduleStack[i].entry.getLoginModuleName()};
-                throwException(null, new LoginException(form.format(source)));
-            } catch (InstantiationException ie) {
-                throwException(null, new LoginException(ResourcesMgr.getString
-                        ("unable.to.instantiate.LoginModule.") +
-                        ie.getMessage()));
-            } catch (ClassNotFoundException cnfe) {
-                throwException(null, new LoginException(ResourcesMgr.getString
-                        ("unable.to.find.LoginModule.class.") +
-                        cnfe.getMessage()));
-            } catch (IllegalAccessException iae) {
-                throwException(null, new LoginException(ResourcesMgr.getString
-                        ("unable.to.access.LoginModule.") +
-                        iae.getMessage()));
-            } catch (InvocationTargetException ite) {
+            } catch (Exception ite) {
 
                 // failure cases
-
                 LoginException le;
 
-                if (ite.getCause() instanceof PendingException &&
+                if (ite instanceof PendingException &&
                     methodName.equals(LOGIN_METHOD)) {
 
                     // XXX
@@ -808,13 +787,13 @@
                     // the only time that is not true is in this case -
                     // do not call throwException here.
 
-                    throw (PendingException)ite.getCause();
+                    throw (PendingException)ite;
 
-                } else if (ite.getCause() instanceof LoginException) {
+                } else if (ite instanceof LoginException) {
 
-                    le = (LoginException)ite.getCause();
+                    le = (LoginException)ite;
 
-                } else if (ite.getCause() instanceof SecurityException) {
+                } else if (ite instanceof SecurityException) {
 
                     // do not want privacy leak
                     // (e.g., sensitive file path in exception msg)
@@ -826,14 +805,14 @@
                             ("original security exception with detail msg " +
                             "replaced by new exception with empty detail msg");
                         debug.println("original security exception: " +
-                                ite.getCause().toString());
+                                ite.toString());
                     }
                 } else {
 
                     // capture an unexpected LoginModule exception
                     java.io.StringWriter sw = new java.io.StringWriter();
-                    ite.getCause().printStackTrace
-                                                (new java.io.PrintWriter(sw));
+                    ite.printStackTrace
+                            (new java.io.PrintWriter(sw));
                     sw.flush();
                     le = new LoginException(sw.toString());
                 }
@@ -938,9 +917,9 @@
      */
     private static class ModuleInfo {
         AppConfigurationEntry entry;
-        Object module;
+        LoginModule module;
 
-        ModuleInfo(AppConfigurationEntry newEntry, Object newModule) {
+        ModuleInfo(AppConfigurationEntry newEntry, LoginModule newModule) {
             this.entry = newEntry;
             this.module = newModule;
         }
diff --git a/jdk/src/java.base/share/classes/javax/security/auth/spi/LoginModule.java b/jdk/src/java.base/share/classes/javax/security/auth/spi/LoginModule.java
index 871bbbc..3206b5a 100644
--- a/jdk/src/java.base/share/classes/javax/security/auth/spi/LoginModule.java
+++ b/jdk/src/java.base/share/classes/javax/security/auth/spi/LoginModule.java
@@ -32,10 +32,9 @@
 import java.util.Map;
 
 /**
- * <p> {@code LoginModule} describes the interface
- * implemented by authentication technology providers.  LoginModules
- * are plugged in under applications to provide a particular type of
- * authentication.
+ * <p> Service-provider interface for authentication technology providers.
+ * LoginModules are plugged in under applications to provide a particular
+ * type of authentication.
  *
  * <p> While applications write to the {@code LoginContext} API,
  * authentication technology providers implement the
diff --git a/jdk/test/javax/security/auth/spi/FirstLoginModule.java b/jdk/test/javax/security/auth/spi/FirstLoginModule.java
new file mode 100644
index 0000000..60112ae
--- /dev/null
+++ b/jdk/test/javax/security/auth/spi/FirstLoginModule.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import java.util.Map;
+
+public class FirstLoginModule implements LoginModule {
+
+    @Override
+    public void initialize(Subject subject, CallbackHandler callbackHandler,
+            Map<String, ?> sharedState, Map<String, ?> options) {
+        // Nothing
+    }
+
+    @Override
+    public boolean login() throws LoginException {
+        return true;
+    }
+
+    @Override
+    public boolean commit() throws LoginException {
+        return true;
+    }
+
+    @Override
+    public boolean abort() throws LoginException {
+        return true;
+    }
+
+    @Override
+    public boolean logout() throws LoginException {
+        return true;
+    }
+}
diff --git a/jdk/test/javax/security/auth/spi/Loader.java b/jdk/test/javax/security/auth/spi/Loader.java
new file mode 100644
index 0000000..e2bac97
--- /dev/null
+++ b/jdk/test/javax/security/auth/spi/Loader.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.security.auth.login.LoginContext;
+import java.io.File;
+
+/*
+ * @test
+ * @bug 8047789
+ * @summary auth.login.LoginContext needs to be updated to work with modules
+ * @build FirstLoginModule SecondLoginModule
+ * @run main/othervm Loader
+ */
+public class Loader {
+
+    public static void main(String[] args) throws Exception {
+
+        System.setProperty("java.security.auth.login.config",
+                new File(System.getProperty("test.src"), "sl.conf").toString());
+        LoginContext lc = new LoginContext("me");
+
+        if (SecondLoginModule.isLoaded) {
+            throw new Exception();
+        }
+
+        lc.login();
+
+        // Although only FirstLoginModule is specified in the JAAS login
+        // config file, LoginContext will first create all LoginModule
+        // implementations that are registered as services, which include
+        // SecondLoginModule.
+        if (!SecondLoginModule.isLoaded) {
+            throw new Exception();
+        }
+    }
+}
diff --git a/jdk/test/javax/security/auth/spi/META-INF/services/javax.security.auth.spi.LoginModule b/jdk/test/javax/security/auth/spi/META-INF/services/javax.security.auth.spi.LoginModule
new file mode 100644
index 0000000..b618924
--- /dev/null
+++ b/jdk/test/javax/security/auth/spi/META-INF/services/javax.security.auth.spi.LoginModule
@@ -0,0 +1 @@
+SecondLoginModule
diff --git a/jdk/test/javax/security/auth/spi/SecondLoginModule.java b/jdk/test/javax/security/auth/spi/SecondLoginModule.java
new file mode 100644
index 0000000..c6c8587
--- /dev/null
+++ b/jdk/test/javax/security/auth/spi/SecondLoginModule.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import java.util.Map;
+
+public class SecondLoginModule implements LoginModule {
+
+    public static boolean isLoaded;
+
+    public SecondLoginModule() {
+        isLoaded = true;
+    }
+
+    @Override
+    public void initialize(Subject subject, CallbackHandler callbackHandler,
+            Map<String, ?> sharedState, Map<String, ?> options) {
+        // Nothing
+    }
+
+    @Override
+    public boolean login() throws LoginException {
+        return true;
+    }
+
+    @Override
+    public boolean commit() throws LoginException {
+        return true;
+    }
+
+    @Override
+    public boolean abort() throws LoginException {
+        return true;
+    }
+
+    @Override
+    public boolean logout() throws LoginException {
+        return true;
+    }
+}
diff --git a/jdk/test/javax/security/auth/spi/sl.conf b/jdk/test/javax/security/auth/spi/sl.conf
new file mode 100644
index 0000000..e14400e
--- /dev/null
+++ b/jdk/test/javax/security/auth/spi/sl.conf
@@ -0,0 +1,3 @@
+me {
+   FirstLoginModule required;
+};