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    }