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 }