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