001/*
002 *  Copyright 2010 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.content;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.Serializable;
021import java.net.URLDecoder;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.avalon.framework.parameters.ParameterException;
029import org.apache.avalon.framework.parameters.Parameters;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.caching.CacheableProcessingComponent;
034import org.apache.cocoon.environment.ObjectModelHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.Response;
037import org.apache.cocoon.environment.SourceResolver;
038import org.apache.cocoon.reading.ServiceableReader;
039import org.apache.commons.io.IOUtils;
040import org.apache.commons.lang.StringUtils;
041import org.apache.excalibur.source.SourceValidity;
042import org.xml.sax.SAXException;
043
044import org.ametys.cms.repository.Content;
045import org.ametys.core.util.ImageHelper;
046import org.ametys.core.util.cocoon.InvalidSourceValidity;
047import org.ametys.plugins.repository.AmetysObjectResolver;
048import org.ametys.plugins.repository.metadata.BinaryMetadata;
049import org.ametys.plugins.repository.metadata.CompositeMetadata;
050import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType;
051import org.ametys.plugins.repository.metadata.MetadataAwareAmetysObject;
052import org.ametys.plugins.repository.version.VersionableAmetysObject;
053
054/**
055 * Reader for binary or file metadata.
056 */
057public class FileReader extends ServiceableReader implements CacheableProcessingComponent
058{
059    
060    /** The ametys object resolver. */
061    protected AmetysObjectResolver _resolver;
062    
063    private int _width;
064    private int _height;
065    private int _maxWidth;
066    private int _maxHeight;
067    private int _cropWidth;
068    private int _cropHeight;
069    
070    private boolean _readForDownload;
071    
072    private MetadataAwareAmetysObject _ametysObject;
073    private BinaryMetadata _binary;
074    
075    private String _path;
076    private Collection<String> _allowedFormats = Arrays.asList(new String[]{"png", "gif", "jpg", "jpeg"});
077    
078    @Override
079    public void service(ServiceManager serviceManager) throws ServiceException
080    {
081        super.service(serviceManager);
082        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
083    }
084    
085    @Override
086    public void setup(SourceResolver res, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
087    {
088        super.setup(res, objModel, src, par);
089        
090        _ametysObject = getAmetysObject();
091        
092        CompositeMetadata metadata = _ametysObject.getMetadataHolder();
093        
094        try
095        {
096            _path = parameters.getParameter("path");
097        }
098        catch (ParameterException e)
099        {
100            throw new ProcessingException("The path parameter is mandatory for reading binary metadata.", e);
101        }
102        
103        List<String> pathElements = Arrays.asList(_path.split("/"));
104        
105        Iterator<String> it = pathElements.iterator();
106        
107        while (it.hasNext())
108        {
109            String pathElement = it.next();
110            
111            if (it.hasNext())
112            {
113                // not the last segment : it is a composite
114                metadata = metadata.getCompositeMetadata(URLDecoder.decode(pathElement, "UTF-8"));
115            }
116            else
117            {
118                String metadataName = URLDecoder.decode(pathElement, "UTF-8");
119                if (metadata.getType(metadataName) != MetadataType.BINARY)
120                {
121                    throw new UnsupportedOperationException("Only binary metadata are allowed");
122                }
123                
124                _binary = metadata.getBinaryMetadata(metadataName);
125            }
126        }
127        
128        _readForDownload = par.getParameterAsBoolean("download", false);
129        
130        // parameters for image resizing
131        _width = par.getParameterAsInteger("width", 0);
132        _height = par.getParameterAsInteger("height", 0);
133        _maxWidth = par.getParameterAsInteger("maxWidth", 0);
134        _maxHeight = par.getParameterAsInteger("maxHeight", 0);
135        _cropWidth = par.getParameterAsInteger("cropWidth", 0);
136        _cropHeight = par.getParameterAsInteger("cropHeight", 0);
137    }
138
139    /**
140     * Get the ametys object.
141     * @return the ametys object.
142     * @throws ProcessingException if the information provided was insufficient to resolve an object.
143     */
144    protected MetadataAwareAmetysObject getAmetysObject() throws ProcessingException
145    {
146        MetadataAwareAmetysObject object = null;
147        
148        Request request = ObjectModelHelper.getRequest(objectModel);
149        String objectPath = parameters.getParameter("objectPath", request.getParameter("objectPath"));
150        String objectId = parameters.getParameter("objectId", request.getParameter("objectId"));
151        // Legacy parameter name.
152        if (StringUtils.isBlank(objectId))
153        {
154            objectId = parameters.getParameter("contentId", request.getParameter("contentId"));
155        }
156        
157        if (StringUtils.isNotBlank(objectId))
158        {
159            object = _resolver.resolveById(objectId);
160        }
161        else if (StringUtils.isNotBlank(objectPath))
162        {
163            objectPath = objectPath.replaceAll("%3A", ":");
164            object = _resolver.resolveByPath(objectPath);
165        }
166        else
167        {
168            object = (Content) request.getAttribute(Content.class.getName());
169        }
170        
171        if (object == null)
172        {
173            throw new ProcessingException("The object ID parameter is mandatory for reading binary metadata.");
174        }
175        
176        String revision = parameters.getParameter("contentVersion", null);
177        if (StringUtils.isNotEmpty(revision) && object instanceof VersionableAmetysObject)
178        {
179            ((VersionableAmetysObject) object).switchToRevision(revision);
180        }
181        
182        return object;
183    }
184
185    @Override
186    public Serializable getKey()
187    {
188        return _path + "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth + "#" + _cropHeight + "#" + _cropWidth;
189    }
190
191    @Override
192    public SourceValidity getValidity()
193    {
194        return new InvalidSourceValidity();
195    }
196    
197    @Override
198    public long getLastModified()
199    {
200        if (_binary != null)
201        {
202            return _binary.getLastModified().getTime();
203        }
204        
205        return super.getLastModified();
206    }
207    
208    @Override
209    public String getMimeType()
210    {
211        if (_binary != null)
212        {
213            return _binary.getMimeType();
214        }
215
216        return super.getMimeType();
217    }
218
219    @Override
220    public void generate() throws IOException, SAXException, ProcessingException
221    {
222        Response response = ObjectModelHelper.getResponse(objectModel);
223        
224        if (_readForDownload)
225        {
226            response.setHeader("Content-Disposition", "attachment; filename=\"" + _binary.getFilename() + "\"");
227        }
228        
229        try (InputStream is = _binary.getInputStream())
230        {
231            if (_isImage())
232            {
233                // it's an image (which must be resized or not)
234                int i = _binary.getFilename().lastIndexOf('.');
235                String format = i != -1 ? _binary.getFilename().substring(i + 1) : "png";
236                format = _allowedFormats.contains(format) ? format : "png";
237                
238                ImageHelper.generateThumbnail(is, out, format, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth);
239            }
240            else
241            {
242                response.setHeader("Content-Length", Long.toString(_binary.getLength()));
243
244                // Copy data in response
245                IOUtils.copy(is, out);
246            }
247        }
248        finally
249        {
250            IOUtils.closeQuietly(out);
251        }
252    }
253    
254    @Override
255    public void recycle()
256    {
257        super.recycle();
258        
259        _binary = null;
260        _ametysObject = null;
261    }
262    
263    /**
264     * Determines if the file is an image
265     * @return <code>true</code> if file is a image
266     */
267    protected boolean _isImage()
268    {
269        if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0)
270        {
271            // resize is required, assume this is a image
272            return true;
273        }
274        else
275        {
276            return getMimeType() != null && getMimeType().startsWith("image/");
277        }
278    }
279}