1 /*
   2  * Copyright (c) 2010, 2018, 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) || (urlConnection instanceof JarURLConnection);
 260         }
 261 
 262         boolean isRandomAccess() {
 263             return false;
 264         }
 265 
 266         int readBlock(long position, int size) throws IOException {
 267             throw new IOException();
 268         }
 269 
 270         public long seek(long position) {
 271             if (urlConnection instanceof HttpURLConnection) {
 272                 URLConnection tmpURLConnection = null;
 273 
 274                 //closeConnection();
 275                 try{
 276                     tmpURLConnection = uri.toURL().openConnection();
 277 
 278                     HttpURLConnection httpConnection = (HttpURLConnection)tmpURLConnection;
 279                     httpConnection.setRequestMethod("GET");
 280                     httpConnection.setUseCaches(false);
 281                     httpConnection.setRequestProperty("Range", "bytes=" + position + "-");
 282                     // If range request worked properly we should get responce code 206 (HTTP_PARTIAL)
 283                     // Else fail seek and let progressbuffer to download all data. It is pointless for us to download it and throw away.
 284                     if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
 285                         closeConnection();
 286                         urlConnection = tmpURLConnection;
 287                         tmpURLConnection = null;
 288                         channel = openChannel(null);
 289                         return position;
 290                     } else {
 291                         return -1;
 292                     }
 293                 } catch (IOException ioex) {
 294                     return -1;
 295                 } finally {
 296                     if (tmpURLConnection != null) {
 297                         Locator.closeConnection(tmpURLConnection);
 298                     }
 299                 }
 300             } else if (urlConnection instanceof JarURLConnection) {
 301                 try {
 302                     closeConnection();
 303 
 304                     urlConnection = uri.toURL().openConnection();
 305 
 306                     // Skip data that we do not need
 307                     long skip_left = position;
 308                     InputStream inputStream = urlConnection.getInputStream();
 309                     do {
 310                         long skip = inputStream.skip(skip_left);
 311                         skip_left -= skip;
 312                     } while (skip_left > 0);
 313 
 314                     channel = openChannel(inputStream);
 315 
 316                     return position;
 317                 } catch (IOException ioex) {
 318                     return -1;
 319                 }
 320             }
 321 
 322             return -1;
 323         }
 324 
 325         @Override
 326         public void closeConnection() {
 327             super.closeConnection();
 328 
 329             Locator.closeConnection(urlConnection);
 330             urlConnection = null;
 331         }
 332 
 333         private ReadableByteChannel openChannel(InputStream inputStream) throws IOException {
 334             return (inputStream == null) ?
 335                     Channels.newChannel(urlConnection.getInputStream()) :
 336                     Channels.newChannel(inputStream);
 337         }
 338     }
 339 
 340     // A "ConnectionHolder" that "reads" from a ByteBuffer, generally loaded from
 341     // some unsupported or buggy source
 342     private static class MemoryConnectionHolder extends ConnectionHolder {
 343         private final ByteBuffer backingBuffer;
 344 
 345         public MemoryConnectionHolder(ByteBuffer buf) {
 346             if (null == buf) {
 347                 throw new IllegalArgumentException("Can't connect to null buffer...");
 348             }
 349 
 350             if (buf.isDirect()) {
 351                 // we can use it, or rather a duplicate directly
 352                 backingBuffer = buf.duplicate();
 353             } else {
 354                 // operate on a copy of the buffer
 355                 backingBuffer = ByteBuffer.allocateDirect(buf.capacity());
 356                 backingBuffer.put(buf);
 357             }
 358 
 359             // rewind since the default position is expected to be at zero
 360             backingBuffer.rewind();
 361 
 362             // readNextBlock should never be called since we're random access
 363             // but just to be safe (and for unit tests...)
 364             channel = new ReadableByteChannel() {
 365                 public int read(ByteBuffer bb) throws IOException {
 366                     if (backingBuffer.remaining() <= 0) {
 367                         return -1; // EOS
 368                     }
 369 
 370                     int actual;
 371                     if (bb.equals(buffer)) {
 372                         // we'll cheat here as we know that bb is buffer and rather
 373                         // than copy the data, just slice it like for readBlock
 374                         actual = Math.min(DEFAULT_BUFFER_SIZE, backingBuffer.remaining());
 375                         if (actual > 0) {
 376                             buffer = backingBuffer.slice();
 377                             buffer.limit(actual);
 378                         }
 379                     } else {
 380                         actual = Math.min(bb.remaining(), backingBuffer.remaining());
 381                         if (actual > 0) {
 382                             backingBuffer.limit(backingBuffer.position() + actual);
 383                             bb.put(backingBuffer);
 384                             backingBuffer.limit(backingBuffer.capacity());
 385                         }
 386                     }
 387                     return actual;
 388                 }
 389 
 390                 public boolean isOpen() {
 391                     return true; // open 24/7/365
 392                 }
 393 
 394                 public void close() throws IOException {
 395                     // never closed...
 396                 }
 397             };
 398         }
 399 
 400         @Override
 401         int readBlock(long position, int size) throws IOException {
 402             // mimic stream behavior
 403             if (null == channel) {
 404                 throw new ClosedChannelException();
 405             }
 406 
 407             if ((int)position > backingBuffer.capacity()) {
 408                 return -1; //EOS
 409             }
 410             backingBuffer.position((int)position);
 411 
 412             buffer = backingBuffer.slice();
 413 
 414             int actual = Math.min(backingBuffer.remaining(), size);
 415             buffer.limit(actual); // only give as much as asked
 416             backingBuffer.position(backingBuffer.position() + actual);
 417 
 418             return actual;
 419         }
 420 
 421         @Override
 422         boolean needBuffer() {
 423             return false;
 424         }
 425 
 426         @Override
 427         boolean isSeekable() {
 428             return true;
 429         }
 430 
 431         @Override
 432         boolean isRandomAccess() {
 433             return true;
 434         }
 435 
 436         @Override
 437         public long seek(long position) {
 438             if ((int)position < backingBuffer.capacity()) {
 439                 backingBuffer.limit(backingBuffer.capacity());
 440                 backingBuffer.position((int)position);
 441                 return position;
 442             }
 443             return -1;
 444         }
 445 
 446         @Override
 447         public void closeConnection() {
 448             // more stream behavior mimicry
 449             channel = null;
 450         }
 451     }
 452 }