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 *******************************************************************************/
019package com.avpkit.core.io;
020
021import java.io.Closeable;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029/**
030 * Implementation of URLProtocolHandler that can read from {@link InputStream}
031 * objects or write to {@link OutputStream} objects.
032 * 
033 * <p>
034 * 
035 * The {@link IURLProtocolHandler#URL_RDWR} mode is not supported, and
036 * {@link #isStreamed(String, int)} will always return true.
037 * 
038 * </p>
039 * 
040 * @author aclarke
041 * 
042 */
043
044public class InputOutputStreamHandler implements IURLProtocolHandler
045{
046  private final Logger log = LoggerFactory.getLogger(this.getClass());
047
048  /**
049   * Whether or not this class will default calling
050   * {@link Closeable#close()} on a stream when
051   * {@link #close()} is called on this method.  Can be
052   * overriden in constructors.
053   */
054  public final static boolean CLOSE_STREAM_ON_CLOSE=true;
055  
056  private final InputStream mInputStream;
057  private long length = -1;
058  private final OutputStream mOutputStream;
059  private final boolean mCloseStreamOnClose;
060  private Closeable mOpenStream = null;
061  
062  /**
063   * Create a handler that can only be opened for reading.
064   * @param stream an input stream
065   * 
066   * @throws IllegalArgumentException if stream is null
067   */
068  public InputOutputStreamHandler(InputStream stream)
069  {
070    this(stream, null, CLOSE_STREAM_ON_CLOSE);
071  }
072  
073  /**
074   * Create a handler that can only be opened for writing.
075   * @param stream an output stream
076   * @throws IllegalArgumentException if stream is null
077   */
078  public InputOutputStreamHandler(OutputStream stream)
079  {
080    this(null, stream, CLOSE_STREAM_ON_CLOSE);
081  }
082  
083  /**
084   * Creates a new handler.  If you pass in non null arguments
085   * for both in and out, the handler may be opened in either
086   * {@link IURLProtocolHandler#URL_RDONLY_MODE} or
087   * {@link IURLProtocolHandler#URL_WRONLY_MODE}, but not at
088   * the same time.
089   * <p>
090   * {@link IURLProtocolHandler#URL_RDWR} mode is not supported
091   * by this handler.
092   * </p>
093   * @param in The stream to read from if asked to.
094   * @param out The stream to write from if asked to.
095   * @param closeStreamOnClose Whether or not to automatically
096   *   call {@link Closeable#close()} on the given stream
097   *   when {@link #close()} is called.
098   * @throws IllegalArgumentException if both in and out are null
099   */
100  public InputOutputStreamHandler(InputStream in, OutputStream out,
101      boolean closeStreamOnClose)
102  {
103    if (in == null && out == null)
104      throw new IllegalArgumentException("must pass one non null stream");
105    mInputStream = in;
106    mOutputStream = out;
107    mCloseStreamOnClose = closeStreamOnClose;
108    if (mInputStream instanceof ISizeable){
109      length = ((ISizeable)mInputStream).getLength();
110    }
111  }
112
113
114  /**
115   * {@inheritDoc}
116   */
117
118  public int close()
119  {
120    int retval = 0;
121    try
122    {
123      if (mOpenStream != null && mCloseStreamOnClose)
124      {
125        mOpenStream.close();
126      }
127    }
128    catch (IOException e)
129    {
130      log.error("could not close stream {}: {}", mOpenStream, e);
131      retval = -1;
132    }
133    mOpenStream = null;
134    return retval;
135  }
136
137  /**
138   * {@inheritDoc}
139   */
140
141  public int open(String url, int flags)
142  {
143    if (mOpenStream != null)
144    {
145      log.debug("attempting to open already open handler: {}",
146          mOpenStream);
147      return -1;
148    }
149    switch (flags)
150    {
151      case URL_RDWR:
152        log.debug("do not support read/write mode for Java IO Handlers");
153        return -1;
154      case URL_WRONLY_MODE:
155        mOpenStream = mOutputStream;
156        if (mOpenStream == null)
157        {
158          log.error("No OutputStream specified for writing: {}",url);
159          return -1;
160        }
161        break;
162      case URL_RDONLY_MODE:
163        mOpenStream = mInputStream;
164        if (mOpenStream == null)
165        {
166          log.error("No InputStream specified for reading: {}", url);
167          return -1;
168        }
169        break;
170      default:
171        log.error("Invalid flag passed to open: {}", url);
172        return -1;
173    }
174
175    return 0;
176  }
177
178  /**
179   * {@inheritDoc}
180   */
181
182  public int read(byte[] buf, int size)
183  {
184    int ret = -1;
185    if (mOpenStream == null || !(mOpenStream instanceof InputStream))
186      return -1;
187
188    try
189    {
190      InputStream stream = (InputStream) mOpenStream;
191      ret = stream.read(buf, 0, size);
192      return ret;
193    }
194    catch (IOException e)
195    {
196      log.error("Got IO exception reading from stream: {}; {}",
197          mOpenStream, e);
198      return -1;
199    }
200  }
201
202  /**
203   * {@inheritDoc}
204   * 
205   * This method is not supported on this class and always return -1;
206   */
207
208  public long seek(long offset, int whence)
209  {
210    if (mOpenStream == null || !(mOpenStream instanceof InputStream))
211      return -1;
212    try
213    {
214      InputStream stream = (InputStream) mOpenStream;
215      if (stream.markSupported()){
216          switch (whence){
217              case SEEK_CUR:
218                  return stream.skip(offset);
219              case SEEK_SET:
220                  stream.reset();
221                  return stream.skip(offset);
222              case SEEK_SIZE:
223                  return length;
224              case SEEK_END:
225                  return -1;//not supported
226              default:
227                  return -1;
228          }
229      }else{
230          return -1;
231      }
232    }
233    catch (IOException e)
234    {
235      log.error("Got IO exception reading from stream: {}; {}",
236          mOpenStream, e);
237      return -1;
238    }
239  }
240
241  /**
242   * {@inheritDoc}
243   */
244
245  public int write(byte[] buf, int size)
246  {
247    if (mOpenStream == null ||
248        !(mOpenStream instanceof OutputStream))
249      return -1;
250
251    try
252    {
253      OutputStream stream = (OutputStream) mOpenStream;
254      stream.write(buf, 0, size);
255      return size;
256    }
257    catch (IOException e)
258    {
259      log.error("Got error writing to file: {}; {}", mOpenStream, e);
260      return -1;
261    }
262  }
263
264  /**
265   * {@inheritDoc}
266   * 
267   */
268
269  public boolean isStreamed(String url, int flags)
270  {
271    return length==-1 || (mInputStream != null ? !mInputStream.markSupported() : true);
272  }
273  /**
274   * Returns the stream we'd input from if asked.
275   * @return the inputStream
276   */
277  public InputStream getInputStream()
278  {
279    return mInputStream;
280  }
281  /**
282   * Returns the stream we'd output to if asked.
283   * @return the outputStream
284   */
285  public OutputStream getOutputStream()
286  {
287    return mOutputStream;
288  }
289  /**
290   * Returns the stream currently {@link #open(String, int)}.
291   * @return the openStream
292   */
293  public Closeable getOpenStream()
294  {
295    return mOpenStream;
296  }
297
298  /**
299   * Will this handler call {@link Closeable#close()} automatically
300   * when its {@link #close()} method is called by AVPKit?
301   * 
302   * @return the closeStreamOnClose setting
303   */
304  public boolean isCloseStreamOnClose()
305  {
306    return mCloseStreamOnClose;
307  }
308
309}