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 }