1 /* 2 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package com.sun.media.jfxmedia.locator; 26 27 import com.sun.media.jfxmedia.MediaException; 28 import com.sun.media.jfxmedia.MediaManager; 29 import com.sun.media.jfxmedia.logging.Logger; 30 import com.sun.media.jfxmediaimpl.HostUtils; 31 import com.sun.media.jfxmediaimpl.MediaUtils; 32 import java.io.FileNotFoundException; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.lang.reflect.InvocationTargetException; 36 import java.lang.reflect.Method; 37 import java.net.HttpURLConnection; 38 import java.net.JarURLConnection; 39 import java.net.MalformedURLException; 40 import java.net.URI; 41 import java.net.URISyntaxException; 42 import java.net.URL; 43 import java.net.URLConnection; 44 import java.nio.ByteBuffer; 45 import java.security.AccessController; 46 import java.security.PrivilegedAction; 47 import java.util.Map; 48 import java.util.TreeMap; 49 import java.util.concurrent.CountDownLatch; 50 51 /** 52 * A 53 * <code>Locator</code> which refers to a 54 * <code>URI</code>. 55 */ 56 public class Locator { 57 58 /** 59 * The content type used if no more specific one may be derived. 60 */ 61 public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; 62 /** 63 * The number of times to attempt to open a URL connection to test the URI. 64 */ 65 private static final int MAX_CONNECTION_ATTEMPTS = 5; 66 /** 67 * The number of milliseconds between attempts to open a URL connection. 68 */ 69 private static final long CONNECTION_RETRY_INTERVAL = 1000L; 70 /** 71 * The content type of the media content. 72 */ 73 protected String contentType = DEFAULT_CONTENT_TYPE; 74 /** 75 * A hint for the internal player. 76 */ 77 protected long contentLength = -1; //Used as a hint for the native layer 78 /** 79 * The URI source. 80 */ 81 protected URI uri; 82 /** 83 * Properties to be associated with the connection made to the URI. The 84 * significance of the properties depends on the URI protocol and type of 85 * media source. 86 */ 87 private Map<String, Object> connectionProperties; 88 /** 89 * Mutex for connectionProperties; 90 */ 91 private final Object propertyLock = new Object(); 92 93 /* 94 * These variables will be initialized by constructor and used by init() 95 */ 96 private String uriString = null; 97 private String scheme = null; 98 private String protocol = null; 99 100 /* 101 * if cached, we store a hard reference to keep it alive 102 */ 103 private LocatorCache.CacheReference cacheEntry = null; 104 105 /* 106 * True if init(), getContentLength() and getContentType() can block; false 107 * otherwise. 108 */ 109 private boolean canBlock = false; 110 111 /* 112 * Used to block getContentLength() and getContentType(). 113 */ 114 private CountDownLatch readySignal = new CountDownLatch(1); 115 116 /** 117 * iOS only: determines if the given URL points to the iPod library 118 */ 119 private boolean isIpod; 120 121 /** 122 * Holds connection and response code returned from getConnection() 123 */ 124 private static class LocatorConnection { 125 126 public HttpURLConnection connection = null; 127 public int responseCode = HttpURLConnection.HTTP_OK; 128 } 129 130 private LocatorConnection getConnection(URI uri, String requestMethod) 131 throws MalformedURLException, IOException { 132 133 // Check ability to connect. 134 LocatorConnection locatorConnection = new LocatorConnection(); 135 HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); 136 connection.setRequestMethod(requestMethod); 137 138 // Set request headers. 139 synchronized (propertyLock) { 140 if (connectionProperties != null) { 141 for (String key : connectionProperties.keySet()) { 142 Object value = connectionProperties.get(key); 143 if (value instanceof String) { 144 connection.setRequestProperty(key, (String) value); 145 } 146 } 147 } 148 } 149 150 // Store response code so we can get more information about 151 // returning connection. 152 locatorConnection.responseCode = connection.getResponseCode(); 153 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { 154 locatorConnection.connection = connection; 155 } else { 156 closeConnection(connection); 157 locatorConnection.connection = null; 158 } 159 return locatorConnection; 160 } 161 162 private static long getContentLengthLong(URLConnection connection) { 163 Method method = AccessController.doPrivileged((PrivilegedAction<Method>) () -> { 164 try { 165 return connection.getClass().getMethod("getContentLengthLong"); 166 } catch (NoSuchMethodException ex) { 167 return null; 168 } 169 }); 170 171 try { 172 if (method != null) { 173 return (long) method.invoke(connection); 174 } else { 175 return connection.getContentLength(); 176 } 177 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { 178 return -1; 179 } 180 } 181 182 /** 183 * Constructs an object representing a media source. 184 * 185 * @param uri The URI source. 186 * @throws NullPointerException if 187 * <code>uri</code> is 188 * <code>null</code>. 189 * @throws IllegalArgumentException if the URI's scheme is 190 * <code>null</code>. 191 * @throws URISyntaxException if the supplied URI requires some further 192 * manipulation in order to be used and this procedure fails to produce a 193 * usable URI. 194 * @throws IllegalArgumentException if the URI is a Jar URL as described in 195 * {@link JarURLConnection https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html}, 196 * and the scheme of the URL after removing the leading four characters is 197 * <code>null</code>. 198 * @throws UnsupportedOperationException if the URI's protocol is 199 * unsupported. 200 */ 201 public Locator(URI uri) throws URISyntaxException { 202 // Check for NULL parameter. 203 if (uri == null) { 204 throw new NullPointerException("uri == null!"); 205 } 206 207 // Get the scheme part. 208 uriString = uri.toASCIIString(); 209 scheme = uri.getScheme(); 210 if (scheme == null) { 211 throw new IllegalArgumentException("uri.getScheme() == null! uri == '" + uri + "'"); 212 } 213 scheme = scheme.toLowerCase(); 214 215 // Get the protocol. 216 if (scheme.equals("jar")) { 217 URI subURI = new URI(uriString.substring(4)); 218 protocol = subURI.getScheme(); 219 if (protocol == null) { 220 throw new IllegalArgumentException("uri.getScheme() == null! subURI == '" + subURI + "'"); 221 } 222 protocol = protocol.toLowerCase(); 223 } else { 224 protocol = scheme; // scheme is already lower case. 225 } 226 227 if (HostUtils.isIOS() && protocol.equals("ipod-library")) { 228 isIpod = true; 229 } 230 231 // Verify the protocol is supported. 232 if (!isIpod && !protocol.equals("file") && !protocol.equals("http")) { 233 throw new UnsupportedOperationException("Unsupported protocol \"" + protocol + "\""); 234 } 235 236 // Check if we can block 237 if (protocol.equals("http")) { 238 canBlock = true; 239 } 240 241 // Set instance variable. 242 this.uri = uri; 243 } 244 245 private InputStream getInputStream(URI uri) 246 throws MalformedURLException, IOException { 247 URL url = uri.toURL(); 248 URLConnection connection = url.openConnection(); 249 250 // Set request headers. 251 synchronized (propertyLock) { 252 if (connectionProperties != null) { 253 for (String key : connectionProperties.keySet()) { 254 Object value = connectionProperties.get(key); 255 if (value instanceof String) { 256 connection.setRequestProperty(key, (String) value); 257 } 258 } 259 } 260 } 261 262 InputStream inputStream = url.openStream(); 263 contentLength = getContentLengthLong(connection); 264 return inputStream; 265 } 266 267 /** 268 * Tell this Locator to preload the media into memory, if it hasn't been 269 * already. 270 */ 271 public void cacheMedia() { 272 LocatorCache.CacheReference ref = LocatorCache.locatorCache().fetchURICache(uri); 273 if (null == ref) { 274 ByteBuffer cacheBuffer; 275 276 // not cached, load it 277 InputStream is; 278 try { 279 is = getInputStream(uri); 280 } catch (Throwable t) { 281 return; // just bail 282 } 283 284 // contentLength is set now, so we can go ahead and allocate 285 cacheBuffer = ByteBuffer.allocateDirect((int) contentLength); 286 byte[] readBuf = new byte[8192]; 287 int total = 0; 288 int count; 289 while (total < contentLength) { 290 try { 291 count = is.read(readBuf); 292 } catch (IOException ioe) { 293 try { 294 is.close(); 295 } catch (Throwable t) { 296 } 297 if (Logger.canLog(Logger.DEBUG)) { 298 Logger.logMsg(Logger.DEBUG, "IOException trying to preload media: " + ioe); 299 } 300 return; 301 } 302 303 if (count == -1) { 304 break; // EOS 305 } 306 307 cacheBuffer.put(readBuf, 0, count); 308 } 309 310 try { 311 is.close(); 312 } catch (Throwable t) { 313 } 314 315 cacheEntry = LocatorCache.locatorCache().registerURICache(uri, cacheBuffer, contentType); 316 canBlock = false; 317 } 318 } 319 320 /* 321 * True if init() can block; false otherwise. 322 */ 323 public boolean canBlock() { 324 return canBlock; 325 } 326 327 /* 328 * Initialize locator. Use canBlock() to determine if init() can block. 329 * 330 * @throws URISyntaxException if the supplied URI requires some further 331 * manipulation in order to be used and this procedure fails to produce a 332 * usable URI. @throws IOExceptions if a stream cannot be opened over a 333 * connection of the corresponding URL. @throws MediaException if the 334 * content type of the media is not supported. @throws FileNotFoundException 335 * if the media is not available. 336 */ 337 public void init() throws URISyntaxException, IOException, FileNotFoundException { 338 try { 339 // Ensure the correct number of '/'s follows the ':'. 340 int firstSlash = uriString.indexOf("/"); 341 if (firstSlash != -1 && uriString.charAt(firstSlash + 1) != '/') { 342 // Only one '/' after the ':'. 343 if (protocol.equals("file")) { 344 // Map file:/somepath to file:///somepath 345 uriString = uriString.replaceFirst("/", "///"); 346 } else if (protocol.equals("http")) { 347 // Map http:/somepath to http://somepath 348 uriString = uriString.replaceFirst("/", "//"); 349 } 350 } 351 352 // On non-Windows systems, replace "/~/" with home directory path + "/". 353 if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1 354 && protocol.equals("file")) { 355 int index = uriString.indexOf("/~/"); 356 if (index != -1) { 357 uriString = uriString.substring(0, index) 358 + System.getProperty("user.home") 359 + uriString.substring(index + 2); 360 } 361 } 362 363 // Recreate the URI if needed 364 uri = new URI(uriString); 365 366 // First check if this URI is cached, if it is then we're done 367 cacheEntry = LocatorCache.locatorCache().fetchURICache(uri); 368 if (null != cacheEntry) { 369 // Cache hit! Grab contentType and contentLength and be done 370 contentType = cacheEntry.getMIMEType(); 371 contentLength = cacheEntry.getBuffer().capacity(); 372 if (Logger.canLog(Logger.DEBUG)) { 373 Logger.logMsg(Logger.DEBUG, "Locator init cache hit:" 374 + "\n uri " + uri 375 + "\n type " + contentType 376 + "\n length " + contentLength); 377 } 378 return; 379 } 380 381 // Try to open a connection on the corresponding URL. 382 boolean isConnected = false; 383 boolean isMediaUnAvailable = false; 384 boolean isMediaSupported = true; 385 if (!isIpod) { 386 for (int numConnectionAttempts = 0; numConnectionAttempts < MAX_CONNECTION_ATTEMPTS; numConnectionAttempts++) { 387 try { 388 // Verify existence. 389 if (scheme.equals("http")) { 390 // Check ability to connect, trying HEAD before GET. 391 LocatorConnection locatorConnection = getConnection(uri, "HEAD"); 392 if (locatorConnection == null || locatorConnection.connection == null) { 393 locatorConnection = getConnection(uri, "GET"); 394 } 395 396 if (locatorConnection != null && locatorConnection.connection != null) { 397 isConnected = true; 398 399 // Get content type. 400 contentType = locatorConnection.connection.getContentType(); 401 contentLength = getContentLengthLong(locatorConnection.connection); 402 403 // Disconnect. 404 closeConnection(locatorConnection.connection); 405 locatorConnection.connection = null; 406 } else if (locatorConnection != null) { 407 if (locatorConnection.responseCode == HttpURLConnection.HTTP_NOT_FOUND) { 408 isMediaUnAvailable = true; 409 } 410 } 411 412 // FIXME: get cache settings from server, honor them 413 } else if (scheme.equals("file") || scheme.equals("jar")) { 414 InputStream stream = getInputStream(uri); 415 stream.close(); 416 isConnected = true; 417 contentType = MediaUtils.filenameToContentType(uriString); // We need to provide at least something 418 } 419 420 if (isConnected) { 421 // Check whether content may be played. 422 // For WAV use file signature, since it can detect audio format 423 // and we can fail sooner, then doing it at runtime. 424 // This is important for AudioClip. 425 if (MediaUtils.CONTENT_TYPE_WAV.equals(contentType)) { 426 contentType = getContentTypeFromFileSignature(uri); 427 if (!MediaManager.canPlayContentType(contentType)) { 428 isMediaSupported = false; 429 } 430 } else { 431 if (contentType == null || !MediaManager.canPlayContentType(contentType)) { 432 // Try content based on file name. 433 contentType = MediaUtils.filenameToContentType(uriString); 434 435 if (Locator.DEFAULT_CONTENT_TYPE.equals(contentType)) { 436 // Try content based on file signature. 437 contentType = getContentTypeFromFileSignature(uri); 438 } 439 440 if (!MediaManager.canPlayContentType(contentType)) { 441 isMediaSupported = false; 442 } 443 } 444 } 445 446 // Break as connection has been made and media type checked. 447 break; 448 } 449 } catch (IOException ioe) { 450 if (numConnectionAttempts + 1 >= MAX_CONNECTION_ATTEMPTS) { 451 throw ioe; 452 } 453 } 454 455 try { 456 Thread.sleep(CONNECTION_RETRY_INTERVAL); 457 } catch (InterruptedException ie) { 458 // Ignore it. 459 } 460 } 461 } 462 else { 463 // in case of iPod files we can be sure all files are supported 464 contentType = MediaUtils.filenameToContentType(uriString); 465 } 466 467 // Check URI validity. 468 if (!isIpod && !isConnected) { 469 if (isMediaUnAvailable) { 470 throw new FileNotFoundException("media is unavailable (" + uri.toString() + ")"); 471 } else { 472 throw new IOException("could not connect to media (" + uri.toString() + ")"); 473 } 474 } else if (!isMediaSupported) { 475 throw new MediaException("media type not supported (" + uri.toString() + ")"); 476 } 477 } catch (FileNotFoundException e) { 478 throw e; // Just re-throw exception 479 } catch (IOException e) { 480 throw e; // Just re-throw exception 481 } catch (MediaException e) { 482 throw e; // Just re-throw exception 483 } finally { 484 readySignal.countDown(); 485 } 486 } 487 488 /** 489 * Retrieves the content type describing the media content or 490 * <code>"application/octet-stream"</code> if no more specific content type 491 * may be detected. 492 */ 493 public String getContentType() { 494 try { 495 readySignal.await(); 496 } catch (Exception e) { 497 } 498 return contentType; 499 } 500 501 /** 502 * Retrieves the protocol of the media URL 503 */ 504 public String getProtocol() { 505 return protocol; 506 } 507 508 /** 509 * Retrieves the media size. 510 * 511 * @return size of the media file in bytes. -1 indicates unknown, which may 512 * happen with network streams. 513 */ 514 public long getContentLength() { 515 try { 516 readySignal.await(); 517 } catch (Exception e) { 518 } 519 return contentLength; 520 } 521 522 /** 523 * Blocks until locator is ready (connection is established or failed). 524 */ 525 public void waitForReadySignal() { 526 try { 527 readySignal.await(); 528 } catch (Exception e) { 529 } 530 } 531 532 /** 533 * Retrieves the associated 534 * <code>URI</code>. 535 * 536 * @return The URI source. 537 */ 538 public URI getURI() { 539 return this.uri; 540 } 541 542 /** 543 * Retrieves a string representation of the 544 * <code>Locator</code> 545 * 546 * @return The 547 * <code>LocatorURI</code> as a 548 * <code>String</code>. 549 */ 550 @Override 551 public String toString() { 552 if (LocatorCache.locatorCache().isCached(uri)) { 553 return "{LocatorURI uri: " + uri.toString() + " (media cached)}"; 554 } 555 return "{LocatorURI uri: " + uri.toString() + "}"; 556 } 557 558 public String getStringLocation() { 559 return uri.toString(); 560 } 561 562 /** 563 * Sets a property to be used by the connection to the media specified by 564 * the URI. The meaning of the property is a function of the URI protocol 565 * and type of media source. This method should be invoked <i>before</i> 566 * calling {@link #createConnectionHolder()} or it will have no effect. 567 * 568 * @param property The name of the property. 569 * @param value The value of the property. 570 */ 571 public void setConnectionProperty(String property, Object value) { 572 synchronized (propertyLock) { 573 if (connectionProperties == null) { 574 connectionProperties = new TreeMap<String, Object>(); 575 } 576 577 connectionProperties.put(property, value); 578 } 579 } 580 581 public ConnectionHolder createConnectionHolder() throws IOException { 582 // first check if it's cached 583 if (null != cacheEntry) { 584 if (Logger.canLog(Logger.DEBUG)) { 585 Logger.logMsg(Logger.DEBUG, "Locator.createConnectionHolder: media cached, creating memory connection holder"); 586 } 587 return ConnectionHolder.createMemoryConnectionHolder(cacheEntry.getBuffer()); 588 } 589 590 // then fall back on other methods 591 ConnectionHolder holder; 592 if ("file".equals(scheme)) { 593 holder = ConnectionHolder.createFileConnectionHolder(uri); 594 } else if (uri.toString().endsWith(".m3u8") || uri.toString().endsWith(".m3u")) { 595 holder = ConnectionHolder.createHLSConnectionHolder(uri); 596 } else { 597 synchronized (propertyLock) { 598 holder = ConnectionHolder.createURIConnectionHolder(uri, connectionProperties); 599 } 600 } 601 602 return holder; 603 } 604 605 private String getContentTypeFromFileSignature(URI uri) throws MalformedURLException, IOException { 606 InputStream stream = getInputStream(uri); 607 byte[] signature = new byte[MediaUtils.MAX_FILE_SIGNATURE_LENGTH]; 608 int size = stream.read(signature); 609 stream.close(); 610 611 return MediaUtils.fileSignatureToContentType(signature, size); 612 } 613 614 static void closeConnection(URLConnection connection) { 615 if (connection instanceof HttpURLConnection) { 616 HttpURLConnection httpConnection = (HttpURLConnection)connection; 617 try { 618 if (httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST && 619 httpConnection.getInputStream() != null) { 620 httpConnection.getInputStream().close(); 621 } 622 } catch (IOException ex) { 623 try { 624 if (httpConnection.getErrorStream() != null) { 625 httpConnection.getErrorStream().close(); 626 } 627 } catch (IOException e) {} 628 } 629 } 630 } 631 }