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