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 011 import javax.servlet.http.HttpServletRequest; 012 import javax.servlet.http.HttpServletResponse; 013 014 import railo.commons.io.IOUtil; 015 import railo.commons.io.res.Resource; 016 import railo.commons.io.res.util.ResourceUtil; 017 import railo.commons.lang.StringUtil; 018 import railo.commons.lang.SystemOut; 019 import railo.runtime.PageContextImpl; 020 import railo.runtime.exp.ApplicationException; 021 import railo.runtime.exp.PageException; 022 import railo.runtime.exp.TemplateException; 023 import railo.runtime.ext.tag.BodyTagImpl; 024 import railo.runtime.net.http.ReqRspUtil; 025 import railo.runtime.op.Caster; 026 import railo.runtime.type.List; 027 028 /** 029 * Defines the MIME type returned by the current page. Optionally, lets you specify the name of a file 030 * to be returned with the page. 031 * 032 * 033 * 034 **/ 035 public final class Content extends BodyTagImpl { 036 037 private static final int RANGE_NONE = 0; 038 private static final int RANGE_YES = 1; 039 private static final int RANGE_NO = 2; 040 041 /** Defines the File/ MIME content type returned by the current page. */ 042 private String type; 043 044 /** The name of the file being retrieved */ 045 private String strFile; 046 047 /** 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 048 ** and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. */ 049 private boolean reset=true; 050 051 private int _range=RANGE_NONE; 052 053 /** Yes or No. Yes deletes the file after the download operation. Defaults to No. 054 ** This attribute applies only if you specify a file with the file attribute. */ 055 private boolean deletefile=false; 056 057 private byte[] content; 058 059 060 /** 061 * @see javax.servlet.jsp.tagext.Tag#release() 062 */ 063 public void release() { 064 super.release(); 065 type=null; 066 strFile=null; 067 reset=true; 068 deletefile=false; 069 content=null; 070 _range=RANGE_NONE; 071 } 072 073 /** set the value type 074 * Defines the File/ MIME content type returned by the current page. 075 * @param type value to set 076 **/ 077 public void setType(String type) { 078 this.type=type.trim(); 079 } 080 081 public void setRange(boolean range) { 082 this._range=range?RANGE_YES:RANGE_NO; 083 } 084 085 /** set the value file 086 * The name of the file being retrieved 087 * @param file value to set 088 **/ 089 public void setFile(String file) { 090 this.strFile=file; 091 } 092 093 /** 094 * the content to output as binary 095 * @param content value to set 096 * @deprecated replaced with <code>{@link #setVariable(String)}</code> 097 **/ 098 public void setContent(byte[] content) { 099 this.content=content; 100 } 101 102 public void setVariable(Object variable) throws PageException { 103 if(variable instanceof String) 104 this.content=Caster.toBinary(pageContext.getVariable((String)variable)); 105 else 106 this.content=Caster.toBinary(variable); 107 } 108 109 /** set the value reset 110 * 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 111 * and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. 112 * @param reset value to set 113 **/ 114 public void setReset(boolean reset) { 115 this.reset=reset; 116 } 117 118 /** set the value deletefile 119 * Yes or No. Yes deletes the file after the download operation. Defaults to No. 120 * This attribute applies only if you specify a file with the file attribute. 121 * @param deletefile value to set 122 **/ 123 public void setDeletefile(boolean deletefile) { 124 this.deletefile=deletefile; 125 } 126 127 128 /** 129 * @see javax.servlet.jsp.tagext.Tag#doStartTag() 130 */ 131 public int doStartTag() throws PageException { 132 //try { 133 return _doStartTag(); 134 /*} 135 catch (IOException e) { 136 throw Caster.toPageException(e); 137 }*/ 138 } 139 private int _doStartTag() throws PageException { 140 // check the file before doing anyrhing else 141 Resource file=null; 142 if(content==null && !StringUtil.isEmpty(strFile)) 143 file = ResourceUtil.toResourceExisting(pageContext,strFile); 144 145 146 147 148 // get response object 149 HttpServletResponse rsp = pageContext. getHttpServletResponse(); 150 151 // check commited 152 if(rsp.isCommitted()) 153 throw new ApplicationException("content ist already flushed","you can't rewrite head of response after the page is flushed"); 154 155 // set type 156 setContentType(rsp); 157 158 Range[] ranges=getRanges(); 159 boolean hasRanges=ranges!=null && ranges.length>0; 160 if(_range==RANGE_YES || hasRanges){ 161 rsp.setHeader("Accept-Ranges", "bytes"); 162 } 163 else if(_range==RANGE_NO) { 164 rsp.setHeader("Accept-Ranges", "none"); 165 hasRanges=false; 166 } 167 168 169 // set content 170 if(this.content!=null || file!=null) { 171 pageContext.clear(); 172 InputStream is=null; 173 OutputStream os=null; 174 long length; 175 try { 176 os=getOutputStream(); 177 178 if(content!=null) { 179 ReqRspUtil.setContentLength(rsp,content.length); 180 length=content.length; 181 is=new BufferedInputStream(new ByteArrayInputStream(content)); 182 } 183 else { 184 ReqRspUtil.setContentLength(rsp,file.length()); 185 pageContext.getConfig().getSecurityManager().checkFileLocation(file); 186 length=file.length(); 187 is=IOUtil.toBufferedInputStream(file.getInputStream()); 188 } 189 190 // write 191 if(!hasRanges) 192 IOUtil.copy(is,os,false,false); 193 else { 194 //print.out("do part"); 195 //print.out(ranges); 196 long off,len; 197 long to; 198 for(int i=0;i<ranges.length;i++) { 199 off=ranges[i].from; 200 if(ranges[i].to==-1) { 201 len=-1; 202 to=length; 203 } 204 else { 205 len=ranges[i].to-ranges[i].from+1; 206 to=ranges[i].to; 207 } 208 rsp.addHeader("Content-Range", "bytes "+off+"-"+to+"/"+Caster.toString(length)); 209 //print.out("Content-Range: bytes "+off+"-"+to+"/"+Caster.toString(length)); 210 IOUtil.copy(is, os,off,len); 211 } 212 } 213 } 214 catch(IOException ioe) {} 215 finally { 216 IOUtil.flushEL(os); 217 IOUtil.closeEL(is,os); 218 if(deletefile && file!=null) file.delete(); 219 ((PageContextImpl)pageContext).getRootOut().setClosed(true); 220 } 221 throw new railo.runtime.exp.Abort(railo.runtime.exp.Abort.SCOPE_REQUEST); 222 } 223 // clear current content 224 else if(reset)pageContext.clear(); 225 226 return EVAL_BODY_INCLUDE;//EVAL_PAGE; 227 } 228 229 private OutputStream getOutputStream() throws PageException, IOException { 230 try { 231 return ((PageContextImpl)pageContext).getResponseStream(); 232 } 233 catch(IllegalStateException ise) { 234 throw new TemplateException("content is already send to user, flush"); 235 } 236 } 237 238 /** 239 * @see javax.servlet.jsp.tagext.Tag#doEndTag() 240 */ 241 public int doEndTag() { 242 return strFile == null ? EVAL_PAGE : SKIP_PAGE; 243 } 244 245 246 /** 247 * set the content type of the side 248 * @param rsp HTTP Servlet Response object 249 */ 250 private void setContentType(HttpServletResponse rsp) { 251 if(!StringUtil.isEmpty(type)) { 252 rsp.setContentType(type); 253 } 254 } 255 256 /** 257 * sets if tag has a body or not 258 * @param hasBody 259 */ 260 public void hasBody(boolean hasBody) { 261 } 262 263 264 265 private Range[] getRanges() { 266 HttpServletRequest req = pageContext.getHttpServletRequest(); 267 Enumeration names = req.getHeaderNames(); 268 if(names==null) return null; 269 String name; 270 Range[] range; 271 while(names.hasMoreElements()) { 272 name=(String) names.nextElement(); 273 //print.out("header:"+name); 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 = List.toStringArray(List.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 }