1 /*
   2  * Copyright (c) 2010, 2019, 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 
  26 package com.sun.media.jfxmedia.locator;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.RandomAccessFile;
  32 import java.net.HttpURLConnection;
  33 import java.net.JarURLConnection;
  34 import java.net.URI;
  35 import java.net.URLConnection;
  36 import java.nio.ByteBuffer;
  37 import java.nio.channels.Channels;
  38 import java.nio.channels.ClosedChannelException;
  39 import java.nio.channels.FileChannel;
  40 import java.nio.channels.ReadableByteChannel;
  41 import java.util.Map;
  42 
  43 /**
  44  * Connection holders hold and maintain connection do different kinds of sources
  45  *
  46  */
  47 public abstract class ConnectionHolder {
  48     private static int DEFAULT_BUFFER_SIZE = 4096;
  49 
  50     ReadableByteChannel channel;
  51     ByteBuffer          buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
  52 
  53     static ConnectionHolder createMemoryConnectionHolder(ByteBuffer buffer) {
  54         return new MemoryConnectionHolder(buffer);
  55     }
  56 
  57     static ConnectionHolder createURIConnectionHolder(URI uri, Map<String,Object> connectionProperties) throws IOException {
  58         return new URIConnectionHolder(uri, connectionProperties);
  59     }
  60 
  61     static ConnectionHolder createFileConnectionHolder(URI uri) throws IOException {
  62         return new FileConnectionHolder(uri);
  63     }
  64 
  65     static ConnectionHolder createHLSConnectionHolder(URI uri) throws IOException {
  66         return new HLSConnectionHolder(uri);
  67     }
  68 
  69     /**
  70      * Reads a block of data from the current position of the opened stream.
  71      *
  72      * @return The number of bytes read, possibly zero, or -1 if the channel
  73      * has reached end-of-stream.
  74      *
  75      * @throws ClosedChannelException if an attempt is made to read after
  76      * closeConnection has been called
  77      */
  78     public int readNextBlock() throws IOException {
  79         buffer.rewind();
  80         if (buffer.limit() < buffer.capacity()) {
  81             buffer.limit(buffer.capacity());
  82         }
  83         // avoid NPE if channel does not exist or has been closed
  84         if (null == channel) {
  85             throw new ClosedChannelException();
  86         }
  87         return channel.read(buffer);
  88     }
  89 
  90     public ByteBuffer getBuffer() {
  91         return buffer;
  92     }
  93 
  94     /**
  95      * Reads a block of data from the arbitrary position of the opened stream.
  96      *
  97      * @return The number of bytes read, possibly zero, or -1 if the given position
  98      * is greater than or equal to the file's current size.
  99      *
 100      * @throws ClosedChannelException if an attempt is made to read after
 101      * closeConnection has been called
 102      */
 103     abstract int readBlock(long position, int size) throws IOException;
 104 
 105     /**
 106      * Detects whether this source needs buffering at the pipeline level.
 107      * When true the pipeline contains progressbuffer after the source.
 108      *
 109      * @return true if the source needs a buffer, false otherwise.
 110      */
 111     abstract boolean needBuffer();
 112 
 113     /**
 114      * Detects whether the source is seekable.
 115      * @return true if the source is seekable, false otherwise.
 116      */
 117     abstract boolean isSeekable();
 118 
 119     /**
 120      * Detects whether the source is a random access source. If the method returns
 121      * true then the source is capable of working in pull mode. To be able to work
 122      * in pull mode holder must provide implementation.
 123      * @return true is the source is random access, false otherwise.
 124      */
 125     abstract boolean isRandomAccess();
 126 
 127     /**
 128      * Performs a seek request to the desired position.
 129      *
 130      * @return -1 if the seek request failed or new stream position
 131      */
 132     public abstract long seek(long position);
 133 
 134     /**
 135      * Closes connection when done.
 136      * Overriding methods should call this method in the beginning of their implementation.
 137      */
 138     public void closeConnection() {
 139         try {
 140             if (channel != null) {
 141                 channel.close();
 142             }
 143         } catch (IOException ioex) {}
 144         finally {
 145             channel = null;
 146         }
 147     }
 148 
 149     /**
 150      * Get or set properties.
 151      *
 152      * @param prop - Property ID.
 153      * @param value - Depends on property ID.
 154      * @return - Depends on property ID.
 155      */
 156     int property(int prop, int value) {
 157         return 0;
 158     }
 159 
 160     /**
 161      * Get stream size.
 162      * Behavior can vary based on subclass implementation.
 163      * For example HLS will load next segment and return segment size.
 164      *
 165      * @return - Stream size.
 166      */
 167     int getStreamSize() {
 168         return -1;
 169     }
 170 
 171     private static class FileConnectionHolder extends ConnectionHolder {
 172         private RandomAccessFile file = null;
 173 
 174         FileConnectionHolder(URI uri) throws IOException {
 175             channel = openFile(uri);
 176         }
 177 
 178         boolean needBuffer() {
 179             return false;
 180         }
 181 
 182         boolean isRandomAccess() {
 183             return true;
 184         }
 185 
 186         boolean isSeekable() {
 187             return true;
 188         }
 189 
 190         public long seek(long position) {
 191             try {
 192                 ((FileChannel)channel).position(position);
 193                 return position;
 194             } catch(IOException ioex) {
 195                 return -1;
 196             }
 197         }
 198 
 199         int readBlock(long position, int size) throws IOException {
 200             if (null == channel) {
 201                 throw new ClosedChannelException();
 202             }
 203 
 204             if (buffer.capacity() < size) {
 205                 buffer = ByteBuffer.allocateDirect(size);
 206             }
 207             buffer.rewind().limit(size);
 208             return ((FileChannel)channel).read(buffer, position);
 209         }
 210 
 211         private ReadableByteChannel openFile(final URI uri) throws IOException {
 212             if (file != null) {
 213                 file.close();
 214             }
 215 
 216             file = new RandomAccessFile(new File(uri), "r");
 217             return file.getChannel();
 218         }
 219 
 220         @Override
 221         public void closeConnection() {
 222             super.closeConnection();
 223 
 224             if (file != null) {
 225                 try {
 226                     file.close();
 227                 } catch (IOException ex) {
 228                 } finally {
 229                     file = null;
 230                 }
 231             }
 232         }
 233     }
 234 
 235     private static class URIConnectionHolder extends ConnectionHolder {
 236         private URI                 uri;
 237         private URLConnection       urlConnection;
 238 
 239         URIConnectionHolder(URI uri, Map<String,Object> connectionProperties) throws IOException {
 240             this.uri = uri;
 241             urlConnection = uri.toURL().openConnection();
 242             if (connectionProperties != null) {
 243                 for(Map.Entry<String,Object> entry : connectionProperties.entrySet()) {
 244                     Object value = entry.getValue();
 245                     if (value instanceof String) {
 246                         urlConnection.setRequestProperty(entry.getKey(), (String)value);
 247                     }
 248                 }
 249             }
 250             channel = openChannel(null);
 251         }
 252 
 253         boolean needBuffer() {
 254             String scheme = uri.getScheme().toLowerCase();
 255             return ("http".equals(scheme) || "https".equals(scheme));
 256         }
 257 
 258         boolean isSeekable() {
 259             return (urlConnection instanceof HttpURLConnection) ||
 260                    (urlConnection instanceof JarURLConnection) ||
 261                    isJRT();
 262         }
 263 
 264         boolean isRandomAccess() {
 265             return false;
 266         }
 267 
 268         int readBlock(long position, int size) throws IOException {
 269             throw new IOException();
 270         }
 271 
 272         public long seek(long position) {
 273             if (urlConnection instanceof HttpURLConnection) {
 274                 URLConnection tmpURLConnection = null;
 275 
 276                 //closeConnection();
 277                 try{
 278                     tmpURLConnection = uri.toURL().openConnection();
 279 
 280                     HttpURLConnection httpConnection = (HttpURLConnection)tmpURLConnection;
 281                     httpConnection.setRequestMethod("GET");
 282                     httpConnection.setUseCaches(false);
 283                     httpConnection.setRequestProperty("Range", "bytes=" + position + "-");
 284                     // If range request worked properly we should get responce code 206 (HTTP_PARTIAL)
 285                     // Else fail seek and let progressbuffer to download all data. It is pointless for us to download it and throw away.
 286                     if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
 287                         closeConnection();
 288                         urlConnection = tmpURLConnection;
 289                         tmpURLConnection = null;
 290                         channel = openChannel(null);
 291                         return position;
 292                     } else {
 293                         return -1;
 294                     }
 295                 } catch (IOException ioex) {
 296                     return -1;
 297                 } finally {
 298                     if (tmpURLConnection != null) {
 299                         Locator.closeConnection(tmpURLConnection);
 300                     }
 301                 }
 302             } else if ((urlConnection instanceof JarURLConnection) || isJRT()) {
 303                 try {
 304                     closeConnection();
 305 
 306                     urlConnection = uri.toURL().openConnection();
 307 
 308                     // Skip data that we do not need
 309                     long skip_left = position;
 310                     InputStream inputStream = urlConnection.getInputStream();
 311                     do {
 312                         long skip = inputStream.skip(skip_left);
 313                         skip_left -= skip;
 314                     } while (skip_left > 0);
 315 
 316                     channel = openChannel(inputStream);
 317 
 318                     return position;
 319                 } catch (IOException ioex) {
 320                     return -1;
 321                 }
 322             }
 323 
 324             return -1;
 325         }
 326 
 327         @Override
 328         public void closeConnection() {
 329             super.closeConnection();
 330 
 331             Locator.closeConnection(urlConnection);
 332             urlConnection = null;
 333         }
 334 
 335         private ReadableByteChannel openChannel(InputStream inputStream) throws IOException {
 336             return (inputStream == null) ?
 337                     Channels.newChannel(urlConnection.getInputStream()) :
 338                     Channels.newChannel(inputStream);
 339         }
 340 
 341         private boolean isJRT() {
 342             String scheme = uri.getScheme().toLowerCase();
 343             return "jrt".equals(scheme);
 344         }
 345     }
 346 
 347     // A "ConnectionHolder" that "reads" from a ByteBuffer, generally loaded from
 348     // some unsupported or buggy source
 349     private static class MemoryConnectionHolder extends ConnectionHolder {
 350         private final ByteBuffer backingBuffer;
 351 
 352         public MemoryConnectionHolder(ByteBuffer buf) {
 353             if (null == buf) {
 354                 throw new IllegalArgumentException("Can't connect to null buffer...");
 355             }
 356 
 357             if (buf.isDirect()) {
 358                 // we can use it, or rather a duplicate directly
 359                 backingBuffer = buf.duplicate();
 360             } else {
 361                 // operate on a copy of the buffer
 362                 backingBuffer = ByteBuffer.allocateDirect(buf.capacity());
 363                 backingBuffer.put(buf);
 364             }
 365 
 366             // rewind since the default position is expected to be at zero
 367             backingBuffer.rewind();
 368 
 369             // readNextBlock should never be called since we're random access
 370             // but just to be safe (and for unit tests...)
 371             channel = new ReadableByteChannel() {
 372                 public int read(ByteBuffer bb) throws IOException {
 373                     if (backingBuffer.remaining() <= 0) {
 374                         return -1; // EOS
 375                     }
 376 
 377                     int actual;
 378                     if (bb.equals(buffer)) {
 379                         // we'll cheat here as we know that bb is buffer and rather
 380                         // than copy the data, just slice it like for readBlock
 381                         actual = Math.min(DEFAULT_BUFFER_SIZE, backingBuffer.remaining());
 382                         if (actual > 0) {
 383                             buffer = backingBuffer.slice();
 384                             buffer.limit(actual);
 385                         }
 386                     } else {
 387                         actual = Math.min(bb.remaining(), backingBuffer.remaining());
 388                         if (actual > 0) {
 389                             backingBuffer.limit(backingBuffer.position() + actual);
 390                             bb.put(backingBuffer);
 391                             backingBuffer.limit(backingBuffer.capacity());
 392                         }
 393                     }
 394                     return actual;
 395                 }
 396 
 397                 public boolean isOpen() {
 398                     return true; // open 24/7/365
 399                 }
 400 
 401                 public void close() throws IOException {
 402                     // never closed...
 403                 }
 404             };
 405         }
 406 
 407         @Override
 408         int readBlock(long position, int size) throws IOException {
 409             // mimic stream behavior
 410             if (null == channel) {
 411                 throw new ClosedChannelException();
 412             }
 413 
 414             if ((int)position > backingBuffer.capacity()) {
 415                 return -1; //EOS
 416             }
 417             backingBuffer.position((int)position);
 418 
 419             buffer = backingBuffer.slice();
 420 
 421             int actual = Math.min(backingBuffer.remaining(), size);
 422             buffer.limit(actual); // only give as much as asked
 423             backingBuffer.position(backingBuffer.position() + actual);
 424 
 425             return actual;
 426         }
 427 
 428         @Override
 429         boolean needBuffer() {
 430             return false;
 431         }
 432 
 433         @Override
 434         boolean isSeekable() {
 435             return true;
 436         }
 437 
 438         @Override
 439         boolean isRandomAccess() {
 440             return true;
 441         }
 442 
 443         @Override
 444         public long seek(long position) {
 445             if ((int)position < backingBuffer.capacity()) {
 446                 backingBuffer.limit(backingBuffer.capacity());
 447                 backingBuffer.position((int)position);
 448                 return position;
 449             }
 450             return -1;
 451         }
 452 
 453         @Override
 454         public void closeConnection() {
 455             // more stream behavior mimicry
 456             channel = null;
 457         }
 458     }
 459 }