package railo.runtime.tag;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.zip.GZIPOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import railo.commons.io.IOUtil;
import railo.commons.io.res.Resource;
import railo.commons.io.res.util.ResourceUtil;
import railo.commons.lang.StringUtil;
import railo.commons.lang.SystemOut;
import railo.commons.net.HTTPUtil;
import railo.runtime.PageContextImpl;
import railo.runtime.exp.ApplicationException;
import railo.runtime.exp.PageException;
import railo.runtime.exp.PostContentAbort;
import railo.runtime.exp.TemplateException;
import railo.runtime.ext.tag.BodyTagImpl;
import railo.runtime.net.http.ReqRspUtil;
import railo.runtime.op.Caster;
import railo.runtime.type.util.ListUtil;
/**
* Defines the MIME type returned by the current page. Optionally, lets you specify the name of a file
* to be returned with the page.
**/
public final class Content extends BodyTagImpl {
private static final int RANGE_NONE = 0;
private static final int RANGE_YES = 1;
private static final int RANGE_NO = 2;
/** Defines the File/ MIME content type returned by the current page. */
private String type;
/** The name of the file being retrieved */
private String strFile;
/** 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
** and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. */
private boolean reset=true;
private int _range=RANGE_NONE;
/** Yes or No. Yes deletes the file after the download operation. Defaults to No.
** This attribute applies only if you specify a file with the file attribute. */
private boolean deletefile=false;
private byte[] content;
@Override
public void release() {
super.release();
type=null;
strFile=null;
reset=true;
deletefile=false;
content=null;
_range=RANGE_NONE;
}
/** set the value type
* Defines the File/ MIME content type returned by the current page.
* @param type value to set
**/
public void setType(String type) {
this.type=type.trim();
}
public void setRange(boolean range) {
this._range=range?RANGE_YES:RANGE_NO;
}
/** set the value file
* The name of the file being retrieved
* @param file value to set
**/
public void setFile(String file) {
this.strFile=file;
}
/**
* the content to output as binary
* @param content value to set
* @deprecated replaced with <code>{@link #setVariable(Object)}</code>
**/
public void setContent(byte[] content) {
this.content=content;
}
public void setVariable(Object variable) throws PageException {
if(variable instanceof String)
this.content=Caster.toBinary(pageContext.getVariable((String)variable));
else
this.content=Caster.toBinary(variable);
}
/** set the value reset
* 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
* and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect.
* @param reset value to set
**/
public void setReset(boolean reset) {
this.reset=reset;
}
/** set the value deletefile
* Yes or No. Yes deletes the file after the download operation. Defaults to No.
* This attribute applies only if you specify a file with the file attribute.
* @param deletefile value to set
**/
public void setDeletefile(boolean deletefile) {
this.deletefile=deletefile;
}
@Override
public int doStartTag() throws PageException {
//try {
return _doStartTag();
/*}
catch (IOException e) {
throw Caster.toPageException(e);
}*/
}
private int _doStartTag() throws PageException {
// check the file before doing anyrhing else
Resource file=null;
if(content==null && !StringUtil.isEmpty(strFile))
file = ResourceUtil.toResourceExisting(pageContext,strFile);
// get response object
HttpServletResponse rsp = pageContext. getHttpServletResponse();
// check committed
if(rsp.isCommitted())
throw new ApplicationException("content is already flushed","you can't rewrite head of response after part of the page is flushed");
// set type
if(!StringUtil.isEmpty(type,true)) {
type=type.trim();
rsp.setContentType(type);
// TODO more dynamic implementation, configuration in admin?
if(!HTTPUtil.isTextMimeType(type)) {
((PageContextImpl)pageContext).getRootOut().setAllowCompression(false);
}
}
Range[] ranges=getRanges();
boolean hasRanges=ranges!=null && ranges.length>0;
if(_range==RANGE_YES || hasRanges){
rsp.setHeader("Accept-Ranges", "bytes");
}
else if(_range==RANGE_NO) {
rsp.setHeader("Accept-Ranges", "none");
hasRanges=false;
}
// set content
if(this.content!=null || file!=null) {
pageContext.clear();
InputStream is=null;
OutputStream os=null;
long totalLength,contentLength;
try {
os=getOutputStream();
if(content!=null) {
//ReqRspUtil.setContentLength(rsp,content.length);
contentLength=content.length;
totalLength=content.length;
is=new BufferedInputStream(new ByteArrayInputStream(content));
}
else {
//ReqRspUtil.setContentLength(rsp,file.length());
pageContext.getConfig().getSecurityManager().checkFileLocation(file);
contentLength=totalLength=file.length();
is=IOUtil.toBufferedInputStream(file.getInputStream());
}
// write
if(!hasRanges)
IOUtil.copy(is,os,false,false);
else {
contentLength=0;
long off,len,to;
for(int i=0;i<ranges.length;i++) {
off=ranges[i].from;
if(ranges[i].to==-1) {
len=-1;
to=totalLength-1;
}
else {
to=ranges[i].to;
if(to>=totalLength)to=totalLength-1;
len=to-ranges[i].from+1;
}
rsp.addHeader("Content-Range", "bytes "+off+"-"+to+"/"+Caster.toString(totalLength));
rsp.setStatus(206);
//print.e("Content-Range: bytes "+off+"-"+to+"/"+Caster.toString(totalLength));
contentLength+=to-off+1L;
// ReqRspUtil.setContentLength(rsp,len);
IOUtil.copy(is, os,off,len);
}
}
if(!(os instanceof GZIPOutputStream))
ReqRspUtil.setContentLength(rsp,contentLength);
}
catch(IOException ioe) {}
finally {
IOUtil.flushEL(os);
IOUtil.closeEL(is,os);
if(deletefile && file!=null) ResourceUtil.removeEL(file, true);
((PageContextImpl)pageContext).getRootOut().setClosed(true);
}
throw new PostContentAbort();
}
// clear current content
else if(reset)pageContext.clear();
return EVAL_BODY_INCLUDE;//EVAL_PAGE;
}
private OutputStream getOutputStream() throws PageException, IOException {
try {
return pageContext.getResponseStream();
}
catch(IllegalStateException ise) {
throw new TemplateException("content is already send to user, flush");
}
}
@Override
public int doEndTag() {
return strFile == null ? EVAL_PAGE : SKIP_PAGE;
}
/**
* sets if tag has a body or not
* @param hasBody
*/
public void hasBody(boolean hasBody) {
}
private Range[] getRanges() {
HttpServletRequest req = pageContext.getHttpServletRequest();
Enumeration names = req.getHeaderNames();
if(names==null) return null;
String name;
Range[] range;
while(names.hasMoreElements()) {
name=(String) names.nextElement();
if("range".equalsIgnoreCase(name)){
range = getRanges(name,req.getHeader(name));
if(range!=null) return range;
}
}
return null;
}
private Range[] getRanges(String name,String range) {
if(StringUtil.isEmpty(range, true)) return null;
range=StringUtil.removeWhiteSpace(range);
if(range.indexOf("bytes=")==0) range=range.substring(6);
String[] arr=null;
try {
arr = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(range, ','));
} catch (PageException e) {
failRange(name,range);
return null;
}
String item;
int index;
long from,to;
Range[] ranges=new Range[arr.length];
for(int i=0;i<ranges.length;i++) {
item=arr[i].trim();
index=item.indexOf('-');
if(index!=-1) {
from = Caster.toLongValue(item.substring(0,index),0);
to = Caster.toLongValue(item.substring(index+1),-1);
if(to!=-1 && from>to){
failRange(name,range);
return null;
//throw new ExpressionException("invalid range definition, from have to bigger than to ("+from+"-"+to+")");
}
}
else {
from = Caster.toLongValue(item,0);
to=-1;
}
ranges[i]=new Range(from,to);
if(i>0 && ranges[i-1].to>=from){
PrintWriter err = pageContext.getConfig().getErrWriter();
SystemOut.printDate(err,"there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")");
//throw new ExpressionException("there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")");
return null;
}
}
return ranges;
}
private void failRange(String name, String range) {
PrintWriter err = pageContext.getConfig().getErrWriter();
SystemOut.printDate(err,"failed to parse the header field ["+name+":"+range+"]");
}
}
class Range {
long from;
long to;
public Range(long from, long len) {
this.from = from;
this.to = len;
}
public String toString() {
return from+"-"+to;
}
}