summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--luni/src/main/java/libcore/net/MimeMap.java142
-rw-r--r--luni/src/main/java/libcore/net/MimeMapImpl.java140
-rw-r--r--luni/src/main/java/libcore/net/MimeUtils.java117
-rw-r--r--luni/src/test/java/libcore/libcore/net/MimeMapTest.java134
-rw-r--r--non_openjdk_java_files.bp2
5 files changed, 423 insertions, 112 deletions
diff --git a/luni/src/main/java/libcore/net/MimeMap.java b/luni/src/main/java/libcore/net/MimeMap.java
new file mode 100644
index 0000000000..1db182414f
--- /dev/null
+++ b/luni/src/main/java/libcore/net/MimeMap.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 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 libcore.net;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import libcore.util.NonNull;
+import libcore.util.Nullable;
+
+/**
+ * Maps from MIME types to file extensions and back.
+ * @hide
+ */
+public abstract class MimeMap {
+ private static AtomicReference<MimeMap> defaultHolder = new AtomicReference<>(
+ MimeMapImpl.parseFromResources("mime.types", "android.mime.types"));
+
+ /**
+ * @return The system's current default {@link MimeMap}.
+ */
+ public static @NonNull MimeMap getDefault() {
+ return defaultHolder.get();
+ }
+
+ /**
+ * Atomically sets the system's default {@link MimeMap} to be {@code update} if the
+ * current value {@code == expect}.
+ *
+ * @param expect the expected current default {@link MimeMap}; must not be null.
+ * @param update the new default {@link MimeMap} to set; must not be null.
+ * @return whether the update was successful.
+ */
+ public static boolean compareAndSetDefault(@NonNull MimeMap expect, @NonNull MimeMap update) {
+ Objects.requireNonNull(expect);
+ Objects.requireNonNull(update);
+ return defaultHolder.compareAndSet(expect, update);
+ }
+
+ /**
+ * Returns whether the given case insensitive extension has a registered MIME type.
+ *
+ * @param extension A file extension without the leading '.'
+ * @return Whether a MIME type has been registered for the given case insensitive file
+ * extension.
+ */
+ public final boolean hasExtension(@Nullable String extension) {
+ return guessMimeTypeFromExtension(extension) != null;
+ }
+
+ /**
+ * Returns the MIME type for the given case insensitive file extension.
+ * If {@code extension} is {@code null} or {@code ""}, then this method always returns
+ * {@code null}. Otherwise, it delegates to
+ * {@link #guessMimeTypeFromLowerCaseExtension(String)}.
+ *
+ * @param extension A file extension without the leading '.'
+ * @return The lower-case MIME type registered for the given case insensitive file extension,
+ * or null if there is none.
+ */
+ public final @Nullable String guessMimeTypeFromExtension(@Nullable String extension) {
+ if (isNullOrEmpty(extension)) {
+ return null;
+ }
+ extension = toLowerCase(extension);
+ String result = guessMimeTypeFromLowerCaseExtension(extension);
+ if (result != null) {
+ result = toLowerCase(result);
+ }
+ return result;
+ }
+
+ /**
+ * @param extension A non-null, non-empty, lowercase file extension.
+ * @return The MIME type registered for the given file extension, or null if there is none.
+ */
+ protected abstract @Nullable String guessMimeTypeFromLowerCaseExtension(
+ @NonNull String extension);
+
+ /**
+ * @param mimeType A MIME type (i.e. {@code "text/plain")
+ * @return Whether the given case insensitive MIME type is
+ * {@link #guessMimeTypeFromExtension(String) mapped} to a file extension.
+ */
+ public final boolean hasMimeType(@Nullable String mimeType) {
+ return guessExtensionFromMimeType(mimeType) != null;
+ }
+
+ /**
+ * Returns the registered extension for the given case insensitive MIME type. Note that some
+ * MIME types map to multiple extensions. This call will return the most
+ * common extension for the given MIME type.
+ * @param mimeType A MIME type (i.e. text/plain)
+ * @return The lower-case file extension (without the leading "." that has been registered for
+ * the given case insensitive MIME type, or null if there is none.
+ */
+ public final @Nullable String guessExtensionFromMimeType(@Nullable String mimeType) {
+ if (isNullOrEmpty(mimeType)) {
+ return null;
+ }
+ mimeType = toLowerCase(mimeType);
+ String result = guessExtensionFromLowerCaseMimeType(mimeType);
+ if (result != null) {
+ result = toLowerCase(result);
+ }
+ return result;
+ }
+
+ /**
+ * @param mimeType A non-null, non-empty, lowercase file extension.
+ * @return The file extension (without the leading ".") for the given mimeType, or null if
+ * there is none.
+ */
+ protected abstract @Nullable String guessExtensionFromLowerCaseMimeType(
+ @NonNull String mimeType);
+
+ /**
+ * Returns the canonical (lowercase) form of the given extension or MIME type.
+ */
+ static @NonNull String toLowerCase(@NonNull String s) {
+ return s.toLowerCase(Locale.ROOT);
+ }
+
+ static boolean isNullOrEmpty(@Nullable String s) {
+ return s == null || s.isEmpty();
+ }
+
+}
diff --git a/luni/src/main/java/libcore/net/MimeMapImpl.java b/luni/src/main/java/libcore/net/MimeMapImpl.java
new file mode 100644
index 0000000000..086e5b9059
--- /dev/null
+++ b/luni/src/main/java/libcore/net/MimeMapImpl.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 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 libcore.net;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+class MimeMapImpl extends MimeMap {
+
+ private static final Pattern splitPattern = 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> mimeTypeToExtension;
+ private final Map<String, String> extensionToMimeType;
+
+ public MimeMapImpl(Map<String, String> mimeTypeToExtension,
+ Map<String, String> extensionToMimeType) {
+ this.mimeTypeToExtension = new HashMap<>(mimeTypeToExtension);
+ for (Map.Entry<String, String> entry : mimeTypeToExtension.entrySet()) {
+ checkValidMimeType(entry.getKey());
+ checkValidExtension(entry.getValue());
+ }
+ this.extensionToMimeType = 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 (isNullOrEmpty(s) || !s.equals(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<>();
+ for (String resourceName : resourceNames) {
+ parseTypes(mimeTypeToExtension, extensionToMimeType, resourceName);
+ }
+ return new MimeMapImpl(mimeTypeToExtension, extensionToMimeType);
+ }
+
+ private static void parseTypes(Map<String, String> mimeTypeToExtension,
+ Map<String, String> extensionToMimeType, String resource) {
+ try (BufferedReader r = new BufferedReader(
+ new InputStreamReader(MimeMap.class.getResourceAsStream(resource)))) {
+ String line;
+ while ((line = r.readLine()) != null) {
+ int commentPos = line.indexOf('#');
+ if (commentPos >= 0) {
+ line = line.substring(0, commentPos);
+ }
+ line = line.trim();
+ if (line.equals("")) {
+ continue;
+ }
+
+ final String[] split = splitPattern.split(line);
+ final String mimeType = toLowerCase(split[0]);
+ if (isNullOrEmpty(mimeType)) {
+ throw new IllegalArgumentException(
+ "Invalid mimeType " + mimeType + " in: " + line);
+ }
+ for (int i = 1; i < split.length; i++) {
+ String extension = toLowerCase(split[i]);
+ if (isNullOrEmpty(extension)) {
+ throw new IllegalArgumentException(
+ "Invalid extension " + extension + " in: " + line);
+ }
+
+ // Normally the first MIME type definition wins, and the
+ // last extension definition wins. However, a file can
+ // override a MIME type definition by adding the "!" suffix
+ // to an extension.
+
+ if (extension.endsWith("!")) {
+ extension = extension.substring(0, extension.length() - 1);
+
+ // Overriding MIME definition wins
+ mimeTypeToExtension.put(mimeType, extension);
+ } else {
+ // First MIME definition wins
+ if (!mimeTypeToExtension.containsKey(mimeType)) {
+ mimeTypeToExtension.put(mimeType, extension);
+ }
+ }
+
+ // Last extension definition wins
+ extensionToMimeType.put(extension, mimeType);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to parse " + resource, e);
+ }
+ }
+
+ @Override
+ protected String guessExtensionFromLowerCaseMimeType(String mimeType) {
+ return mimeTypeToExtension.get(mimeType);
+ }
+
+ @Override
+ protected String guessMimeTypeFromLowerCaseExtension(String extension) {
+ return extensionToMimeType.get(extension);
+ }
+}
diff --git a/luni/src/main/java/libcore/net/MimeUtils.java b/luni/src/main/java/libcore/net/MimeUtils.java
index 8cd0147c12..a9dd00a237 100644
--- a/luni/src/main/java/libcore/net/MimeUtils.java
+++ b/luni/src/main/java/libcore/net/MimeUtils.java
@@ -17,13 +17,7 @@
package libcore.net;
import dalvik.annotation.compat.UnsupportedAppUsage;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.regex.Pattern;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Utilities for dealing with MIME types.
@@ -33,105 +27,12 @@ import java.util.regex.Pattern;
@libcore.api.CorePlatformApi
public final class MimeUtils {
- private static final Pattern splitPattern = Pattern.compile("\\s+");
-
- /**
- * Note: These maps only contain lowercase keys/values, regarded as the
- * {@link #canonicalize(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 static final Map<String, String> mimeTypeToExtensionMap = new HashMap<>();
- private static final Map<String, String> extensionToMimeTypeMap = new HashMap<>();
-
- static {
- parseTypes("mime.types");
- parseTypes("android.mime.types");
- }
-
- private static void parseTypes(String resource) {
- try (BufferedReader r = new BufferedReader(
- new InputStreamReader(MimeUtils.class.getResourceAsStream(resource)))) {
- String line;
- while ((line = r.readLine()) != null) {
- int commentPos = line.indexOf('#');
- if (commentPos >= 0) {
- line = line.substring(0, commentPos);
- }
- line = line.trim();
- if (line.equals("")) {
- continue;
- }
-
- final String[] split = splitPattern.split(line);
- final String mimeType = canonicalize(split[0]);
- if (!allowedInMap(mimeType)) {
- throw new IllegalArgumentException(
- "Invalid mimeType " + mimeType + " in: " + line);
- }
- for (int i = 1; i < split.length; i++) {
- String extension = canonicalize(split[i]);
- if (!allowedInMap(extension)) {
- throw new IllegalArgumentException(
- "Invalid extension " + extension + " in: " + line);
- }
-
- // Normally the first MIME type definition wins, and the
- // last extension definition wins. However, a file can
- // override a MIME type definition by adding the "!" suffix
- // to an extension.
-
- if (extension.endsWith("!")) {
- extension = extension.substring(0, extension.length() - 1);
-
- // Overriding MIME definition wins
- mimeTypeToExtensionMap.put(mimeType, extension);
- } else {
- // First MIME definition wins
- if (!mimeTypeToExtensionMap.containsKey(mimeType)) {
- mimeTypeToExtensionMap.put(mimeType, extension);
- }
- }
-
- // Last extension definition wins
- extensionToMimeTypeMap.put(extension, mimeType);
- }
- }
- } catch (IOException e) {
- throw new RuntimeException("Failed to parse " + resource, e);
- }
- }
-
private MimeUtils() {
}
- /**
- * Returns the canonical (lowercase) form of the given extension or MIME type.
- */
- private static String canonicalize(String s) {
- return s.toLowerCase(Locale.ROOT);
- }
-
- /**
- * Checks whether the given extension or MIME type might be valid and
- * therefore may appear in the mimeType <-> extension maps.
- */
- private static boolean allowedInMap(String s) {
- return s != null && !s.isEmpty();
- }
-
- /**
- * Returns true if the given case insensitive MIME type has an entry in the map.
- * @param mimeType A MIME type (i.e. text/plain)
- * @return True if a extension has been registered for
- * the given case insensitive MIME type.
- */
@libcore.api.CorePlatformApi
public static boolean hasMimeType(String mimeType) {
- return (guessExtensionFromMimeType(mimeType) != null);
+ return MimeMap.getDefault().hasMimeType(mimeType);
}
/**
@@ -143,11 +44,7 @@ public final class MimeUtils {
@UnsupportedAppUsage
@libcore.api.CorePlatformApi
public static String guessMimeTypeFromExtension(String extension) {
- if (!allowedInMap(extension)) {
- return null;
- }
- extension = canonicalize(extension);
- return extensionToMimeTypeMap.get(extension);
+ return MimeMap.getDefault().guessMimeTypeFromExtension(extension);
}
/**
@@ -158,7 +55,7 @@ public final class MimeUtils {
*/
@libcore.api.CorePlatformApi
public static boolean hasExtension(String extension) {
- return (guessMimeTypeFromExtension(extension) != null);
+ return MimeMap.getDefault().hasExtension(extension);
}
/**
@@ -172,10 +69,6 @@ public final class MimeUtils {
@UnsupportedAppUsage
@libcore.api.CorePlatformApi
public static String guessExtensionFromMimeType(String mimeType) {
- if (!allowedInMap(mimeType)) {
- return null;
- }
- mimeType = canonicalize(mimeType);
- return mimeTypeToExtensionMap.get(mimeType);
+ return MimeMap.getDefault().guessExtensionFromMimeType(mimeType);
}
}
diff --git a/luni/src/test/java/libcore/libcore/net/MimeMapTest.java b/luni/src/test/java/libcore/libcore/net/MimeMapTest.java
new file mode 100644
index 0000000000..59980929d2
--- /dev/null
+++ b/luni/src/test/java/libcore/libcore/net/MimeMapTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 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 libcore.libcore.net;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+import libcore.net.MimeMap;
+import libcore.util.NonNull;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+public class MimeMapTest {
+
+ /** Exposes {@link MimeMap}'s protected methods publicly so that mock calls can be verified. */
+ public static abstract class TestMimeMap extends MimeMap {
+ @Override
+ public abstract String guessMimeTypeFromLowerCaseExtension(@NonNull String extension);
+
+ @Override
+ public abstract String guessExtensionFromLowerCaseMimeType(@NonNull String mimeType);
+ }
+
+ private TestMimeMap mimeMap;
+
+
+ @Before public void setUp() {
+ mimeMap = mock(TestMimeMap.class);
+ }
+
+ @After public void tearDown() {
+ mimeMap = null;
+ }
+
+ @Test public void invalidExtension() {
+ assertNull(mimeMap.guessMimeTypeFromExtension(null));
+ assertNull(mimeMap.guessMimeTypeFromExtension(""));
+ assertFalse(mimeMap.hasExtension(null));
+ assertFalse(mimeMap.hasExtension(""));
+
+ verify(mimeMap, never()).guessExtensionFromLowerCaseMimeType(anyString());
+ verify(mimeMap, never()).guessMimeTypeFromLowerCaseExtension(anyString());
+
+ }
+
+ @Test public void invalidMimeType() {
+ assertNull(mimeMap.guessExtensionFromMimeType(null));
+ assertNull(mimeMap.guessExtensionFromMimeType(""));
+ assertFalse(mimeMap.hasMimeType(null));
+ assertFalse(mimeMap.hasMimeType(""));
+
+ verify(mimeMap, never()).guessExtensionFromLowerCaseMimeType(anyString());
+ verify(mimeMap, never()).guessMimeTypeFromLowerCaseExtension(anyString());
+ }
+
+ @Test public void caseNormalization() {
+ when(mimeMap.guessExtensionFromLowerCaseMimeType("application/msword")).thenReturn("DoC");
+ when(mimeMap.guessMimeTypeFromLowerCaseExtension("doc")).thenReturn("APPLication/msWORD");
+
+ assertEquals("application/msword", mimeMap.guessMimeTypeFromExtension("dOc"));
+ assertEquals("doc", mimeMap.guessExtensionFromMimeType("appliCATion/mSWOrd"));
+ }
+
+ @Test public void unmapped() {
+ assertNull(mimeMap.guessExtensionFromMimeType("test/mime"));
+ assertFalse(mimeMap.hasMimeType("test/mime"));
+
+ assertNull(mimeMap.guessMimeTypeFromExtension("test"));
+ assertFalse(mimeMap.hasExtension("test"));
+
+ verify(mimeMap, times(2)).guessExtensionFromLowerCaseMimeType("test/mime");
+ verify(mimeMap, times(2)).guessMimeTypeFromLowerCaseExtension("test");
+ }
+
+ @Test public void compareAndSetDefault() {
+ MimeMap otherMimeMap = mock(TestMimeMap.class);
+ MimeMap defaultMimeMap = MimeMap.getDefault();
+ assertTrue(MimeMap.compareAndSetDefault(defaultMimeMap, mimeMap));
+ try {
+ assertNotNull(defaultMimeMap);
+ assertEquals(mimeMap, MimeMap.getDefault());
+ assertFalse(MimeMap.compareAndSetDefault(defaultMimeMap, otherMimeMap));
+ } finally {
+ assertTrue(MimeMap.compareAndSetDefault(mimeMap, defaultMimeMap));
+ }
+ }
+
+ @Test public void compareAndSetDefault_null() {
+ MimeMap defaultMimeMap = MimeMap.getDefault();
+ try {
+ MimeMap.compareAndSetDefault(defaultMimeMap, null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+
+ try {
+ MimeMap.compareAndSetDefault(null, defaultMimeMap);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+
+ // For comparison, this does not throw (but has no effect):
+ MimeMap.compareAndSetDefault(defaultMimeMap, defaultMimeMap);
+ assertEquals(defaultMimeMap, MimeMap.getDefault());
+ }
+
+}
diff --git a/non_openjdk_java_files.bp b/non_openjdk_java_files.bp
index 3e4083fdef..9628017ad0 100644
--- a/non_openjdk_java_files.bp
+++ b/non_openjdk_java_files.bp
@@ -176,6 +176,8 @@ filegroup {
"luni/src/main/java/libcore/io/Os.java",
"luni/src/main/java/libcore/io/Streams.java",
"luni/src/main/java/libcore/net/InetAddressUtils.java",
+ "luni/src/main/java/libcore/net/MimeMap.java",
+ "luni/src/main/java/libcore/net/MimeMapImpl.java",
"luni/src/main/java/libcore/net/MimeUtils.java",
"luni/src/main/java/libcore/net/NetworkSecurityPolicy.java",
"luni/src/main/java/libcore/net/event/NetworkEventDispatcher.java",