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    @SuppressWarnings("deprecation")
101    public void generate() throws IOException, SAXException, ProcessingException
102    {
103        Response response = ObjectModelHelper.getResponse(objectModel);
104        
105        try (InputStream is = getInputStream())
106        {
107            String fileName = getFilename();
108            int i = fileName.lastIndexOf('.');
109            String fileExtension = i != -1 ? fileName.substring(i + 1).toLowerCase() : __DEFAULT_FORMAT;
110            
111            if (processImage(fileExtension))
112            {
113                String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : __DEFAULT_FORMAT;
114                
115                generateThumbnail(is, outputFormat);
116                
117                if (getLogger().isDebugEnabled())
118                {
119                    getLogger().debug("Image generated: " + ObjectModelHelper.getRequest(objectModel).getRequestURI());
120                }
121            }
122            else
123            {
124                // Copy data in response
125                response.setHeader("Content-Length", Long.toString(getLength()));
126                IOUtils.copy(is, out);
127
128                if (getLogger().isDebugEnabled())
129                {
130                    getLogger().debug("Resource generated: " + ObjectModelHelper.getRequest(objectModel).getRequestURI());
131                }
132            }
133        }
134        finally
135        {
136            IOUtils.closeQuietly(out);
137        }
138    }
139    
140    /**
141     * Generate the thumbnail in {@link OutputStream} out
142     * @param is original image
143     * @param format format of the file
144     * @throws IOException if an error occurs when manipulating streams.
145     */
146    protected void generateThumbnail(InputStream is, String format) throws IOException
147    {
148        ImageHelper.generateThumbnail(is, out, format, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth);
149    }
150    
151    /**
152     * Returns the resource's {@link InputStream}.
153     * @return the resource's {@link InputStream}.
154     */
155    protected abstract InputStream getInputStream();
156    
157    /**
158     * Returns the resource's name.
159     * @return the resource's name.
160     */
161    protected abstract String getFilename();
162    
163    /**
164     * If needed, returns the resource's name, properly encoded for using in a "Content-Disposition" HTTP header.<br>
165     * May be null, in which case the result of {@link #getFilename()} is used instead.
166     * @return the encoded resource's name, if any.
167     */
168    protected abstract String getEncodedFilename();
169    
170    /**
171     * Returns the resource's length.
172     * @return the resource's length.
173     */
174    protected abstract long getLength();
175    
176    
177    /**
178     * Determines if the file is an image and should be processed.
179     * @param fileExtension The file extension to process
180     * @return <code>true</code> if file is a image
181     */
182    protected boolean processImage(String fileExtension)
183    {
184        if (__UNRESIZABLE_FORMATS.contains(fileExtension))
185        {
186            return false;
187        }
188        else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0)
189        {
190            // resize or crop is required, assume this is an image
191            return true;
192        }
193        else if (!_readForDownload)
194        {
195            // only process image if it is for rendering purposes
196            return getMimeType() != null && getMimeType().startsWith("image/");
197        }
198        else
199        {
200            return false;
201        }
202    }
203    
204    /**
205     * Helper method to compute a suffix based on resizing and cropping properties.
206     * @return a String to be used in cache keys.
207     */
208    protected String getKeySuffix()
209    {
210        return "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth + "#" + _cropHeight + "#" + _cropWidth;
211    }
212}