001/*******************************************************************************
002 * Copyright (c) 2024, 2026, Olivier Ayache.  All rights reserved.
003 *
004 * This file is part of AVPKit.
005 *
006 * AVPKit is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Lesser General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * AVPKit is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with AVPKit.  If not, see <http://www.gnu.org/licenses/>.
018 *******************************************************************************/
019
020package com.avpkit.core.io;
021
022import java.io.Closeable;
023import java.io.DataInput;
024import java.io.DataOutput;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.io.RandomAccessFile;
028import java.nio.channels.ByteChannel;
029import java.nio.channels.ReadableByteChannel;
030import java.nio.channels.WritableByteChannel;
031import java.util.UUID;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.concurrent.ConcurrentMap;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import com.avpkit.core.IContainer;
039
040/**
041 * Allows AVPKit to read from and write to many different types of Java I/O
042 * objects, plus custom {@link IURLProtocolHandler} objects.
043 * 
044 * <p>
045 * Most of the time, IContainer hides this away from you, but in case you're
046 * interested, here's the underlying class doing the IO magic.
047 * </p>
048 * 
049 * To use for reading (assuming an InputStream object called inputStream): </p>
050 * 
051 * <pre>
052 * IContainer container = IContainer.make();
053 * container.open(inputStream, null);
054 * </pre>
055 * <p>
056 * or for writing:
057 * </p>
058 * 
059 * <pre>
060 * IContainer container = IContainer.make();
061 * container.open(outputStream, null);
062 * </pre>
063 * <p>
064 * Really. That's it.
065 * </p>
066 * <p>
067 * 
068 * All streams that are mapped in this factory share the same name space, even
069 * if registered under different protocols. So, if "exampleone" and "exampletwo"
070 * were both registered as protocols for this factory, then
071 * "exampleone:filename" is the same as "exampletwo:filename" and will map to
072 * the same input and output streams. In reality, they are all mapped to the
073 * {@link #DEFAULT_PROTOCOL} protocol.
074 * 
075 * </p>
076 */
077
078public class AVPKitIO implements IURLProtocolHandlerFactory
079{
080  private final Logger log = LoggerFactory.getLogger(this.getClass());
081  /**
082   * The default protocol string that this factory uses (
083   * {@value #DEFAULT_PROTOCOL}).
084   */
085
086  public final static String DEFAULT_PROTOCOL = "avpkit";
087
088  /**
089   * Do we undo mappings on open by default?
090   */
091  private final static boolean DEFAULT_UNMAP_URL_ON_OPEN = true;
092
093  /**
094   * Do we call {@link Closeable#close()} by default when closing?
095   */
096  private final static boolean DEFAULT_CLOSE_STREAM_ON_CLOSE = true;
097
098  /**
099   * A thread-safe mapping between URLs and registration information
100   */
101  private final ConcurrentMap<String, RegistrationInformation> mURLs = new ConcurrentHashMap<String, RegistrationInformation>();
102
103  /**
104   * The singleton Factory object for this class loader.
105   */
106  private final static AVPKitIO mFactory = new AVPKitIO();
107
108  /**
109   * The static constructor just registered the singleton factory under the
110   * DEFAULT_PROTOCOL protocol.
111   */
112  static
113  {
114    registerFactory(DEFAULT_PROTOCOL);
115  }
116
117  /**
118   * Package level constructor. We don't allow people to create their own
119   * version of this factory
120   */
121  AVPKitIO()
122  {
123
124  }
125
126  /**
127   * Register a new protocol name for this factory that AVPKit.IO will use for
128   * the given protocol.
129   * 
130   * <p>
131   * 
132   * A default factory for the protocol {@value #DEFAULT_PROTOCOL} will already
133   * be registered. This just allows you to register the same factory under
134   * different strings if you want.
135   * 
136   * </p>
137   * <p>
138   * 
139   * <strong>NOTE: Protocol can only contain alpha characters.</strong>
140   * 
141   * </p>
142   * 
143   * @param protocolPrefix The protocol (e.g. "yourapphandler").
144   * @return The factory registered
145   */
146
147  static AVPKitIO registerFactory(String protocolPrefix)
148  {
149    URLProtocolManager manager = URLProtocolManager.getManager();
150    manager.registerFactory(protocolPrefix, mFactory);
151    return mFactory;
152  }
153
154  /**
155   * Get the singleton factory object for this class.
156   * 
157   * @return the factory
158   */
159
160  static public AVPKitIO getFactory()
161  {
162    return mFactory;
163  }
164
165  /**
166   * Generates a unique name suitable for using in the map methods for the URL
167   * parameter.
168   * 
169   * @param src The object you want to generate a unique name for, or null if
170   *        you don't have one.
171   * @return A unique name (will be unique across time and space).
172   */
173  static public String generateUniqueName(Object src)
174  {
175    return generateUniqueName(src, null);
176  }
177
178  /**
179   * Generates a unique name suitable for using in the map methods for the URL
180   * parameter.
181   * 
182   * @param src The object you want to generate a unique name for, or null if
183   *        you don't have one.
184   * @param extension an option extension to append to the generated URL.
185   * @return A unique name (will be unique across time and space).
186   */
187  static public String generateUniqueName(Object src, String extension)
188  {
189    StringBuilder builder = new StringBuilder();
190    builder.append(UUID.randomUUID().toString());
191    if (src != null)
192    {
193      builder.append("-");
194      builder.append(src.getClass().getSimpleName());
195      builder.append("-");
196      builder.append(Integer.toHexString(src.hashCode()));
197    }
198    if (extension != null)
199    {
200      builder.append(extension);
201    }
202    return builder.toString();
203  }
204
205
206  /**
207   * Maps a {@link IURLProtocolHandler} to a url that AVPKit can open.
208   * 
209   * {@link #unmap(String)} will be called automatically after this URL is
210   * opened.
211   * 
212   * @param handler the handler
213   * @return a string that is suitable for passing to {@link IContainer}'s open
214   *         methods.
215   */
216  public static String map(IURLProtocolHandler handler)
217  {
218    return map(generateUniqueName(handler), handler, DEFAULT_UNMAP_URL_ON_OPEN);
219  }
220
221  /**
222   * Maps a {@link IURLProtocolHandler} to a url that AVPKit can open.
223   * 
224   * {@link #unmap(String)} will be called automatically after this URL is
225   * opened.
226   * 
227   * @param url the unique string to use for the mapping.
228   * @param handler the handler
229   * @return a string that is suitable for passing to {@link IContainer}'s open
230   *         methods.
231   */
232  public static String map(String url, IURLProtocolHandler handler)
233  {
234    return map(url, handler, DEFAULT_UNMAP_URL_ON_OPEN);
235  }
236
237  /**
238   * Maps a {@link DataInput} object to a URL for use by AVPKit.
239   * 
240   * @param input the {@link DataInput}
241   * @return a string that can be passed to {@link IContainer}'s open methods.
242   */
243  public static String map(DataInput input)
244  {
245    return map(generateUniqueName(input), input, null,
246        DEFAULT_UNMAP_URL_ON_OPEN, DEFAULT_CLOSE_STREAM_ON_CLOSE);
247  }
248
249  /**
250   * Maps a {@link DataInput} object to a URL for use by AVPKit.
251   * 
252   * @param url the URL to use.
253   * @param input the {@link DataInput}
254   * @return a string that can be passed to {@link IContainer}'s open methods.
255   */
256  public static String map(String url, DataInput input)
257  {
258    return map(url, input, null, DEFAULT_UNMAP_URL_ON_OPEN,
259        DEFAULT_CLOSE_STREAM_ON_CLOSE);
260  }
261
262  /**
263   * Maps a {@link DataOutput} object to a URL for use by AVPKit.
264   * 
265   * @param output the {@link DataOutput}
266   * @return a string that can be passed to {@link IContainer}'s open methods.
267   */
268  public static String map(DataOutput output)
269  {
270    return map(generateUniqueName(output), null, output,
271        DEFAULT_UNMAP_URL_ON_OPEN, DEFAULT_CLOSE_STREAM_ON_CLOSE);
272  }
273
274  /**
275   * Maps a {@link DataOutput} object to a URL for use by AVPKit.
276   * 
277   * @param url the URL to use.
278   * @param output the {@link DataOutput}
279   * @return a string that can be passed to {@link IContainer}'s open methods.
280   */
281  public static String map(String url, DataOutput output)
282  {
283    return map(url, null, output, DEFAULT_UNMAP_URL_ON_OPEN,
284        DEFAULT_CLOSE_STREAM_ON_CLOSE);
285  }
286
287  /**
288   * Maps a {@link RandomAccessFile} object to a URL for use by AVPKit.
289   * 
290   * @param file the {@link RandomAccessFile}
291   * @return a string that can be passed to {@link IContainer}'s open methods.
292   */
293  public static String map(RandomAccessFile file)
294  {
295    return map(generateUniqueName(file), file, file, DEFAULT_UNMAP_URL_ON_OPEN,
296        DEFAULT_CLOSE_STREAM_ON_CLOSE);
297  }
298
299  /**
300   * Maps a {@link RandomAccessFile} object to a URL for use by AVPKit.
301   * 
302   * @param url the URL to use.
303   * @param file the {@link RandomAccessFile}
304   * @return a string that can be passed to {@link IContainer}'s open methods.
305   */
306  public static String map(String url, RandomAccessFile file)
307  {
308    return map(url, file, file, DEFAULT_UNMAP_URL_ON_OPEN,
309        DEFAULT_CLOSE_STREAM_ON_CLOSE);
310  }
311
312  /**
313   * Maps a {@link ReadableByteChannel} to a URL for use by AVPKit.
314   * 
315   * @param channel the {@link ReadableByteChannel}
316   * @return a string that can be passed to {@link IContainer}'s open methods.
317   */
318  public static String map(ReadableByteChannel channel)
319  {
320    return map(generateUniqueName(channel), channel, null,
321        DEFAULT_UNMAP_URL_ON_OPEN, DEFAULT_CLOSE_STREAM_ON_CLOSE);
322  }
323
324  /**
325   * Maps a {@link ReadableByteChannel} to a URL for use by AVPKit.
326   * 
327   * @param url the URL to use.
328   * @param channel the {@link ReadableByteChannel}
329   * @return a string that can be passed to {@link IContainer}'s open methods.
330   */
331  public static String map(String url, ReadableByteChannel channel)
332  {
333    return map(url, channel, null, DEFAULT_UNMAP_URL_ON_OPEN,
334        DEFAULT_CLOSE_STREAM_ON_CLOSE);
335  }
336
337  /**
338   * Maps a {@link WritableByteChannel} to a URL for use by AVPKit.
339   * 
340   * @param channel the {@link WritableByteChannel}
341   * @return a string that can be passed to {@link IContainer}'s open methods.
342   */
343  public static String map(WritableByteChannel channel)
344  {
345    return map(generateUniqueName(channel), null, channel,
346        DEFAULT_UNMAP_URL_ON_OPEN, DEFAULT_CLOSE_STREAM_ON_CLOSE);
347  }
348
349  /**
350   * Maps a {@link WritableByteChannel} to a URL for use by AVPKit.
351   * 
352   * @param url the URL to use.
353   * @param channel the {@link WritableByteChannel}
354   * @return a string that can be passed to {@link IContainer}'s open methods.
355   */
356  public static String map(String url, WritableByteChannel channel)
357  {
358    return map(url, null, channel, DEFAULT_UNMAP_URL_ON_OPEN,
359        DEFAULT_CLOSE_STREAM_ON_CLOSE);
360  }
361
362  /**
363   * Maps a {@link ByteChannel} to a URL for use by AVPKit.
364   * 
365   * @param channel the {@link ByteChannel}
366   * @return a string that can be passed to {@link IContainer}'s open methods.
367   */
368  public static String map(ByteChannel channel)
369  {
370    return map(generateUniqueName(channel), channel, channel,
371        DEFAULT_UNMAP_URL_ON_OPEN, DEFAULT_CLOSE_STREAM_ON_CLOSE);
372  }
373
374  /**
375   * Maps a {@link ByteChannel} to a URL for use by AVPKit.
376   * 
377   * @param url the URL to use.
378   * @param channel the {@link ByteChannel}
379   * @return a string that can be passed to {@link IContainer}'s open methods.
380   */
381  public static String map(String url, ByteChannel channel)
382  {
383    return map(url, channel, channel, DEFAULT_UNMAP_URL_ON_OPEN,
384        DEFAULT_CLOSE_STREAM_ON_CLOSE);
385  }
386
387  /**
388   * Maps an {@link InputStream} to a URL for use by AVPKit.
389   * 
390   * @param in the stream
391   * 
392   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
393   *         open method, and will result in IO being performed on the passed in
394   *         streams.
395   */
396
397  public static String map(InputStream in)
398  {
399    return map(generateUniqueName(in), in, null, DEFAULT_UNMAP_URL_ON_OPEN,
400        DEFAULT_CLOSE_STREAM_ON_CLOSE);
401  }
402
403  /**
404   * Maps an {@link InputStream} to a URL for use by AVPKit.
405   * 
406   * @param url the URL to use.
407   * @param in the stream
408   * 
409   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
410   *         open method, and will result in IO being performed on the passed in
411   *         streams.
412   */
413
414  public static String map(String url, InputStream in)
415  {
416    return map(url, in, null, DEFAULT_UNMAP_URL_ON_OPEN,
417        DEFAULT_CLOSE_STREAM_ON_CLOSE);
418  }
419
420  /**
421   * Maps an {@link OutputStream} to a URL for use by AVPKit.
422   * 
423   * @param out the stream
424   * 
425   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
426   *         open method, and will result in IO being performed on the passed in
427   *         streams.
428   */
429
430  public static String map(OutputStream out)
431  {
432    return map(generateUniqueName(out), null, out, DEFAULT_UNMAP_URL_ON_OPEN,
433        DEFAULT_CLOSE_STREAM_ON_CLOSE);
434  }
435
436  /**
437   * Maps an {@link OutputStream} to a URL for use by AVPKit.
438   * 
439   * @param url the URL to use.
440   * @param out the stream
441   * 
442   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
443   *         open method, and will result in IO being performed on the passed in
444   *         streams.
445   */
446
447  public static String map(String url, OutputStream out)
448  {
449    return map(url, null, out, DEFAULT_UNMAP_URL_ON_OPEN,
450        DEFAULT_CLOSE_STREAM_ON_CLOSE);
451  }
452
453  /**
454   * Maps a {@link DataInput} or {@link DataOutput} object to a URL for use by
455   * AVPKit.
456   * 
457   * @param url the url
458   * @param in the input to use
459   * @param out the output to use
460   * @param unmapOnOpen should we remove the mapping as soon as the resulting
461   *        handler is opened?
462   * @param closeOnClose should we call {@link Closeable#close()} on the
463   *        underlying input or output objects when
464   *        {@link IURLProtocolHandler#close()} is called (if supported by the
465   *        underlying object)?
466   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
467   *         open method, and will result in IO being performed on the passed in
468   *         streams.
469   */
470
471  public static String map(String url, DataInput in, DataOutput out,
472      boolean unmapOnOpen, boolean closeOnClose)
473  {
474    return map(url, new DataInputOutputHandler(in, out, closeOnClose));
475  }
476
477  /**
478   * Maps an {@link ReadableByteChannel} or {@link WritableByteChannel} to a URL
479   * for use by AVPKit.
480   * 
481   * @param url the url
482   * @param in the input to use
483   * @param out the output to use
484   * @param unmapOnOpen should we remove the mapping as soon as the resulting
485   *        handler is opened?
486   * @param closeOnClose should we call {@link Closeable#close()} on the
487   *        underlying input or output objects when
488   *        {@link IURLProtocolHandler#close()} is called (if supported by the
489   *        underlying object)?
490   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
491   *         open method, and will result in IO being performed on the passed in
492   *         streams.
493   */
494
495  public static String map(String url, ReadableByteChannel in,
496      WritableByteChannel out, boolean unmapOnOpen, boolean closeOnClose)
497  {
498    return map(url, new ReadableWritableChannelHandler(in, out, closeOnClose));
499  }
500
501  /**
502   * Maps an {@link InputStream} or {@link OutputStream} to a URL for use by
503   * AVPKit.
504   * 
505   * @param url the url
506   * @param in the input to use
507   * @param out the output to use
508   * @param unmapOnOpen should we remove the mapping as soon as the resulting
509   *        handler is opened?
510   * @param closeOnClose should we call {@link Closeable#close()} on the
511   *        underlying input or output objects when
512   *        {@link IURLProtocolHandler#close()} is called?
513   * 
514   * @return Returns a URL that can be passed to AVPKit's {@link IContainer}'s
515   *         open method, and will result in IO being performed on the passed in
516   *         streams.
517   */
518
519  public static String map(String url, InputStream in, OutputStream out,
520      boolean unmapOnOpen, boolean closeOnClose)
521  {
522    return map(url, new InputOutputStreamHandler(in, out, closeOnClose));
523  }
524
525  /**
526   * Maps a {@link IURLProtocolHandler} to a url that AVPKit can open.
527   * 
528   * @param url the unique string to use for the mapping.
529   * @param handler the handler
530   * @param unmapUrlOnOpen if true, when AVPKit opens the
531   *        {@link IURLProtocolHandler}, {@link #unmap(String)} will be called
532   *        automatically.
533   * @return a string that is suitable for passing to {@link IContainer}'s open
534   *         methods.
535   */
536  public static String map(String url, IURLProtocolHandler handler,
537      boolean unmapUrlOnOpen)
538  {
539    if (mFactory.mapIO(url, handler, unmapUrlOnOpen) != null)
540      throw new RuntimeException("url is already mapped: " + url);
541
542    return DEFAULT_PROTOCOL + ":" + URLProtocolManager.getResourceFromURL(url);
543  }
544
545  /**
546   * Undoes a URL to {@link InputStream} or {@link OutputStream} mapping.
547   * Forwards to {@link #getFactory()}.{@link #unmapIO(String)}
548   */
549  public static IURLProtocolHandler unmap(String url)
550  {
551    return mFactory.unmapIO(url);
552  }
553
554  /**
555   * Maps the given url or file name to the given {@link IURLProtocolHandler} or
556   * so that AVPKit calls to open the URL it will call back to the handler.
557   * 
558   * <p>
559   * 
560   * If you set unmapOnOpen to false, or you never actually open this mapping,
561   * then you must ensure that you call {@link #unmapIO(String)} at some point
562   * in time to remove the mapping, or we will hold onto references to the
563   * handler you passed in.
564   * 
565   * </p>
566   * 
567   * @param url A file or URL. If a URL, the protocol will be stripped off and
568   *        replaced with {@link #DEFAULT_PROTOCOL} when registering.
569   * @param handler An {@link IURLProtocolHandler} for the url
570   * @param unmapUrlOnOpen If true, the handler will unmap itself after an
571   *        {@link IContainer} opens the registered URL. If true, you do not
572   *        need to call {@link #unmapIO(String)} for this url.
573   * @return The previous handler for this url, or null if none.
574   * 
575   * 
576   * @throws IllegalArgumentException if url is null or zero length
577   * @throws IllegalArgumentException if handler is null
578   */
579
580  public IURLProtocolHandler mapIO(String url, IURLProtocolHandler handler,
581      boolean unmapUrlOnOpen)
582  {
583    {
584      if (url == null || url.length() <= 0)
585        throw new IllegalArgumentException("must pass in non-zero url");
586      if (handler == null)
587      {
588        throw new IllegalArgumentException("must pass in a non null handler");
589      }
590      String streamName = URLProtocolManager.getResourceFromURL(url);
591      RegistrationInformation tuple = new RegistrationInformation(streamName,
592          handler, unmapUrlOnOpen);
593      RegistrationInformation oldTuple = mURLs.putIfAbsent(streamName, tuple);
594      return oldTuple == null ? null : oldTuple.getHandler();
595    }
596  }
597
598  /**
599   * Unmaps a registration between a URL and the underlying i/o objects.
600   * <p>
601   * If URL contains a protocol it is ignored when trying to find the matching
602   * IO stream.
603   * </p>
604   * 
605   * @param url The stream name to unmap
606   * @return the {@link IURLProtocolHandler} that had been registered for that
607   *         url, or null if none.
608   */
609  public IURLProtocolHandler unmapIO(String url)
610  {
611    if (url == null || url.length() <= 0)
612      throw new IllegalArgumentException("must pass in non-zero url");
613    String streamName = URLProtocolManager.getResourceFromURL(url);
614    RegistrationInformation oldTuple = mURLs.remove(streamName);
615    return oldTuple == null ? null : oldTuple.getHandler();
616  }
617
618  /**
619   * {@inheritDoc}
620   */
621
622  public IURLProtocolHandler getHandler(String url, int flags)
623  {
624    // Note: We need to remove any protocol markers from the url
625    String streamName = URLProtocolManager.getResourceFromURL(url);
626
627    RegistrationInformation tuple = mURLs.get(streamName);
628    if (tuple != null)
629    {
630      IURLProtocolHandler handler = tuple.getHandler();
631      if (tuple.isUnmappingOnOpen())
632      {
633        IURLProtocolHandler oldHandler = unmapIO(tuple.getName());
634
635        // the unmapIO is an atomic operation
636        if (handler != null && !handler.equals(oldHandler))
637        {
638          // someone already unmapped this stream
639          log.error("stream {} already unmapped; it was likely already opened",
640
641          tuple.getName());
642          return null;
643        }
644      }
645      return handler;
646    }
647    return null;
648  }
649
650  /**
651   * A set of information about a registered handler.
652   * 
653   * @author aclarke
654   * 
655   */
656
657  private static class RegistrationInformation
658  {
659    private final String mName;
660    private final boolean mIsUnmappingOnOpen;
661    private final IURLProtocolHandler mHandler;
662
663    public RegistrationInformation(String streamName,
664        IURLProtocolHandler handler, boolean unmapUrlOnOpen)
665    {
666      mName = streamName;
667      mHandler = handler;
668      mIsUnmappingOnOpen = unmapUrlOnOpen;
669    }
670
671    /**
672     * The name of this handler registration, without any protocol.
673     * 
674     * @return the name
675     */
676    public String getName()
677    {
678      return mName;
679    }
680
681    /**
682     * Should the handler unmap the stream after it is opened?
683     * 
684     * @return the decision
685     */
686    public boolean isUnmappingOnOpen()
687    {
688      return mIsUnmappingOnOpen;
689    }
690
691    public IURLProtocolHandler getHandler()
692    {
693      return mHandler;
694    }
695  }
696}