• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package libcore.content.type;
18 
19 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
20 
21 import android.annotation.SystemApi;
22 
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.Set;
31 import java.util.function.Supplier;
32 import libcore.api.CorePlatformApi;
33 import libcore.util.NonNull;
34 import libcore.util.Nullable;
35 
36 /**
37  * Maps from MIME types to file extensions and back.
38  *
39  * @hide
40  */
41 @SystemApi(client = MODULE_LIBRARIES)
42 @libcore.api.CorePlatformApi(status = CorePlatformApi.Status.STABLE)
43 public final class MimeMap {
44 
45     /**
46      * Creates a MIME type map builder.
47      *
48      * @return builder
49      *
50      * @see MimeMap.Builder
51      *
52      * @hide
53      */
54     @SystemApi(client = MODULE_LIBRARIES)
55     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
builder()56     public static @NonNull Builder builder() {
57         return new Builder();
58     }
59 
60     /**
61      * Creates a MIME type map builder with values based on {@code this} instance.
62      * This builder will contain all previously added MIMEs and extensions.
63      *
64      * @return builder
65      *
66      * @see MimeMap.Builder
67      *
68      * @hide
69      */
70     @SystemApi(client = MODULE_LIBRARIES)
71     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
buildUpon()72     public @NonNull Builder buildUpon() {
73         return new Builder(mimeToExt, extToMime);
74     }
75 
76     // Contain only lowercase, valid keys/values.
77     private final Map<String, String> mimeToExt;
78     private final Map<String, String> extToMime;
79 
80     /**
81      * A basic implementation of MimeMap used if a new default isn't explicitly
82      * {@link MimeMap#setDefaultSupplier(Supplier) installed}. Hard-codes enough
83      * mappings to satisfy libcore tests. Android framework code is expected to
84      * replace this implementation during runtime initialization.
85      */
86     private static volatile MemoizingSupplier<@NonNull MimeMap> instanceSupplier =
87             new MemoizingSupplier<>(
88                     () -> builder()
89                             .addMimeMapping("application/pdf", "pdf")
90                             .addMimeMapping("image/jpeg", "jpg")
91                             .addMimeMapping("image/x-ms-bmp", "bmp")
92                             .addMimeMapping("text/html", Arrays.asList("htm", "html"))
93                             .addMimeMapping("text/plain", Arrays.asList("text", "txt"))
94                             .addMimeMapping("text/x-java", "java")
95                             .build());
96 
MimeMap(Map<String, String> mimeToExt, Map<String, String> extToMime)97     private MimeMap(Map<String, String> mimeToExt, Map<String, String> extToMime) {
98         this.mimeToExt = Objects.requireNonNull(mimeToExt);
99         this.extToMime = Objects.requireNonNull(extToMime);
100         for (Map.Entry<String, String> entry : this.mimeToExt.entrySet()) {
101             checkValidMimeType(entry.getKey());
102             checkValidExtension(entry.getValue());
103         }
104         for (Map.Entry<String, String> entry : this.extToMime.entrySet()) {
105             checkValidExtension(entry.getKey());
106             checkValidMimeType(entry.getValue());
107         }
108     }
109 
110     /**
111      * Gets system's current default {@link MimeMap}
112      *
113      * @return The system's current default {@link MimeMap}.
114      *
115      * @hide
116      */
117     @SystemApi(client = MODULE_LIBRARIES)
118     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
getDefault()119     public static @NonNull MimeMap getDefault() {
120         return Objects.requireNonNull(instanceSupplier.get());
121     }
122 
123     /**
124      * Sets the {@link Supplier} of the {@link #getDefault() default MimeMap
125      * instance} to be used from now on.
126      *
127      * {@code mimeMapSupplier.get()} will be invoked only the first time that
128      * {@link #getDefault()} is called after this method call; that
129      * {@link MimeMap} instance is memoized such that subsequent calls to
130      * {@link #getDefault()} without an intervening call to
131      * {@link #setDefaultSupplier(Supplier)} will return that same instance
132      * without consulting {@code mimeMapSupplier} a second time.
133      *
134      * @hide
135      */
136     @SystemApi(client = MODULE_LIBRARIES)
137     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
setDefaultSupplier(@onNull Supplier<@NonNull MimeMap> mimeMapSupplier)138     public static void setDefaultSupplier(@NonNull Supplier<@NonNull MimeMap> mimeMapSupplier) {
139         instanceSupplier = new MemoizingSupplier<>(Objects.requireNonNull(mimeMapSupplier));
140     }
141 
142     /**
143      * Returns whether the given case insensitive extension has a registered MIME type.
144      *
145      * @param extension A file extension without the leading '.'
146      * @return Whether a MIME type has been registered for the given case insensitive file
147      *         extension.
148      *
149      * @hide
150      */
151     @SystemApi(client = MODULE_LIBRARIES)
152     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
hasExtension(@ullable String extension)153     public final boolean hasExtension(@Nullable String extension) {
154         return guessMimeTypeFromExtension(extension) != null;
155     }
156 
157     /**
158      * Returns the MIME type for the given case insensitive file extension, or null
159      * if the extension isn't mapped to any.
160      *
161      * @param extension A file extension without the leading '.'
162      * @return The lower-case MIME type registered for the given case insensitive file extension,
163      *         or null if there is none.
164      *
165      * @hide
166      */
167     @SystemApi(client = MODULE_LIBRARIES)
168     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
guessMimeTypeFromExtension(@ullable String extension)169     public final @Nullable String guessMimeTypeFromExtension(@Nullable String extension) {
170         if (extension == null) {
171             return null;
172         }
173         extension = toLowerCase(extension);
174         return extToMime.get(extension);
175     }
176 
177     /**
178      * Returns whether given case insensetive MIME type is mapped to a file extension.
179      *
180      * @param mimeType A MIME type (i.e. {@code "text/plain")
181      * @return Whether the given case insensitive MIME type is
182      *         {@link #guessMimeTypeFromExtension(String) mapped} to a file extension.
183      *
184      * @hide
185      */
186     @SystemApi(client = MODULE_LIBRARIES)
187     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
hasMimeType(@ullable String mimeType)188     public final boolean hasMimeType(@Nullable String mimeType) {
189         return guessExtensionFromMimeType(mimeType) != null;
190     }
191 
192     /**
193      * Returns the registered extension for the given case insensitive MIME type. Note that some
194      * MIME types map to multiple extensions. This call will return the most
195      * common extension for the given MIME type.
196      * @param mimeType A MIME type (i.e. text/plain)
197      * @return The lower-case file extension (without the leading "." that has been registered for
198      *         the given case insensitive MIME type, or null if there is none.
199      *
200      * @hide
201      */
202     @SystemApi(client = MODULE_LIBRARIES)
203     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
guessExtensionFromMimeType(@ullable String mimeType)204     public final @Nullable String guessExtensionFromMimeType(@Nullable String mimeType) {
205         if (mimeType == null) {
206             return null;
207         }
208         mimeType = toLowerCase(mimeType);
209         return mimeToExt.get(mimeType);
210     }
211 
212     /**
213      * Returns the set of MIME types that this {@link MimeMap}
214      * {@link #hasMimeType(String) maps to some extension}. Note that the
215      * reverse mapping might not exist.
216      *
217      * @return unmodifiable {@link Set} of MIME types mapped to some extension
218      *
219      * @hide
220      */
221     @SystemApi(client = MODULE_LIBRARIES)
222     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
mimeTypes()223     public @NonNull Set<String> mimeTypes() {
224         return Collections.unmodifiableSet(mimeToExt.keySet());
225     }
226 
227     /**
228      * Returns the set of extensions that this {@link MimeMap}
229      * {@link #hasExtension(String) maps to some MIME type}. Note that the
230      * reverse mapping might not exist.
231      *
232      * @return unmodifiable {@link Set} of extensions that this {@link MimeMap}
233      *         maps to some MIME type
234      *
235      * @hide
236      */
237     @SystemApi(client = MODULE_LIBRARIES)
238     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
extensions()239     public @NonNull Set<String> extensions() {
240         return Collections.unmodifiableSet(extToMime.keySet());
241     }
242 
243     /**
244      * Returns the canonical (lowercase) form of the given extension or MIME type.
245      */
toLowerCase(@onNull String s)246     private static @NonNull String toLowerCase(@NonNull String s) {
247         return s.toLowerCase(Locale.ROOT);
248     }
249 
250     private volatile int hashCode = 0;
251 
252     /**
253      *
254      * @hide
255      */
256     @Override
hashCode()257     public int hashCode() {
258         if (hashCode == 0) { // potentially uninitialized
259             hashCode = mimeToExt.hashCode() + 31 * extToMime.hashCode();
260         }
261         return hashCode;
262     }
263 
264     /**
265      *
266      * @hide
267      */
268     @Override
equals(Object obj)269     public boolean equals(Object obj) {
270         if (!(obj instanceof MimeMap)) {
271             return false;
272         }
273         MimeMap that = (MimeMap) obj;
274         if (hashCode() != that.hashCode()) {
275             return false;
276         }
277         return mimeToExt.equals(that.mimeToExt) && extToMime.equals(that.extToMime);
278     }
279 
280     /**
281      *
282      * @hide
283      */
284     @Override
toString()285     public String toString() {
286         return "MimeMap[" + mimeToExt + ", " + extToMime + "]";
287     }
288 
289     /**
290      * A builder for mapping of MIME types to extensions and back.
291      * Use {@link #addMimeMapping(String, List)} and {@link #addMimeMapping(String, String)} to add
292      * mapping entries and build final {@link MimeMap} with {@link #build()}.
293      *
294      * @hide
295      */
296     @SystemApi(client = MODULE_LIBRARIES)
297     @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
298     public static final class Builder {
299         private final Map<String, String> mimeToExt;
300         private final Map<String, String> extToMime;
301 
302         /**
303          * Constructs a Builder that starts with an empty mapping.
304          */
Builder()305         Builder() {
306             this.mimeToExt = new HashMap<>();
307             this.extToMime = new HashMap<>();
308         }
309 
310         /**
311          * Constructs a Builder that starts with the given mapping.
312          * @param mimeToExt
313          * @param extToMime
314          */
Builder(Map<String, String> mimeToExt, Map<String, String> extToMime)315         Builder(Map<String, String> mimeToExt, Map<String, String> extToMime) {
316             this.mimeToExt = new HashMap<>(mimeToExt);
317             this.extToMime = new HashMap<>(extToMime);
318         }
319 
320         /**
321          * An element of a *mime.types file.
322          */
323         static class Element {
324             final String mimeOrExt;
325             final boolean keepExisting;
326 
327             /**
328              * @param spec A MIME type or an extension, with an optional
329              *        prefix of "?" (if not overriding an earlier value).
330              * @param isMimeSpec whether this Element denotes a MIME type (as opposed to an
331              *        extension).
332              */
Element(String spec, boolean isMimeSpec)333             private Element(String spec, boolean isMimeSpec) {
334                 if (spec.startsWith("?")) {
335                     this.keepExisting = true;
336                     this.mimeOrExt = toLowerCase(spec.substring(1));
337                 } else {
338                     this.keepExisting = false;
339                     this.mimeOrExt = toLowerCase(spec);
340                 }
341                 if (isMimeSpec) {
342                     checkValidMimeType(mimeOrExt);
343                 } else {
344                     checkValidExtension(mimeOrExt);
345                 }
346             }
347 
ofMimeSpec(String s)348             public static Element ofMimeSpec(String s) { return new Element(s, true); }
ofExtensionSpec(String s)349             public static Element ofExtensionSpec(String s) { return new Element(s, false); }
350         }
351 
maybePut(Map<String, String> map, Element keyElement, String value)352         private static String maybePut(Map<String, String> map, Element keyElement, String value) {
353             if (keyElement.keepExisting) {
354                 return map.putIfAbsent(keyElement.mimeOrExt, value);
355             } else {
356                 return map.put(keyElement.mimeOrExt, value);
357             }
358         }
359 
360         /**
361          * Puts the mapping {@quote mimeType -> first extension}, and also the mappings
362          * {@quote extension -> mimeType} for each given extension.
363          *
364          * The values passed to this function are carry an optional  prefix of {@quote "?"}
365          * which is stripped off in any case before any such key/value is added to a mapping.
366          * The prefix {@quote "?"} controls whether the mapping <i>from></i> the corresponding
367          * value is added via {@link Map#putIfAbsent} semantics ({@quote "?"}
368          * present) vs. {@link Map#put} semantics ({@quote "?" absent}),
369          *
370          * For example, {@code put("text/html", "?htm", "html")} would add the following
371          * mappings:
372          * <ol>
373          *   <li>MIME type "text/html" -> extension "htm", overwriting any earlier mapping
374          *       from MIME type "text/html" that might already have existed.</li>
375          *   <li>extension "htm" -> MIME type "text/html", but only if no earlier mapping
376          *       for extension "htm" existed.</li>
377          *   <li>extension "html" -> MIME type "text/html", overwriting any earlier mapping
378          *       from extension "html" that might already have existed.</li>
379          * </ol>
380          * {@code put("?text/html", "?htm", "html")} would have the same effect except
381          * that an earlier mapping from MIME type {@code "text/html"} would not be
382          * overwritten.
383          *
384          * @param mimeSpec A MIME type carrying an optional prefix of {@code "?"}. If present,
385          *                 the {@code "?"} is stripped off and mapping for the resulting MIME
386          *                 type is only added to the map if no mapping had yet existed for that
387          *                 type.
388          * @param extensionSpecs The extensions from which to add mappings back to
389          *                 the {@code "?"} is stripped off and mapping for the resulting extension
390          *                 is only added to the map if no mapping had yet existed for that
391          *                 extension.
392          *                 If {@code extensionSpecs} is empty, then calling this method has no
393          *                 effect on the mapping that is being constructed.
394          * @throws IllegalArgumentException if {@code mimeSpec} or any of the {@code extensionSpecs}
395          *                 are invalid (null, empty, contain ' ', or '?' after an initial '?' has
396          *                 been stripped off).
397          * @return This builder.
398          *
399          * @hide
400          */
401         @SystemApi(client = MODULE_LIBRARIES)
402         @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
addMimeMapping(@onNull String mimeSpec, @NonNull List<@NonNull String> extensionSpecs)403         public @NonNull Builder addMimeMapping(@NonNull String mimeSpec, @NonNull List<@NonNull String> extensionSpecs)
404         {
405             Element mimeElement = Element.ofMimeSpec(mimeSpec); // validate mimeSpec unconditionally
406             if (extensionSpecs.isEmpty()) {
407                 return this;
408             }
409             Element firstExtensionElement = Element.ofExtensionSpec(extensionSpecs.get(0));
410             maybePut(mimeToExt, mimeElement, firstExtensionElement.mimeOrExt);
411             maybePut(extToMime, firstExtensionElement, mimeElement.mimeOrExt);
412             for (String spec : extensionSpecs.subList(1, extensionSpecs.size())) {
413                 Element element = Element.ofExtensionSpec(spec);
414                 maybePut(extToMime, element, mimeElement.mimeOrExt);
415             }
416             return this;
417         }
418 
419         /**
420          * Convenience method.
421          *
422          * @hide
423          */
addMimeMapping(@onNull String mimeSpec, @NonNull String extensionSpec)424         public @NonNull Builder addMimeMapping(@NonNull String mimeSpec, @NonNull String extensionSpec) {
425             return addMimeMapping(mimeSpec, Collections.singletonList(extensionSpec));
426         }
427 
428         /**
429          * Builds {@link MimeMap} containing all added MIME mappings.
430          *
431          * @return {@link MimeMap} containing previously added MIME mapping entries
432          *
433          * @hide
434          */
435         @SystemApi(client = MODULE_LIBRARIES)
436         @CorePlatformApi(status = CorePlatformApi.Status.STABLE)
build()437         public @NonNull MimeMap build() {
438             return new MimeMap(mimeToExt, extToMime);
439         }
440 
441         /**
442          *
443          * @hide
444          */
445         @Override
toString()446         public String toString() {
447             return "MimeMap.Builder[" + mimeToExt + ", " + extToMime + "]";
448         }
449     }
450 
isValidMimeTypeOrExtension(String s)451     private static boolean isValidMimeTypeOrExtension(String s) {
452         return s != null
453                 && !s.isEmpty()
454                 && s.indexOf('?') < 0
455                 && s.indexOf(' ') < 0
456                 && s.indexOf('\t') < 0
457                 && s.equals(toLowerCase(s));
458     }
459 
checkValidMimeType(String s)460     static void checkValidMimeType(String s) {
461         if (!isValidMimeTypeOrExtension(s) || s.indexOf('/') < 0) {
462             throw new IllegalArgumentException("Invalid MIME type: " + s);
463         }
464     }
465 
checkValidExtension(String s)466     static void checkValidExtension(String s) {
467         if (!isValidMimeTypeOrExtension(s) || s.indexOf('/') >= 0) {
468             throw new IllegalArgumentException("Invalid extension: " + s);
469         }
470     }
471 
472     private static final class MemoizingSupplier<T> implements Supplier<T> {
473         private volatile Supplier<T> mDelegate;
474         private volatile T mInstance;
475         private volatile boolean mInitialized = false;
476 
MemoizingSupplier(Supplier<T> delegate)477         public MemoizingSupplier(Supplier<T> delegate) {
478             this.mDelegate = delegate;
479         }
480 
481         @Override
get()482         public T get() {
483             if (!mInitialized) {
484                 synchronized (this) {
485                     if (!mInitialized) {
486                         mInstance = mDelegate.get();
487                         mDelegate = null;
488                         mInitialized = true;
489                     }
490                 }
491             }
492             return mInstance;
493         }
494     }
495 }
496