001/*
002 *  Copyright 2019 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.data;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileNotFoundException;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.time.ZonedDateTime;
028
029import org.ametys.plugins.repository.AmetysRepositoryException;
030import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
031
032/**
033 * Class representing a resource
034 */
035public class Resource
036{
037    /** Empty array */
038    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
039
040    /** Max size for keeping temporary data in memory */
041    private static final int MAX_BUFFER_SIZE = 0x10000;
042    
043    /** The resource's mime type */
044    protected String _mimeType;
045    
046    /** The resource's encoding */
047    protected String _encoding;
048    
049    /** The resource's last modification date */
050    protected ZonedDateTime _lastModificationDate;
051    
052    /** Underlying tmp file */
053    protected File _tmpFile;
054
055    /** Buffer for small-sized data */
056    protected byte[] _buffer = EMPTY_BYTE_ARRAY;
057    
058    /** The resource's repository data */
059    protected RepositoryData _repositoryData;
060    
061    /**
062     * Default constructor
063     */
064    public Resource()
065    {
066        // Empty constructor
067    }
068    
069    /**
070     * Constructor to use when reading the resource from the repository
071     * @param repositoryData the repository data containing the resource's data
072     */
073    public Resource(RepositoryData repositoryData)
074    {
075        _repositoryData = repositoryData;
076    }
077
078    /**
079     * Retrieve the resource's data
080     * If the data has already been read, the stream is reseted so it can be read again from the start
081     * @return the resource's data, or <code>null</code> if there is no data
082     */
083    public InputStream getInputStream()
084    {
085        if (_repositoryData != null)
086        {
087            if (_repositoryData.hasValue("data", "jcr"))
088            {
089                return _repositoryData.getStream("data", "jcr");
090            }
091            else
092            {
093                return null;
094            }
095        }
096        else if (_tmpFile != null)
097        {
098            try 
099            {
100                // this instance is backed by a temp file
101                return new FileInputStream(_tmpFile);
102            }
103            catch (FileNotFoundException e)
104            {
105                return null;
106            }
107        }
108        else if (_buffer.length > 0)
109        {
110            // this instance is backed by an in-memory buffer
111            return new ByteArrayInputStream(_buffer);
112        }
113        else
114        {
115            return null;
116        }
117    }
118
119    /**
120     * Retrieves the length of the resource's data
121     * @return the length of the resource's data
122     * @throws AmetysRepositoryException if an error occurs while reading the data in the repository
123     */
124    public long getLength() throws AmetysRepositoryException
125    {
126        if (_repositoryData != null)
127        {
128            return _repositoryData.getStreamLength("data", "jcr");
129        }
130        else if (_tmpFile != null)
131        {
132            return _tmpFile.length();
133        }
134        else
135        {
136            return _buffer.length;
137        }
138    }
139    
140    /**
141     * Sets the resource's data
142     * @param in the data to set
143     * @throws IOException if an error occurs while reading the input stream
144     */
145    public void setInputStream(InputStream in) throws IOException
146    {
147        byte[] spoolBuffer = new byte[0x2000];
148        int read;
149        int len = 0;
150        @SuppressWarnings("resource")
151        OutputStream out = null;
152        File spoolFile = null;
153        try 
154        {
155            while ((read = in.read(spoolBuffer)) > 0)
156            {
157                if (out != null)
158                {
159                    // spool to temporary file
160                    out.write(spoolBuffer, 0, read);
161                    len += read;
162                }
163                else if (len + read > MAX_BUFFER_SIZE)
164                {
165                    // threshold for keeping data in memory exceeded;
166                    // create temporary file and spool buffer contents
167                    spoolFile = File.createTempFile("bin", null, null);
168                    spoolFile.deleteOnExit();
169                    out = new FileOutputStream(spoolFile);
170                    out.write(_buffer, 0, len);
171                    out.write(spoolBuffer, 0, read);
172                    _buffer = null;
173                    len += read;
174                }
175                else
176                {
177                    // reallocate new buffer and spool old buffer contents
178                    byte[] newBuffer = new byte[len + read];
179                    System.arraycopy(_buffer, 0, newBuffer, 0, len);
180                    System.arraycopy(spoolBuffer, 0, newBuffer, len, read);
181                    _buffer = newBuffer;
182                    len += read;
183                }
184            }
185        }
186        finally
187        {
188            if (out != null)
189            {
190                out.close();
191            }
192        }
193
194        // init fields
195        if (_tmpFile != null)
196        {
197            _tmpFile.delete();
198        }
199        _tmpFile = spoolFile;
200        _repositoryData = null;
201    }
202
203    /**
204     * Retrieves an output stream that allows to modify the resource's data
205     * @return the output stream
206     */
207    public OutputStream getOutputStream()
208    {
209        return new ByteArrayOutputStream()
210        {
211            @Override
212            public void close() throws IOException
213            {
214                super.close();
215                closeOutputStream(this);
216            }
217        };
218    }
219    
220    /**
221     * Closes the given {@link OutputStream}
222     * @param outputStream the {@link OutputStream} to close
223     * @throws IOException if an error occurs while registering the stream
224     */
225    protected void closeOutputStream(ByteArrayOutputStream outputStream) throws IOException
226    {
227        _buffer = outputStream.toByteArray();
228        if (_tmpFile != null)
229        {
230            _tmpFile.delete();
231        }
232        _repositoryData = null;
233    }
234    
235    /**
236     * Retrieves the mime type of the resource's data
237     * @return the mime type of the resource's data
238     */
239    public String getMimeType()
240    {
241        return _mimeType;
242    }
243
244    /**
245     * Sets the mime type of the resource's data
246     * @param mimeType the mime type to set
247     */
248    public void setMimeType(String mimeType)
249    {
250        _mimeType = mimeType;
251    }
252
253    /**
254     * Retrieves the encoding of the resource's data
255     * @return the encoding of the resource's data
256     */
257    public String getEncoding()
258    {
259        return _encoding;
260    }
261
262    /**
263     * Sets the encoding of the resource's data
264     * @param encoding the encoding to set
265     */
266    public void setEncoding(String encoding)
267    {
268        _encoding = encoding;
269    }
270
271    /**
272     * Retrieves the last modification date of the resource's data
273     * @return the last modification date of the resource's data
274     */
275    public ZonedDateTime getLastModificationDate()
276    {
277        return _lastModificationDate;
278    }
279
280    /**
281     * Sets the last modification date of the resource's data
282     * @param lastModificationDate the last modification date to set
283     */
284    public void setLastModificationDate(ZonedDateTime lastModificationDate)
285    {
286        _lastModificationDate = lastModificationDate;
287    }
288}