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 objModel, String src, Parameters setupParameters) throws ProcessingException, SAXException, IOException 080 { 081 super.setup(res, objModel, src, setupParameters); 082 083 try 084 { 085 _resourceHandler = _resourcesHandlerEP.getResourceHandler(src); 086 } 087 catch (Exception e) 088 { 089 throw new ProcessingException("Exception while retrieving resource handler for resource '" + src + "'", e); 090 } 091 092 _readForDownload = setupParameters.getParameterAsBoolean("download", false); 093 _source = _resourceHandler.setup(src, objModel, setupParameters, _readForDownload); 094 095 // Minimizer does not receive the real lastModified through sitemap source 096 @SuppressWarnings("unchecked") 097 Map<String, Object> params = (Map<String, Object>) objModel.get(ObjectModelHelper.PARENT_CONTEXT); 098 if (params != null) 099 { 100 params.put(LAST_MODIFIED, getLastModified()); 101 } 102 103 Response response = ObjectModelHelper.getResponse(objModel); 104 105 if (_resourceHandler.acceptRanges()) 106 { 107 response.setHeader("Accept-Ranges", "bytes"); 108 109 Request request = ObjectModelHelper.getRequest(objModel); 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}