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