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 }