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    }