001/*
002 *  Copyright 2016 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.resources;
017
018import java.io.IOException;
019import java.io.Serializable;
020import java.util.Map;
021
022import org.apache.avalon.framework.context.Context;
023import org.apache.avalon.framework.context.ContextException;
024import org.apache.avalon.framework.context.Contextualizable;
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.cocoon.Constants;
030import org.apache.cocoon.ProcessingException;
031import org.apache.cocoon.caching.CacheableProcessingComponent;
032import org.apache.cocoon.components.LifecycleHelper;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.environment.Response;
036import org.apache.cocoon.environment.http.HttpResponse;
037import org.apache.cocoon.reading.AbstractReader;
038import org.apache.excalibur.source.Source;
039import org.apache.excalibur.source.SourceResolver;
040import org.apache.excalibur.source.SourceValidity;
041import org.xml.sax.SAXException;
042
043import org.ametys.core.cocoon.source.NamedSource;
044import org.ametys.core.util.URIUtils;
045
046/**
047 * Default resource reader, that handle different resources type using the ResourcesExtensionPoint.
048 */
049public class ResourceReader extends AbstractReader implements CacheableProcessingComponent, Serviceable, Contextualizable
050{
051    /** last modified parameter name for resources parameters */
052    public static final String LAST_MODIFIED = "lastModified";
053    
054    private SourceResolver _resolver;
055    private org.apache.cocoon.environment.Context _cocoonContext;
056
057    private ResourceHandlerProviderExtensionPoint _resourcesHandlerEP;
058    private ResourceHandler _resourceHandler;
059
060    private Source _source;
061    private boolean _readForDownload;
062    
063    private boolean _processRange;
064    private long _rangeStart;
065    private long _rangeEnd;
066    
067    public void contextualize(Context context) throws ContextException
068    {
069        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
070    }
071    
072    public void service(ServiceManager sManager) throws ServiceException
073    {
074        _resourcesHandlerEP = (ResourceHandlerProviderExtensionPoint) sManager.lookup(ResourceHandlerProviderExtensionPoint.ROLE);
075        _resolver = (SourceResolver) sManager.lookup(SourceResolver.ROLE);
076    }
077    
078    @Override
079    public void setup(org.apache.cocoon.environment.SourceResolver res, Map objectModel, String source, Parameters parameters) throws ProcessingException, SAXException, IOException
080    {
081        super.setup(res, objectModel, source, parameters);
082        
083        try
084        {
085            _resourceHandler = _resourcesHandlerEP.getResourceHandler(source);
086        }
087        catch (Exception e)
088        {
089            throw new ProcessingException("Exception while retrieving resource handler for resource '" + source + "'", e);
090        }
091        
092        _readForDownload = parameters.getParameterAsBoolean("download", false);
093        _source = _resourceHandler.setup(source, objectModel, parameters, _readForDownload);
094        
095        // Minimizer does not receive the real lastModified through sitemap source 
096        @SuppressWarnings("unchecked")
097        Map<String, Object> params = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
098        if (params != null)
099        {
100            params.put(LAST_MODIFIED, getLastModified());
101        }
102        
103        Response response = ObjectModelHelper.getResponse(objectModel);
104
105        if (_resourceHandler.acceptRanges())
106        {
107            response.setHeader("Accept-Ranges", "bytes");
108            
109            Request request = ObjectModelHelper.getRequest(objectModel);
110            String range = request.getHeader("Range");
111            if (range != null)
112            {
113                try
114                {
115                    _parseRange(range.trim());
116                    _processRange = !(_rangeStart == 0 && _rangeEnd == Long.MAX_VALUE); // only process range if it's not the whole resource
117                }
118                catch (IllegalArgumentException e)
119                {
120                    ((HttpResponse) response).setStatus(416); // Range not satisfiable
121                    getLogger().error("Illegal range request: " + e.getMessage());
122                }
123            }
124        }
125        else
126        {
127            response.setHeader("Accept-Ranges", "none");
128        }
129        
130        if (_readForDownload)
131        {
132            String name = _source instanceof NamedSource ? URIUtils.encodeHeader(((NamedSource) _source).getName()) : null;
133            response.setHeader("Content-Disposition", "attachment" + (name != null ? ";filename=\"" + name + "\";filename*=UTF-8''" + name : ""));
134        }
135    }
136
137    @Override
138    public void generate() throws IOException, ProcessingException
139    {
140        Response response = ObjectModelHelper.getResponse(objectModel);
141        
142        long contentLength = _resourceHandler.getLength();
143        
144        if (!_processRange || contentLength == -1)
145        {
146            if (contentLength != -1) 
147            {
148                response.setHeader("Content-Length", Long.toString(contentLength));
149            }
150            
151            _resourceHandler.generate(out);
152        }
153        else
154        {
155            ((HttpResponse) response).setStatus(206); // partial content
156            
157            long actualEnd = _rangeEnd == Long.MAX_VALUE ? contentLength - 1 : _rangeEnd;
158            long length = actualEnd - _rangeStart + 1;
159            
160            response.setHeader("Content-Range", "bytes " + _rangeStart + "-" + actualEnd + "/" + contentLength);
161            response.setHeader("Content-Length", Long.toString(length));
162            
163            _resourceHandler.generate(out, _rangeStart, length);
164        }
165        
166        out.flush();
167    }
168
169    @Override
170    public Serializable getKey()
171    {
172        if (_processRange)
173        {
174            return null;
175        }
176        
177        return _resourceHandler.getKey();
178    }
179    
180    @Override
181    public SourceValidity getValidity()
182    {
183        return _resourceHandler.getValidity();
184    }
185
186    @Override
187    public void recycle()
188    {
189        super.recycle();
190        
191        _resolver.release(_source);
192        LifecycleHelper.dispose(_resourceHandler);
193        
194        _processRange = false;
195        _rangeStart = 0;
196        _rangeEnd = 0;
197    }
198    
199    @Override
200    public String getMimeType()
201    {
202        String sourceMimeType = _source.getMimeType();
203        
204        if (sourceMimeType != null)
205        {
206            return sourceMimeType;
207        }
208        
209        if (_cocoonContext != null) 
210        {
211            final String mimeType = _cocoonContext.getMimeType(_source.getURI());
212            
213            if (mimeType != null) 
214            {
215                return mimeType;
216            }
217        }
218        
219        return null;
220    }
221    
222    @Override
223    public long getLastModified()
224    {
225        return _resourceHandler.getLastModified();
226    }
227    
228    private void _parseRange(String requestedRange)
229    {
230        if (!requestedRange.startsWith("bytes="))
231        {
232            throw new IllegalArgumentException("Ranges are only accepted for bytes: " + requestedRange);
233        }
234        
235        String range = requestedRange.substring("bytes=".length());
236
237        if (range.contains(","))
238        {
239            throw new IllegalArgumentException("Only single-values ranges are allowed: " + range);
240        }
241        
242        int i = range.indexOf('-');
243        if (i < 0)
244        {
245            throw new IllegalArgumentException("Wrong format for range: " + range);
246        }
247        
248        if (i == 0)
249        {
250            throw new IllegalArgumentException("Suffix ranges not supported: " + range);
251        }
252        
253        _rangeStart = Long.parseLong(range.substring(0, i));
254        
255        int length = range.length();
256        if (i < length - 1) 
257        {
258            _rangeEnd = Integer.parseInt(range.substring(i + 1, length));
259        } 
260        else 
261        {
262            _rangeEnd = Long.MAX_VALUE;
263        }
264        
265        if (_rangeStart > _rangeEnd) 
266        {
267            throw new IllegalArgumentException("Start value is greater than end value in range: " + range);
268        }
269    }
270}