Merge "Late binding: add support to Mac"
diff --git a/luni/src/main/java/javax/crypto/Mac.java b/luni/src/main/java/javax/crypto/Mac.java
index d74c2b9..5a73dc5 100644
--- a/luni/src/main/java/javax/crypto/Mac.java
+++ b/luni/src/main/java/javax/crypto/Mac.java
@@ -24,8 +24,10 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.security.Provider;
+import java.security.ProviderException;
 import java.security.Security;
 import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
 import org.apache.harmony.security.fortress.Engine;
 
 
@@ -35,18 +37,29 @@
  */
 public class Mac implements Cloneable {
 
+    // The service name.
+    private static final String SERVICE = "Mac";
+
     //Used to access common engine functionality
-    private static final Engine ENGINE = new Engine("Mac");
+    private static final Engine ENGINE = new Engine(SERVICE);
 
     // Store used provider
-    private final Provider provider;
+    private Provider provider;
+
+    // Provider that was requested during creation.
+    private final Provider specifiedProvider;
 
     // Store used spi implementation
-    private final MacSpi spiImpl;
+    private MacSpi spiImpl;
 
     // Store used algorithm name
     private final String algorithm;
 
+    /**
+     * Lock held while the SPI is initializing.
+     */
+    private final Object initLock = new Object();
+
     // Store Mac state (initialized or not initialized)
     private boolean isInitMac;
 
@@ -61,7 +74,7 @@
      *            the name of the MAC algorithm.
      */
     protected Mac(MacSpi macSpi, Provider provider, String algorithm) {
-        this.provider = provider;
+        this.specifiedProvider = provider;
         this.algorithm = algorithm;
         this.spiImpl = macSpi;
         this.isInitMac = false;
@@ -82,6 +95,7 @@
      * @return the provider of this {@code Mac} instance.
      */
     public final Provider getProvider() {
+        getSpi();
         return provider;
     }
 
@@ -100,11 +114,7 @@
      */
     public static final Mac getInstance(String algorithm)
             throws NoSuchAlgorithmException {
-        if (algorithm == null) {
-            throw new NullPointerException("algorithm == null");
-        }
-        Engine.SpiAndProvider sap = ENGINE.getInstance(algorithm, null);
-        return new Mac((MacSpi) sap.spi, sap.provider, algorithm);
+        return getMac(algorithm, null);
     }
 
     /**
@@ -136,7 +146,7 @@
         if (impProvider == null) {
             throw new NoSuchProviderException(provider);
         }
-        return getInstance(algorithm, impProvider);
+        return getMac(algorithm, impProvider);
     }
 
     /**
@@ -163,11 +173,102 @@
         if (provider == null) {
             throw new IllegalArgumentException("provider == null");
         }
+        return getMac(algorithm, provider);
+    }
+
+    private static Mac getMac(String algorithm, Provider provider)
+            throws NoSuchAlgorithmException {
         if (algorithm == null) {
             throw new NullPointerException("algorithm == null");
         }
-        Object spi = ENGINE.getInstance(algorithm, provider, null);
-        return new Mac((MacSpi) spi, provider, algorithm);
+
+        if (tryAlgorithm(null, provider, algorithm) == null) {
+            if (provider == null) {
+                throw new NoSuchAlgorithmException("No provider found for " + algorithm);
+            } else {
+                throw new NoSuchAlgorithmException("Provider " + provider.getName()
+                        + " does not provide " + algorithm);
+            }
+        }
+        return new Mac(null, provider, algorithm);
+    }
+
+    private static Engine.SpiAndProvider tryAlgorithm(Key key, Provider provider, String algorithm) {
+        if (provider != null) {
+            Provider.Service service = provider.getService(SERVICE, algorithm);
+            if (service == null) {
+                return null;
+            }
+            return tryAlgorithmWithProvider(key, service);
+        }
+        ArrayList<Provider.Service> services = ENGINE.getServices(algorithm);
+        if (services == null) {
+            return null;
+        }
+        for (Provider.Service service : services) {
+            Engine.SpiAndProvider sap = tryAlgorithmWithProvider(key, service);
+            if (sap != null) {
+                return sap;
+            }
+        }
+        return null;
+    }
+
+    private static Engine.SpiAndProvider tryAlgorithmWithProvider(Key key, Provider.Service service) {
+        try {
+            if (key != null && !service.supportsParameter(key)) {
+                return null;
+            }
+
+            Engine.SpiAndProvider sap = ENGINE.getInstance(service, null);
+            if (sap.spi == null || sap.provider == null) {
+                return null;
+            }
+            if (!(sap.spi instanceof MacSpi)) {
+                return null;
+            }
+            return sap;
+        } catch (NoSuchAlgorithmException ignored) {
+        }
+        return null;
+    }
+
+    /**
+     * Makes sure a MacSpi that matches this type is selected.
+     */
+    private MacSpi getSpi(Key key) {
+        synchronized (initLock) {
+            if (spiImpl != null && provider != null && key == null) {
+                return spiImpl;
+            }
+
+            if (algorithm == null) {
+                return null;
+            }
+
+            final Engine.SpiAndProvider sap = tryAlgorithm(key, specifiedProvider, algorithm);
+            if (sap == null) {
+                throw new ProviderException("No provider for " + getAlgorithm());
+            }
+
+            /*
+             * Set our Spi if we've never been initialized or if we have the Spi
+             * specified and have a null provider.
+             */
+            if (spiImpl == null || provider != null) {
+                spiImpl = (MacSpi) sap.spi;
+            }
+            provider = sap.provider;
+
+            return spiImpl;
+        }
+    }
+
+    /**
+     * Convenience call when the Key is not available.
+     */
+    private MacSpi getSpi() {
+        return getSpi(null);
     }
 
     /**
@@ -176,7 +277,7 @@
      * @return the length of this MAC (in bytes).
      */
     public final int getMacLength() {
-        return spiImpl.engineGetMacLength();
+        return getSpi().engineGetMacLength();
     }
 
     /**
@@ -199,7 +300,7 @@
         if (key == null) {
             throw new InvalidKeyException("key == null");
         }
-        spiImpl.engineInit(key, params);
+        getSpi(key).engineInit(key, params);
         isInitMac = true;
     }
 
@@ -220,7 +321,7 @@
             throw new InvalidKeyException("key == null");
         }
         try {
-            spiImpl.engineInit(key, null);
+            getSpi(key).engineInit(key, null);
             isInitMac = true;
         } catch (InvalidAlgorithmParameterException e) {
             throw new RuntimeException(e);
@@ -239,7 +340,7 @@
         if (!isInitMac) {
             throw new IllegalStateException();
         }
-        spiImpl.engineUpdate(input);
+        getSpi().engineUpdate(input);
     }
 
     /**
@@ -270,7 +371,7 @@
                                                + " input.length=" + input.length
                                                + " offset=" + offset + ", len=" + len);
         }
-        spiImpl.engineUpdate(input, offset, len);
+        getSpi().engineUpdate(input, offset, len);
     }
 
     /**
@@ -286,7 +387,7 @@
             throw new IllegalStateException();
         }
         if (input != null) {
-            spiImpl.engineUpdate(input, 0, input.length);
+            getSpi().engineUpdate(input, 0, input.length);
         }
     }
 
@@ -305,7 +406,7 @@
             throw new IllegalStateException();
         }
         if (input != null) {
-            spiImpl.engineUpdate(input);
+            getSpi().engineUpdate(input);
         } else {
             throw new IllegalArgumentException("input == null");
         }
@@ -327,7 +428,7 @@
         if (!isInitMac) {
             throw new IllegalStateException();
         }
-        return spiImpl.engineDoFinal();
+        return getSpi().engineDoFinal();
     }
 
     /**
@@ -362,11 +463,12 @@
         if ((outOffset < 0) || (outOffset >= output.length)) {
             throw new ShortBufferException("Incorrect outOffset: " + outOffset);
         }
-        int t = spiImpl.engineGetMacLength();
+        MacSpi spi = getSpi();
+        int t = spi.engineGetMacLength();
         if (t > (output.length - outOffset)) {
             throw new ShortBufferException("Output buffer is short. Needed " + t + " bytes.");
         }
-        byte[] result = spiImpl.engineDoFinal();
+        byte[] result = spi.engineDoFinal();
         System.arraycopy(result, 0, output, outOffset, result.length);
 
     }
@@ -390,10 +492,11 @@
         if (!isInitMac) {
             throw new IllegalStateException();
         }
+        MacSpi spi = getSpi();
         if (input != null) {
-            spiImpl.engineUpdate(input, 0, input.length);
+            spi.engineUpdate(input, 0, input.length);
         }
-        return spiImpl.engineDoFinal();
+        return spi.engineDoFinal();
     }
 
     /**
@@ -404,7 +507,7 @@
      * initialized with different parameters.
      */
     public final void reset() {
-        spiImpl.engineReset();
+        getSpi().engineReset();
     }
 
     /**
@@ -416,7 +519,11 @@
      */
     @Override
     public final Object clone() throws CloneNotSupportedException {
-        MacSpi newSpiImpl = (MacSpi)spiImpl.clone();
+        MacSpi newSpiImpl = null;
+        final MacSpi spi = getSpi();
+        if (spi != null) {
+            newSpiImpl = (MacSpi) spi.clone();
+        }
         Mac mac = new Mac(newSpiImpl, this.provider, this.algorithm);
         mac.isInitMac = this.isInitMac;
         return mac;
diff --git a/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MacTest.java b/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MacTest.java
index aaf2a15..ddd0695 100644
--- a/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MacTest.java
+++ b/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MacTest.java
@@ -27,27 +27,26 @@
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
 import java.security.Provider;
 import java.security.Security;
 import java.security.spec.PSSParameterSpec;
 import java.util.ArrayList;
 import java.util.Arrays;
-
 import javax.crypto.Mac;
 import javax.crypto.MacSpi;
 import javax.crypto.SecretKey;
 import javax.crypto.ShortBufferException;
 import javax.crypto.spec.DHGenParameterSpec;
-
 import javax.crypto.spec.SecretKeySpec;
-
 import org.apache.harmony.crypto.tests.support.MyMacSpi;
 import org.apache.harmony.security.tests.support.SpiEngUtils;
-
 import junit.framework.TestCase;
-
 import junit.framework.Test;
 import junit.framework.TestSuite;
+import libcore.java.security.StandardNames;
+import libcore.javax.crypto.MockKey;
+import libcore.javax.crypto.MockKey2;
 
 /**
  * Tests for Mac class constructors and methods
@@ -766,15 +765,14 @@
         }
         MacSpi spi = new MyMacSpi();
         Mac mac = new myMac(spi, defaultProvider, defaultAlgorithm);
-        assertEquals("Incorrect algorithm", mac.getAlgorithm(),
-                defaultAlgorithm);
-        assertEquals("Incorrect provider", mac.getProvider(), defaultProvider);
+        assertEquals("Incorrect algorithm", defaultAlgorithm, mac.getAlgorithm());
+        assertEquals("Incorrect provider", defaultProvider, mac.getProvider());
         try {
             mac.init(null, null);
             fail("Exception should be thrown because init(..) uses incorrect parameters");
         } catch (Exception e) {
         }
-        assertEquals("Invalid mac length", mac.getMacLength(), 0);
+        assertEquals("Invalid mac length", 0, mac.getMacLength());
 
         mac = new myMac(null, null, null);
         assertNull("Algorithm must be null", mac.getAlgorithm());
@@ -877,6 +875,127 @@
         }
     }
 
+    private static abstract class MockProvider extends Provider {
+        public MockProvider(String name) {
+            super(name, 1.0, "Mock provider used for testing");
+            setup();
+        }
+
+        public abstract void setup();
+    }
+
+    public void testMac_getInstance_SuppliedProviderNotRegistered_Success() throws Exception {
+        Provider mockProvider = new MockProvider("MockProvider") {
+            public void setup() {
+                put("Mac.FOO", MockMacSpi.AllKeyTypes.class.getName());
+            }
+        };
+
+        {
+            Mac s = Mac.getInstance("FOO", mockProvider);
+            s.init(new MockKey());
+            assertEquals(mockProvider, s.getProvider());
+        }
+    }
+
+    public void testMac_getInstance_OnlyUsesSpecifiedProvider_SameNameAndClass_Success()
+            throws Exception {
+        Provider mockProvider = new MockProvider("MockProvider") {
+            public void setup() {
+                put("Mac.FOO", MockMacSpi.AllKeyTypes.class.getName());
+            }
+        };
+
+        Security.addProvider(mockProvider);
+        try {
+            {
+                Provider mockProvider2 = new MockProvider("MockProvider") {
+                    public void setup() {
+                        put("Mac.FOO", MockMacSpi.AllKeyTypes.class.getName());
+                    }
+                };
+                Mac s = Mac.getInstance("FOO", mockProvider2);
+                assertEquals(mockProvider2, s.getProvider());
+            }
+        } finally {
+            Security.removeProvider(mockProvider.getName());
+        }
+    }
+
+    public void testMac_getInstance_DelayedInitialization_KeyType() throws Exception {
+        Provider mockProviderSpecific = new MockProvider("MockProviderSpecific") {
+            public void setup() {
+                put("Mac.FOO", MockMacSpi.SpecificKeyTypes.class.getName());
+                put("Mac.FOO SupportedKeyClasses", MockKey.class.getName());
+            }
+        };
+        Provider mockProviderSpecific2 = new MockProvider("MockProviderSpecific2") {
+            public void setup() {
+                put("Mac.FOO", MockMacSpi.SpecificKeyTypes2.class.getName());
+                put("Mac.FOO SupportedKeyClasses", MockKey2.class.getName());
+            }
+        };
+        Provider mockProviderAll = new MockProvider("MockProviderAll") {
+            public void setup() {
+                put("Mac.FOO", MockMacSpi.AllKeyTypes.class.getName());
+            }
+        };
+
+        Security.addProvider(mockProviderSpecific);
+        Security.addProvider(mockProviderSpecific2);
+        Security.addProvider(mockProviderAll);
+
+        try {
+            {
+                Mac s = Mac.getInstance("FOO");
+                s.init(new MockKey());
+                assertEquals(mockProviderSpecific, s.getProvider());
+
+                try {
+                    s.init(new MockKey2());
+                    assertEquals(mockProviderSpecific2, s.getProvider());
+                    if (StandardNames.IS_RI) {
+                        fail("RI was broken before; fix tests now that it works!");
+                    }
+                } catch (InvalidKeyException e) {
+                    if (!StandardNames.IS_RI) {
+                        fail("Non-RI should select the right provider");
+                    }
+                }
+            }
+
+            {
+                Mac s = Mac.getInstance("FOO");
+                s.init(new PrivateKey() {
+                    @Override
+                    public String getAlgorithm() {
+                        throw new UnsupportedOperationException("not implemented");
+                    }
+
+                    @Override
+                    public String getFormat() {
+                        throw new UnsupportedOperationException("not implemented");
+                    }
+
+                    @Override
+                    public byte[] getEncoded() {
+                        throw new UnsupportedOperationException("not implemented");
+                    }
+                });
+                assertEquals(mockProviderAll, s.getProvider());
+            }
+
+            {
+                Mac s = Mac.getInstance("FOO");
+                assertEquals(mockProviderSpecific, s.getProvider());
+            }
+        } finally {
+            Security.removeProvider(mockProviderSpecific.getName());
+            Security.removeProvider(mockProviderSpecific2.getName());
+            Security.removeProvider(mockProviderAll.getName());
+        }
+    }
+
     public static Test suite() {
         return new TestSuite(MacTest.class);
     }
diff --git a/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MockMacSpi.java b/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MockMacSpi.java
new file mode 100644
index 0000000..6a28fb3e
--- /dev/null
+++ b/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/MockMacSpi.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * 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.apache.harmony.crypto.tests.javax.crypto;
+
+import java.security.InvalidKeyException;
+import java.security.InvalidParameterException;
+import java.security.Key;
+import java.security.spec.AlgorithmParameterSpec;
+import javax.crypto.MacSpi;
+import libcore.javax.crypto.MockKey;
+import libcore.javax.crypto.MockKey2;
+
+public class MockMacSpi extends MacSpi {
+    public static class SpecificKeyTypes extends MockMacSpi {
+        @Override
+        public void checkKeyType(Key key) throws InvalidKeyException {
+            if (!(key instanceof MockKey)) {
+                throw new InvalidKeyException("Must be MockKey!");
+            }
+        }
+    }
+
+    public static class SpecificKeyTypes2 extends MockMacSpi {
+        @Override
+        public void checkKeyType(Key key) throws InvalidKeyException {
+            if (!(key instanceof MockKey2)) {
+                throw new InvalidKeyException("Must be MockKey2!");
+            }
+        }
+    }
+
+    public static class AllKeyTypes extends MockMacSpi {
+    }
+
+    public void checkKeyType(Key key) throws InvalidKeyException {
+    }
+
+    @Override
+    protected int engineGetMacLength() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException,
+            InvalidParameterException {
+        checkKeyType(key);
+    }
+
+    @Override
+    protected void engineUpdate(byte input) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected void engineUpdate(byte[] input, int offset, int len) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected byte[] engineDoFinal() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected void engineReset() {
+        throw new UnsupportedOperationException();
+    }
+}