001 package railo.runtime.tag; 002 003 import java.io.BufferedInputStream; 004 import java.io.ByteArrayInputStream; 005 import java.io.IOException; 006 import java.io.InputStream; 007 import java.io.OutputStream; 008 import java.io.PrintWriter; 009 import java.util.Enumeration; 010 import java.util.zip.GZIPOutputStream; 011 012 import javax.servlet.http.HttpServletRequest; 013 import javax.servlet.http.HttpServletResponse; 014 015 import railo.commons.io.IOUtil; 016 import railo.commons.io.res.Resource; 017 import railo.commons.io.res.util.ResourceUtil; 018 import railo.commons.lang.StringUtil; 019 import railo.commons.lang.SystemOut; 020 import railo.commons.lang.mimetype.MimeType; 021 import railo.commons.net.HTTPUtil; 022 import railo.runtime.PageContextImpl; 023 import railo.runtime.exp.ApplicationException; 024 import railo.runtime.exp.PageException; 025 import railo.runtime.exp.TemplateException; 026 import railo.runtime.ext.tag.BodyTagImpl; 027 import railo.runtime.net.http.ReqRspUtil; 028 import railo.runtime.op.Caster; 029 import railo.runtime.type.util.ListUtil; 030 031 /** 032 * Defines the MIME type returned by the current page. Optionally, lets you specify the name of a file 033 * to be returned with the page. 034 * 035 * 036 * 037 **/ 038 public final class Content extends BodyTagImpl { 039 040 private static final int RANGE_NONE = 0; 041 private static final int RANGE_YES = 1; 042 private static final int RANGE_NO = 2; 043 044 /** Defines the File/ MIME content type returned by the current page. */ 045 private String type; 046 047 /** The name of the file being retrieved */ 048 private String strFile; 049 050 /** Yes or No. Yes discards output that precedes the call to cfcontent. No preserves the output that precedes the call. Defaults to Yes. The reset 051 ** and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. */ 052 private boolean reset=true; 053 054 private int _range=RANGE_NONE; 055 056 /** Yes or No. Yes deletes the file after the download operation. Defaults to No. 057 ** This attribute applies only if you specify a file with the file attribute. */ 058 private boolean deletefile=false; 059 060 private byte[] content; 061 062 063 @Override 064 public void release() { 065 super.release(); 066 type=null; 067 strFile=null; 068 reset=true; 069 deletefile=false; 070 content=null; 071 _range=RANGE_NONE; 072 } 073 074 /** set the value type 075 * Defines the File/ MIME content type returned by the current page. 076 * @param type value to set 077 **/ 078 public void setType(String type) { 079 this.type=type.trim(); 080 } 081 082 public void setRange(boolean range) { 083 this._range=range?RANGE_YES:RANGE_NO; 084 } 085 086 /** set the value file 087 * The name of the file being retrieved 088 * @param file value to set 089 **/ 090 public void setFile(String file) { 091 this.strFile=file; 092 } 093 094 /** 095 * the content to output as binary 096 * @param content value to set 097 * @deprecated replaced with <code>{@link #setVariable(String)}</code> 098 **/ 099 public void setContent(byte[] content) { 100 this.content=content; 101 } 102 103 public void setVariable(Object variable) throws PageException { 104 if(variable instanceof String) 105 this.content=Caster.toBinary(pageContext.getVariable((String)variable)); 106 else 107 this.content=Caster.toBinary(variable); 108 } 109 110 /** set the value reset 111 * Yes or No. Yes discards output that precedes the call to cfcontent. No preserves the output that precedes the call. Defaults to Yes. The reset 112 * and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. 113 * @param reset value to set 114 **/ 115 public void setReset(boolean reset) { 116 this.reset=reset; 117 } 118 119 /** set the value deletefile 120 * Yes or No. Yes deletes the file after the download operation. Defaults to No. 121 * This attribute applies only if you specify a file with the file attribute. 122 * @param deletefile value to set 123 **/ 124 public void setDeletefile(boolean deletefile) { 125 this.deletefile=deletefile; 126 } 127 128 129 @Override 130 public int doStartTag() throws PageException { 131 //try { 132 return _doStartTag(); 133 /*} 134 catch (IOException e) { 135 throw Caster.toPageException(e); 136 }*/ 137 } 138 private int _doStartTag() throws PageException { 139 // check the file before doing anyrhing else 140 Resource file=null; 141 if(content==null && !StringUtil.isEmpty(strFile)) 142 file = ResourceUtil.toResourceExisting(pageContext,strFile); 143 144 145 146 147 // get response object 148 HttpServletResponse rsp = pageContext. getHttpServletResponse(); 149 150 // check commited 151 if(rsp.isCommitted()) 152 throw new ApplicationException("content is already flushed","you can't rewrite head of response after part of the page is flushed"); 153 154 // set type 155 if(!StringUtil.isEmpty(type,true)) { 156 type=type.trim(); 157 rsp.setContentType(type); 158 159 // TODO more dynamic implementation, configuration in admin? 160 if(!HTTPUtil.isTextMimeType(type)) { 161 ((PageContextImpl)pageContext).getRootOut().setAllowCompression(false); 162 } 163 } 164 165 Range[] ranges=getRanges(); 166 boolean hasRanges=ranges!=null && ranges.length>0; 167 if(_range==RANGE_YES || hasRanges){ 168 rsp.setHeader("Accept-Ranges", "bytes"); 169 } 170 else if(_range==RANGE_NO) { 171 rsp.setHeader("Accept-Ranges", "none"); 172 hasRanges=false; 173 } 174 175 176 // set content 177 if(this.content!=null || file!=null) { 178 pageContext.clear(); 179 InputStream is=null; 180 OutputStream os=null; 181 long totalLength,contentLength; 182 try { 183 os=getOutputStream(); 184 185 if(content!=null) { 186 //ReqRspUtil.setContentLength(rsp,content.length); 187 contentLength=content.length; 188 totalLength=content.length; 189 is=new BufferedInputStream(new ByteArrayInputStream(content)); 190 } 191 else { 192 //ReqRspUtil.setContentLength(rsp,file.length()); 193 pageContext.getConfig().getSecurityManager().checkFileLocation(file); 194 contentLength=totalLength=file.length(); 195 is=IOUtil.toBufferedInputStream(file.getInputStream()); 196 } 197 198 // write 199 if(!hasRanges) 200 IOUtil.copy(is,os,false,false); 201 else { 202 contentLength=0; 203 long off,len,to; 204 for(int i=0;i<ranges.length;i++) { 205 off=ranges[i].from; 206 if(ranges[i].to==-1) { 207 len=-1; 208 to=totalLength-1; 209 } 210 else { 211 to=ranges[i].to; 212 if(to>=totalLength)to=totalLength-1; 213 len=to-ranges[i].from+1; 214 } 215 rsp.addHeader("Content-Range", "bytes "+off+"-"+to+"/"+Caster.toString(totalLength)); 216 rsp.setStatus(206); 217 //print.e("Content-Range: bytes "+off+"-"+to+"/"+Caster.toString(totalLength)); 218 contentLength+=to-off+1L; 219 // ReqRspUtil.setContentLength(rsp,len); 220 IOUtil.copy(is, os,off,len); 221 } 222 } 223 if(!(os instanceof GZIPOutputStream)) 224 ReqRspUtil.setContentLength(rsp,contentLength); 225 } 226 catch(IOException ioe) {} 227 finally { 228 IOUtil.flushEL(os); 229 IOUtil.closeEL(is,os); 230 if(deletefile && file!=null) ResourceUtil.removeEL(file, true); 231 ((PageContextImpl)pageContext).getRootOut().setClosed(true); 232 } 233 throw new railo.runtime.exp.Abort(railo.runtime.exp.Abort.SCOPE_REQUEST); 234 } 235 // clear current content 236 else if(reset)pageContext.clear(); 237 238 return EVAL_BODY_INCLUDE;//EVAL_PAGE; 239 } 240 241 private OutputStream getOutputStream() throws PageException, IOException { 242 try { 243 return pageContext.getResponseStream(); 244 } 245 catch(IllegalStateException ise) { 246 throw new TemplateException("content is already send to user, flush"); 247 } 248 } 249 250 @Override 251 public int doEndTag() { 252 return strFile == null ? EVAL_PAGE : SKIP_PAGE; 253 } 254 255 /** 256 * sets if tag has a body or not 257 * @param hasBody 258 */ 259 public void hasBody(boolean hasBody) { 260 } 261 262 263 264 private Range[] getRanges() { 265 HttpServletRequest req = pageContext.getHttpServletRequest(); 266 Enumeration names = req.getHeaderNames(); 267 268 if(names==null) return null; 269 String name; 270 Range[] range; 271 while(names.hasMoreElements()) { 272 name=(String) names.nextElement(); 273 274 if("range".equalsIgnoreCase(name)){ 275 range = getRanges(name,req.getHeader(name)); 276 if(range!=null) return range; 277 } 278 } 279 return null; 280 } 281 private Range[] getRanges(String name,String range) { 282 if(StringUtil.isEmpty(range, true)) return null; 283 range=StringUtil.removeWhiteSpace(range); 284 if(range.indexOf("bytes=")==0) range=range.substring(6); 285 String[] arr=null; 286 try { 287 arr = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(range, ',')); 288 } catch (PageException e) { 289 failRange(name,range); 290 return null; 291 } 292 String item; 293 int index; 294 long from,to; 295 296 Range[] ranges=new Range[arr.length]; 297 for(int i=0;i<ranges.length;i++) { 298 item=arr[i].trim(); 299 index=item.indexOf('-'); 300 if(index!=-1) { 301 from = Caster.toLongValue(item.substring(0,index),0); 302 to = Caster.toLongValue(item.substring(index+1),-1); 303 if(to!=-1 && from>to){ 304 failRange(name,range); 305 return null; 306 //throw new ExpressionException("invalid range definition, from have to bigger than to ("+from+"-"+to+")"); 307 } 308 } 309 else { 310 from = Caster.toLongValue(item,0); 311 to=-1; 312 } 313 ranges[i]=new Range(from,to); 314 315 if(i>0 && ranges[i-1].to>=from){ 316 PrintWriter err = pageContext.getConfig().getErrWriter(); 317 SystemOut.printDate(err,"there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")"); 318 //throw new ExpressionException("there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")"); 319 return null; 320 } 321 322 } 323 return ranges; 324 } 325 326 private void failRange(String name, String range) { 327 PrintWriter err = pageContext.getConfig().getErrWriter(); 328 SystemOut.printDate(err,"fails to parse the header field ["+name+":"+range+"]"); 329 } 330 } 331 class Range { 332 long from; 333 long to; 334 public Range(long from, long len) { 335 this.from = from; 336 this.to = len; 337 } 338 339 public String toString() { 340 return from+"-"+to; 341 } 342 }