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