Make MimeMap final and introduce MimeMap.Builder.

This CL topic introduces a new @CorePlatformApi MimeMap.Builder
and uses it to make MimeMap a concrete, final, immutable type.

This has the following advantages:

 - Consistency of behavior of MimeMap implementations with regards
   to lower-casing and treatment of null is trivial to guarantee
   because there is only one implementation.
 - The @CorePlatformApi surface now makes more sense. The responsibility
   for lower-casing and treatment of null was previously split between
   MimeMap in libcore and  MimeMapImpl in frameworks/base, which is why
   MimeMap.toLowerCase() and MimeMap.isNullOrEmpty() were in the
   @CorePlatformApi.
 - Most of the logic now lives in libcore / ART module.
   frameworks/base now has minimal logic. This makes it easier to write
   (in a follow-up CL) a CTS test that asserts all the default mappings,
   because that test can now duplicate that small amount of logic in
   order to read from a copy of the same data files.

Note: The semantics of the @CorePlatformApi Builder.put(String, List<String>)
are fairly complex, which isn't great. This was done because:
 - By following the semantics of the *mime.types file format, it allows
   us to minimize the logic in frameworks/base.
 - It's simpler than having multiple overloads of put() for
   mimeType -> file extension mapping and vice versa,
   and for whether or not any existing mapping should be overwritten.
   If we had named classes for MimeType and FileExtension with
   appropriate case-insensitive equals and hashCode semantics, then
   we could instead have API such as
      builder.asMimeToExtensionMap().put(...)
   but existing API (e.g. Intent.getType(), android.webkit.MimeTypeMap)
   has set precedent for treating these as Strings.

Bug: 136256059
Test: atest CtsLibcoreTestCases
Test: atest CtsMimeMapTestCases

Change-Id: I9a185a689745726dd79b15117892001461fa4a0d
diff --git a/mime/java/android/content/type/MimeMapImpl.java b/mime/java/android/content/type/MimeMapImpl.java
index c904ea3..3671603 100644
--- a/mime/java/android/content/type/MimeMapImpl.java
+++ b/mime/java/android/content/type/MimeMapImpl.java
@@ -21,10 +21,8 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.regex.Pattern;
 
 /**
@@ -36,96 +34,27 @@
  *
  * @hide
  */
-public class MimeMapImpl extends MimeMap {
+public class MimeMapImpl {
 
     /**
      * Creates and returns a new {@link MimeMapImpl} instance that implements.
      * Android's default mapping between MIME types and extensions.
      */
-    public static MimeMapImpl createDefaultInstance() {
+    public static MimeMap createDefaultInstance() {
         return parseFromResources("/mime.types", "/android.mime.types");
     }
 
     private static final Pattern SPLIT_PATTERN = Pattern.compile("\\s+");
 
-    /**
-     * Note: These maps only contain lowercase keys/values, regarded as the
-     * {@link #toLowerCase(String) canonical form}.
-     *
-     * <p>This is the case for both extensions and MIME types. The mime.types
-     * data file contains examples of mixed-case MIME types, but some applications
-     * use the lowercase version of these same types. RFC 2045 section 2 states
-     * that MIME types are case insensitive.
-     */
-    private final Map<String, String> mMimeTypeToExtension;
-    private final Map<String, String> mExtensionToMimeType;
-
-    public MimeMapImpl(Map<String, String> mimeTypeToExtension,
-            Map<String, String> extensionToMimeType) {
-        this.mMimeTypeToExtension = new HashMap<>(mimeTypeToExtension);
-        for (Map.Entry<String, String> entry : mimeTypeToExtension.entrySet()) {
-            checkValidMimeType(entry.getKey());
-            checkValidExtension(entry.getValue());
-        }
-        this.mExtensionToMimeType = new HashMap<>(extensionToMimeType);
-        for (Map.Entry<String, String> entry : extensionToMimeType.entrySet()) {
-            checkValidExtension(entry.getKey());
-            checkValidMimeType(entry.getValue());
-        }
-    }
-
-    private static void checkValidMimeType(String s) {
-        if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) {
-            throw new IllegalArgumentException("Invalid MIME type: " + s);
-        }
-    }
-
-    private static void checkValidExtension(String s) {
-        if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) {
-            throw new IllegalArgumentException("Invalid extension: " + s);
-        }
-    }
-
-    static MimeMapImpl parseFromResources(String... resourceNames) {
-        Map<String, String> mimeTypeToExtension = new HashMap<>();
-        Map<String, String> extensionToMimeType = new HashMap<>();
+    static MimeMap parseFromResources(String... resourceNames) {
+        MimeMap.Builder builder = MimeMap.builder();
         for (String resourceName : resourceNames) {
-            parseTypes(mimeTypeToExtension, extensionToMimeType, resourceName);
+            parseTypes(builder, resourceName);
         }
-        return new MimeMapImpl(mimeTypeToExtension, extensionToMimeType);
+        return builder.build();
     }
 
-    /**
-     * An element of a *mime.types file: A MIME type or an extension, with an optional
-     * prefix of "?" (if not overriding an earlier value).
-     */
-    private static class Element {
-        public final boolean keepExisting;
-        public final String s;
-
-        Element(boolean keepExisting, String value) {
-            this.keepExisting = keepExisting;
-            this.s = toLowerCase(value);
-            if (value.isEmpty()) {
-                throw new IllegalArgumentException();
-            }
-        }
-
-        public String toString() {
-            return keepExisting ? ("?" + s) : s;
-        }
-    }
-
-    private static String maybePut(Map<String, String> map, Element keyElement, String value) {
-        if (keyElement.keepExisting) {
-            return map.putIfAbsent(keyElement.s, value);
-        } else {
-            return map.put(keyElement.s, value);
-        }
-    }
-
-    private static void parseTypes(Map<String, String> mimeTypeToExtension,
-            Map<String, String> extensionToMimeType, String resource) {
+    private static void parseTypes(MimeMap.Builder builder, String resource) {
         try (BufferedReader r = new BufferedReader(
                 new InputStreamReader(MimeMapImpl.class.getResourceAsStream(resource)))) {
             String line;
@@ -135,60 +64,15 @@
                     line = line.substring(0, commentPos);
                 }
                 line = line.trim();
-                // The first time a MIME type is encountered it is mapped to the first extension
-                // listed in its line. The first time an extension is encountered it is mapped
-                // to the MIME type.
-                //
-                // When encountering a previously seen MIME type or extension, then by default
-                // the later ones override earlier mappings (put() semantics); however if a MIME
-                // type or extension is prefixed with '?' then any earlier mapping _from_ that
-                // MIME type / extension is kept (putIfAbsent() semantics).
-                final String[] split = SPLIT_PATTERN.split(line);
-                if (split.length <= 1) {
-                    // Need mimeType + at least one extension to make a mapping.
-                    // "mime.types" files may also contain lines with just a mimeType without
-                    // an extension but we skip them as they provide no mapping info.
+                if (line.isEmpty()) {
                     continue;
                 }
-                List<Element> lineElements = new ArrayList<>(split.length);
-                for (String s : split) {
-                    boolean keepExisting = s.startsWith("?");
-                    if (keepExisting) {
-                        s = s.substring(1);
-                    }
-                    if (s.isEmpty()) {
-                        throw new IllegalArgumentException("Invalid entry in '" + line + "'");
-                    }
-                    lineElements.add(new Element(keepExisting, s));
-                }
-
-                // MIME type -> first extension (one mapping)
-                // This will override any earlier mapping from this MIME type to another
-                // extension, unless this MIME type was prefixed with '?'.
-                Element mimeElement = lineElements.get(0);
-                List<Element> extensionElements = lineElements.subList(1, lineElements.size());
-                String firstExtension = extensionElements.get(0).s;
-                maybePut(mimeTypeToExtension, mimeElement, firstExtension);
-
-                // extension -> MIME type (one or more mappings).
-                // This will override any earlier mapping from this extension to another
-                // MIME type, unless this extension was prefixed with '?'.
-                for (Element extensionElement : extensionElements) {
-                    maybePut(extensionToMimeType, extensionElement, mimeElement.s);
-                }
+                List<String> specs = Arrays.asList(SPLIT_PATTERN.split(line));
+                builder.put(specs.get(0), specs.subList(1, specs.size()));
             }
         } catch (IOException | RuntimeException e) {
             throw new RuntimeException("Failed to parse " + resource, e);
         }
     }
 
-    @Override
-    protected String guessExtensionFromLowerCaseMimeType(String mimeType) {
-        return mMimeTypeToExtension.get(mimeType);
-    }
-
-    @Override
-    protected String guessMimeTypeFromLowerCaseExtension(String extension) {
-        return mExtensionToMimeType.get(extension);
-    }
 }