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