/* * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.media.jfxmedia.locator; import com.sun.media.jfxmedia.MediaException; import com.sun.media.jfxmedia.MediaManager; import com.sun.media.jfxmedia.logging.Logger; import com.sun.media.jfxmediaimpl.HostUtils; import com.sun.media.jfxmediaimpl.MediaUtils; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.nio.ByteBuffer; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.CountDownLatch; /** * A * Locator which refers to a * URI. */ public class Locator { /** * The content type used if no more specific one may be derived. */ public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; /** * The number of times to attempt to open a URL connection to test the URI. */ private static final int MAX_CONNECTION_ATTEMPTS = 5; /** * The number of milliseconds between attempts to open a URL connection. */ private static final long CONNECTION_RETRY_INTERVAL = 1000L; /** * The content type of the media content. */ protected String contentType = DEFAULT_CONTENT_TYPE; /** * A hint for the internal player. */ protected long contentLength = -1; //Used as a hint for the native layer /** * The URI source. */ protected URI uri; /** * Properties to be associated with the connection made to the URI. The * significance of the properties depends on the URI protocol and type of * media source. */ private Map connectionProperties; /** * Mutex for connectionProperties; */ private final Object propertyLock = new Object(); /* * These variables will be initialized by constructor and used by init() */ private String uriString = null; private String scheme = null; private String protocol = null; /* * if cached, we store a hard reference to keep it alive */ private LocatorCache.CacheReference cacheEntry = null; /* * True if init(), getContentLength() and getContentType() can block; false * otherwise. */ private boolean canBlock = false; /* * Used to block getContentLength() and getContentType(). */ private CountDownLatch readySignal = new CountDownLatch(1); /** * iOS only: determines if the given URL points to the iPod library */ private boolean isIpod; /** * Holds connection and response code returned from getConnection() */ private static class LocatorConnection { public HttpURLConnection connection = null; public int responseCode = HttpURLConnection.HTTP_OK; } private LocatorConnection getConnection(URI uri, String requestMethod) throws MalformedURLException, IOException { // Check ability to connect. LocatorConnection locatorConnection = new LocatorConnection(); HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); connection.setRequestMethod(requestMethod); // Set request headers. synchronized (propertyLock) { if (connectionProperties != null) { for (String key : connectionProperties.keySet()) { Object value = connectionProperties.get(key); if (value instanceof String) { connection.setRequestProperty(key, (String) value); } } } } // Store response code so we can get more information about // returning connection. locatorConnection.responseCode = connection.getResponseCode(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { locatorConnection.connection = connection; } else { closeConnection(connection); locatorConnection.connection = null; } return locatorConnection; } private static long getContentLengthLong(URLConnection connection) { Method method = AccessController.doPrivileged((PrivilegedAction) () -> { try { return URLConnection.class.getMethod("getContentLengthLong"); } catch (NoSuchMethodException ex) { return null; } }); try { if (method != null) { return (long) method.invoke(connection); } else { return connection.getContentLength(); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { return -1; } } /** * Constructs an object representing a media source. * * @param uri The URI source. * @throws NullPointerException if * uri is * null. * @throws IllegalArgumentException if the URI's scheme is * null. * @throws URISyntaxException if the supplied URI requires some further * manipulation in order to be used and this procedure fails to produce a * usable URI. * @throws IllegalArgumentException if the URI is a Jar URL as described in * {@link JarURLConnection https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html}, * and the scheme of the URL after removing the leading four characters is * null. * @throws UnsupportedOperationException if the URI's protocol is * unsupported. */ public Locator(URI uri) throws URISyntaxException { // Check for NULL parameter. if (uri == null) { throw new NullPointerException("uri == null!"); } // Get the scheme part. uriString = uri.toASCIIString(); scheme = uri.getScheme(); if (scheme == null) { throw new IllegalArgumentException("uri.getScheme() == null! uri == '" + uri + "'"); } scheme = scheme.toLowerCase(); // Get the protocol. if (scheme.equals("jar")) { URI subURI = new URI(uriString.substring(4)); protocol = subURI.getScheme(); if (protocol == null) { throw new IllegalArgumentException("uri.getScheme() == null! subURI == '" + subURI + "'"); } protocol = protocol.toLowerCase(); } else { protocol = scheme; // scheme is already lower case. } if (HostUtils.isIOS() && protocol.equals("ipod-library")) { isIpod = true; } // Verify the protocol is supported. if (!isIpod && !MediaManager.canPlayProtocol(protocol)) { throw new UnsupportedOperationException("Unsupported protocol \"" + protocol + "\""); } // Check if we can block if (protocol.equals("http") || protocol.equals("https")) { canBlock = true; } // Set instance variable. this.uri = uri; } private InputStream getInputStream(URI uri) throws MalformedURLException, IOException { URL url = uri.toURL(); URLConnection connection = url.openConnection(); // Set request headers. synchronized (propertyLock) { if (connectionProperties != null) { for (String key : connectionProperties.keySet()) { Object value = connectionProperties.get(key); if (value instanceof String) { connection.setRequestProperty(key, (String) value); } } } } InputStream inputStream = url.openStream(); contentLength = getContentLengthLong(connection); return inputStream; } /** * Tell this Locator to preload the media into memory, if it hasn't been * already. */ public void cacheMedia() { LocatorCache.CacheReference ref = LocatorCache.locatorCache().fetchURICache(uri); if (null == ref) { ByteBuffer cacheBuffer; // not cached, load it InputStream is; try { is = getInputStream(uri); } catch (Throwable t) { return; // just bail } // contentLength is set now, so we can go ahead and allocate cacheBuffer = ByteBuffer.allocateDirect((int) contentLength); byte[] readBuf = new byte[8192]; int total = 0; int count; while (total < contentLength) { try { count = is.read(readBuf); } catch (IOException ioe) { try { is.close(); } catch (Throwable t) { } if (Logger.canLog(Logger.DEBUG)) { Logger.logMsg(Logger.DEBUG, "IOException trying to preload media: " + ioe); } return; } if (count == -1) { break; // EOS } cacheBuffer.put(readBuf, 0, count); } try { is.close(); } catch (Throwable t) { } cacheEntry = LocatorCache.locatorCache().registerURICache(uri, cacheBuffer, contentType); canBlock = false; } } /* * True if init() can block; false otherwise. */ public boolean canBlock() { return canBlock; } /* * Initialize locator. Use canBlock() to determine if init() can block. * * @throws URISyntaxException if the supplied URI requires some further * manipulation in order to be used and this procedure fails to produce a * usable URI. @throws IOExceptions if a stream cannot be opened over a * connection of the corresponding URL. @throws MediaException if the * content type of the media is not supported. @throws FileNotFoundException * if the media is not available. */ public void init() throws URISyntaxException, IOException, FileNotFoundException { try { // Ensure the correct number of '/'s follows the ':'. int firstSlash = uriString.indexOf("/"); if (firstSlash != -1 && uriString.charAt(firstSlash + 1) != '/') { // Only one '/' after the ':'. if (protocol.equals("file")) { // Map file:/somepath to file:///somepath uriString = uriString.replaceFirst("/", "///"); } else if (protocol.equals("http") || protocol.equals("https")) { // Map http:/somepath to http://somepath uriString = uriString.replaceFirst("/", "//"); } } // On non-Windows systems, replace "/~/" with home directory path + "/". if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1 && protocol.equals("file")) { int index = uriString.indexOf("/~/"); if (index != -1) { uriString = uriString.substring(0, index) + System.getProperty("user.home") + uriString.substring(index + 2); } } // Recreate the URI if needed uri = new URI(uriString); // First check if this URI is cached, if it is then we're done cacheEntry = LocatorCache.locatorCache().fetchURICache(uri); if (null != cacheEntry) { // Cache hit! Grab contentType and contentLength and be done contentType = cacheEntry.getMIMEType(); contentLength = cacheEntry.getBuffer().capacity(); if (Logger.canLog(Logger.DEBUG)) { Logger.logMsg(Logger.DEBUG, "Locator init cache hit:" + "\n uri " + uri + "\n type " + contentType + "\n length " + contentLength); } return; } // Try to open a connection on the corresponding URL. boolean isConnected = false; boolean isMediaUnAvailable = false; boolean isMediaSupported = true; if (!isIpod) { for (int numConnectionAttempts = 0; numConnectionAttempts < MAX_CONNECTION_ATTEMPTS; numConnectionAttempts++) { try { // Verify existence. if (scheme.equals("http") || scheme.equals("https")) { // Check ability to connect, trying HEAD before GET. LocatorConnection locatorConnection = getConnection(uri, "HEAD"); if (locatorConnection == null || locatorConnection.connection == null) { locatorConnection = getConnection(uri, "GET"); } if (locatorConnection != null && locatorConnection.connection != null) { isConnected = true; // Get content type. contentType = locatorConnection.connection.getContentType(); contentLength = getContentLengthLong(locatorConnection.connection); // Disconnect. closeConnection(locatorConnection.connection); locatorConnection.connection = null; } else if (locatorConnection != null) { if (locatorConnection.responseCode == HttpURLConnection.HTTP_NOT_FOUND) { isMediaUnAvailable = true; } } // FIXME: get cache settings from server, honor them } else if (scheme.equals("file") || scheme.equals("jar")) { InputStream stream = getInputStream(uri); stream.close(); isConnected = true; contentType = MediaUtils.filenameToContentType(uriString); // We need to provide at least something } if (isConnected) { // Check whether content may be played. // For WAV use file signature, since it can detect audio format // and we can fail sooner, then doing it at runtime. // This is important for AudioClip. if (MediaUtils.CONTENT_TYPE_WAV.equals(contentType)) { contentType = getContentTypeFromFileSignature(uri); if (!MediaManager.canPlayContentType(contentType)) { isMediaSupported = false; } } else { if (contentType == null || !MediaManager.canPlayContentType(contentType)) { // Try content based on file name. contentType = MediaUtils.filenameToContentType(uriString); if (Locator.DEFAULT_CONTENT_TYPE.equals(contentType)) { // Try content based on file signature. contentType = getContentTypeFromFileSignature(uri); } if (!MediaManager.canPlayContentType(contentType)) { isMediaSupported = false; } } } // Break as connection has been made and media type checked. break; } } catch (IOException ioe) { if (numConnectionAttempts + 1 >= MAX_CONNECTION_ATTEMPTS) { throw ioe; } } try { Thread.sleep(CONNECTION_RETRY_INTERVAL); } catch (InterruptedException ie) { // Ignore it. } } } else { // in case of iPod files we can be sure all files are supported contentType = MediaUtils.filenameToContentType(uriString); } // Check URI validity. if (!isIpod && !isConnected) { if (isMediaUnAvailable) { throw new FileNotFoundException("media is unavailable (" + uri.toString() + ")"); } else { throw new IOException("could not connect to media (" + uri.toString() + ")"); } } else if (!isMediaSupported) { throw new MediaException("media type not supported (" + uri.toString() + ")"); } } catch (FileNotFoundException e) { throw e; // Just re-throw exception } catch (IOException e) { throw e; // Just re-throw exception } catch (MediaException e) { throw e; // Just re-throw exception } finally { readySignal.countDown(); } } /** * Retrieves the content type describing the media content or * "application/octet-stream" if no more specific content type * may be detected. */ public String getContentType() { try { readySignal.await(); } catch (Exception e) { } return contentType; } /** * Retrieves the protocol of the media URL */ public String getProtocol() { return protocol; } /** * Retrieves the media size. * * @return size of the media file in bytes. -1 indicates unknown, which may * happen with network streams. */ public long getContentLength() { try { readySignal.await(); } catch (Exception e) { } return contentLength; } /** * Blocks until locator is ready (connection is established or failed). */ public void waitForReadySignal() { try { readySignal.await(); } catch (Exception e) { } } /** * Retrieves the associated * URI. * * @return The URI source. */ public URI getURI() { return this.uri; } /** * Retrieves a string representation of the * Locator * * @return The * LocatorURI as a * String. */ @Override public String toString() { if (LocatorCache.locatorCache().isCached(uri)) { return "{LocatorURI uri: " + uri.toString() + " (media cached)}"; } return "{LocatorURI uri: " + uri.toString() + "}"; } public String getStringLocation() { return uri.toString(); } /** * Sets a property to be used by the connection to the media specified by * the URI. The meaning of the property is a function of the URI protocol * and type of media source. This method should be invoked before * calling {@link #createConnectionHolder()} or it will have no effect. * * @param property The name of the property. * @param value The value of the property. */ public void setConnectionProperty(String property, Object value) { synchronized (propertyLock) { if (connectionProperties == null) { connectionProperties = new TreeMap(); } connectionProperties.put(property, value); } } public ConnectionHolder createConnectionHolder() throws IOException { // first check if it's cached if (null != cacheEntry) { if (Logger.canLog(Logger.DEBUG)) { Logger.logMsg(Logger.DEBUG, "Locator.createConnectionHolder: media cached, creating memory connection holder"); } return ConnectionHolder.createMemoryConnectionHolder(cacheEntry.getBuffer()); } // then fall back on other methods ConnectionHolder holder; if ("file".equals(scheme)) { holder = ConnectionHolder.createFileConnectionHolder(uri); } else if (uri.toString().endsWith(".m3u8") || uri.toString().endsWith(".m3u")) { holder = ConnectionHolder.createHLSConnectionHolder(uri); } else { synchronized (propertyLock) { holder = ConnectionHolder.createURIConnectionHolder(uri, connectionProperties); } } return holder; } private String getContentTypeFromFileSignature(URI uri) throws MalformedURLException, IOException { InputStream stream = getInputStream(uri); byte[] signature = new byte[MediaUtils.MAX_FILE_SIGNATURE_LENGTH]; int size = stream.read(signature); stream.close(); return MediaUtils.fileSignatureToContentType(signature, size); } static void closeConnection(URLConnection connection) { if (connection instanceof HttpURLConnection) { HttpURLConnection httpConnection = (HttpURLConnection)connection; try { if (httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST && httpConnection.getInputStream() != null) { httpConnection.getInputStream().close(); } } catch (IOException ex) { try { if (httpConnection.getErrorStream() != null) { httpConnection.getErrorStream().close(); } } catch (IOException e) {} } } } }