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