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.core.util.cocoon;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.OutputStream;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.cocoon.ProcessingException;
028import org.apache.cocoon.environment.ObjectModelHelper;
029import org.apache.cocoon.environment.Response;
030import org.apache.cocoon.environment.SourceResolver;
031import org.apache.cocoon.reading.AbstractReader;
032import org.apache.commons.io.IOUtils;
033import org.xml.sax.SAXException;
034
035import org.ametys.core.util.ImageHelper;
036import org.ametys.core.util.URIUtils;
037
038/**
039 * Abstract reader for Ametys resources.
040 * If the resource is an image, this reader handles resizing and cropping.
041 */
042public abstract class AbstractResourceReader extends AbstractReader
043{
044    /** Default returned format */
045    protected static final String __DEFAULT_FORMAT = "png";
046    
047    /** List of allowed output formats */
048    protected static final Collection<String> __ALLOWED_OUTPUT_FORMATS = Arrays.asList("png", "gif", "jpg", "jpeg");
049    /** List of format that cannot be resized */
050    protected static final Collection<String> __UNRESIZABLE_FORMATS = Set.of("svg");
051
052    private int _width;
053    private int _height;
054    private int _maxWidth;
055    private int _maxHeight;
056    private int _cropWidth;
057    private int _cropHeight;
058    
059    private boolean _readForDownload;
060    
061    @Override
062    public void setup(SourceResolver sresolver, Map sObjectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
063    {
064        super.setup(sresolver, sObjectModel, src, par);
065        
066        doSetup(sresolver, sObjectModel, src, par);
067        
068        _readForDownload = par.getParameterAsBoolean("download", false);
069        Response response = ObjectModelHelper.getResponse(sObjectModel);
070
071        if (_readForDownload)
072        {
073            String name = getFilename();
074            String encodedName = getEncodedFilename();
075            encodedName = encodedName != null ? URIUtils.encodeHeader(encodedName) : null;
076            
077            response.setHeader("Content-Disposition", "attachment; filename=\"" + (encodedName != null ? encodedName : name) + "\"" + (encodedName != null ? ";filename*=UTF-8''" + encodedName : ""));
078        }
079        
080        // parameters for image resizing
081        _width = par.getParameterAsInteger("width", 0);
082        _height = par.getParameterAsInteger("height", 0);
083        _maxWidth = par.getParameterAsInteger("maxWidth", 0);
084        _maxHeight = par.getParameterAsInteger("maxHeight", 0);
085        _cropWidth = par.getParameterAsInteger("cropWidth", 0);
086        _cropHeight = par.getParameterAsInteger("cropHeight", 0);
087    }
088
089    /**
090     * Called by {@link #setup(SourceResolver, Map, String, Parameters)}. This method should be implemented by subclasses to retrieve the actual resource.
091     * @param res the {@link SourceResolver}.
092     * @param objModel the Cocoon's object model.
093     * @param src the source, as given by the sitemap.
094     * @param par the parameters, as given by the sitemap.
095     * @throws ProcessingException if an error occurs while processing the resource.
096     * @throws IOException if an error occurs while accessing the resource.
097     */
098    protected abstract void doSetup(SourceResolver res, Map objModel, String src, Parameters par) throws ProcessingException, IOException;
099    
100    public void generate() throws IOException, SAXException, ProcessingException
101    {
102        Response response = ObjectModelHelper.getResponse(objectModel);
103        
104        try (InputStream is = getInputStream())
105        {
106            String fileName = getFilename();
107            int i = fileName.lastIndexOf('.');
108            String fileExtension = i != -1 ? fileName.substring(i + 1).toLowerCase() : __DEFAULT_FORMAT;
109            
110            if (processImage(fileExtension))
111            {
112                String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : __DEFAULT_FORMAT;
113                
114                generateThumbnail(is, outputFormat);
115                
116                if (getLogger().isDebugEnabled())
117                {
118                    getLogger().debug("Image generated: " + ObjectModelHelper.getRequest(objectModel).getRequestURI());
119                }
120            }
121            else
122            {
123                // Copy data in response
124                response.setHeader("Content-Length", Long.toString(getLength()));
125                IOUtils.copy(is, out);
126
127                if (getLogger().isDebugEnabled())
128                {
129                    getLogger().debug("Resource generated: " + ObjectModelHelper.getRequest(objectModel).getRequestURI());
130                }
131            }
132        }
133        finally
134        {
135            IOUtils.closeQuietly(out);
136        }
137    }
138    
139    /**
140     * Generate the thumbnail in {@link OutputStream} out
141     * @param is original image
142     * @param format format of the file
143     * @throws IOException if an error occurs when manipulating streams.
144     */
145    protected void generateThumbnail(InputStream is, String format) throws IOException
146    {
147        ImageHelper.generateThumbnail(is, out, format, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth);
148    }
149    
150    /**
151     * Returns the resource's {@link InputStream}.
152     * @return the resource's {@link InputStream}.
153     */
154    protected abstract InputStream getInputStream();
155    
156    /**
157     * Returns the resource's name.
158     * @return the resource's name.
159     */
160    protected abstract String getFilename();
161    
162    /**
163     * If needed, returns the resource's name, properly encoded for using in a "Content-Disposition" HTTP header.<br>
164     * May be null, in which case the result of {@link #getFilename()} is used instead.
165     * @return the encoded resource's name, if any.
166     */
167    protected abstract String getEncodedFilename();
168    
169    /**
170     * Returns the resource's length.
171     * @return the resource's length.
172     */
173    protected abstract long getLength();
174    
175    
176    /**
177     * Determines if the file is an image and should be processed.
178     * @param fileExtension The file extension to process
179     * @return <code>true</code> if file is a image
180     */
181    protected boolean processImage(String fileExtension)
182    {
183        if (__UNRESIZABLE_FORMATS.contains(fileExtension))
184        {
185            return false;
186        }
187        else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0)
188        {
189            // resize or crop is required, assume this is an image
190            return true;
191        }
192        else if (!_readForDownload)
193        {
194            // only process image if it is for rendering purposes
195            return getMimeType() != null && getMimeType().startsWith("image/");
196        }
197        else
198        {
199            return false;
200        }
201    }
202    
203    /**
204     * Helper method to compute a suffix based on resizing and cropping properties.
205     * @return a String to be used in cache keys.
206     */
207    protected String getKeySuffix()
208    {
209        return "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth + "#" + _cropHeight + "#" + _cropWidth;
210    }
211}