001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.tag;
020
021import java.io.IOException;
022import java.nio.charset.Charset;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027
028import lucee.commons.io.CharsetUtil;
029import lucee.commons.io.log.LogUtil;
030import lucee.commons.io.log.log4j.Log4jUtil;
031import lucee.commons.io.log.log4j.LogAdapter;
032import lucee.commons.io.res.Resource;
033import lucee.commons.io.retirement.RetireListener;
034import lucee.commons.io.retirement.RetireOutputStream;
035import lucee.commons.lang.ExceptionUtil;
036import lucee.commons.lang.StringUtil;
037import lucee.runtime.PageContext;
038import lucee.runtime.PageContextImpl;
039import lucee.runtime.config.Config;
040import lucee.runtime.config.ConfigImpl;
041import lucee.runtime.exp.ApplicationException;
042import lucee.runtime.exp.CasterException;
043import lucee.runtime.exp.PageException;
044import lucee.runtime.ext.tag.TagImpl;
045import lucee.runtime.op.Caster;
046import lucee.runtime.tag.util.DeprecatedUtil;
047import lucee.runtime.type.KeyImpl;
048
049import org.apache.log4j.Level;
050
051/**
052* Writes a message to a log file.
053*
054*
055*
056**/
057public final class Log extends TagImpl {
058
059        private static final String DEfAULT_LOG = "application"; 
060
061        /** If you omit the file attribute, specifies the standard log file in which to write the message. 
062        **              Ignored if you specify a file attribute */
063        private String log=DEfAULT_LOG;
064
065        /** The message text to log. */
066        private String text;
067
068        /** The type or severity of the message. */
069        private short type=lucee.commons.io.log.Log.LEVEL_INFO;
070        /**  */
071        private String file;
072        private Throwable exception;
073
074        /** Specifies whether to log the application name if one has been specified in a application tag. */
075        private boolean application;
076        private Charset charset=null;
077
078        private boolean async;
079        
080        @Override
081        public void release()   {
082                super.release();
083                log=DEfAULT_LOG;
084                type=lucee.commons.io.log.Log.LEVEL_INFO;
085                file=null;
086                application=false;
087                charset=null;
088                exception=null;
089                text=null;
090                async=false;
091        }
092
093        /** set the value log
094        *  If you omit the file attribute, specifies the standard log file in which to write the message. 
095        *               Ignored if you specify a file attribute
096        * @param log value to set
097         * @throws ApplicationException
098        **/
099        public void setLog(String log) throws ApplicationException      {
100                if(StringUtil.isEmpty(log,true)) return;
101            this.log=log.trim();
102            // throw new ApplicationException("invalid value for attribute log ["+log+"]","valid values are [application, scheduler,console]");
103        }
104
105        /** set the value text
106        *  The message text to log.
107        * @param text value to set
108        **/
109        public void setText(String text)        {
110                this.text=text;
111        }
112        public void setException(Object exception) throws PageException {
113                this.exception=Throw.toPageException(exception, null);
114                if(this.exception==null) throw new CasterException(exception,Exception.class);
115        }
116        
117        
118
119        /** set the value type
120        *  The type or severity of the message.
121        * @param type value to set
122         * @throws ApplicationException
123        **/
124        public void setType(String type) throws ApplicationException    {
125            type=type.toLowerCase().trim(); 
126            if(type.equals("information")) this.type=lucee.commons.io.log.Log.LEVEL_INFO;
127            else if(type.equals("info")) this.type=lucee.commons.io.log.Log.LEVEL_INFO;
128            else if(type.equals("warning")) this.type=lucee.commons.io.log.Log.LEVEL_WARN;
129            else if(type.equals("warn")) this.type=lucee.commons.io.log.Log.LEVEL_WARN;
130            else if(type.equals("error")) this.type=lucee.commons.io.log.Log.LEVEL_ERROR;
131        else if(type.startsWith("fatal")) this.type=lucee.commons.io.log.Log.LEVEL_FATAL;
132        else if(type.startsWith("debug")) this.type=lucee.commons.io.log.Log.LEVEL_DEBUG;
133        else if(type.startsWith("trace")) this.type=lucee.commons.io.log.LogUtil.LEVEL_TRACE;
134                else
135                    throw new ApplicationException("invalid value for attribute type ["+type+"]",
136                      "valid values are [information,warning,error,fatal,debug]");
137
138        }
139
140        /** set the value time
141        *  Specifies whether to log the system time.
142        * @param time value to set
143         * @throws ApplicationException
144        **/
145        public void setTime(boolean useTime) throws ApplicationException        {
146                if(useTime) return;
147                DeprecatedUtil.tagAttribute(pageContext,"Log", "time");
148            throw new ApplicationException("attribute [time] for tag [log] is deprecated, only the value true is allowed");
149        }
150
151        /** set the value file
152        *  
153        * @param file value to set
154         * @throws ApplicationException
155        **/
156        public void setFile(String file) throws ApplicationException    {
157                if(StringUtil.isEmpty(file))return;
158                
159            if(file.indexOf('/')!=-1 || file.indexOf('\\')!=-1)
160                throw new ApplicationException("value ["+file+"] from attribute [file] at tag [log] can only contain a filename, file separators like [\\/] are not allowed");
161                if(!file.endsWith(".log"))file+=".log";
162                this.file=file;
163        }
164
165        /** set the value date
166        *  Specifies whether to log the system date.
167        * @param date value to set
168         * @throws ApplicationException
169        **/
170        public void setDate(boolean useDate) throws ApplicationException        {
171                if(useDate) return;
172                DeprecatedUtil.tagAttribute(pageContext,"Log", "date");
173            throw new ApplicationException("attribute [date] for tag [log] is deprecated, only the value true is allowed");
174        }
175
176        /** set the value thread
177        *  Specifies whether to log the thread ID. The thread ID identifies which internal service thread logged a 
178        *               message. Since a service thread normally services a CFML page request to completion, then moves on to 
179        *               the next queued request, the thread ID serves as a rough indication of which request logged a message. 
180        *               Leaving thread IDs turned on can help diagnose patterns of server activity.
181        * @param thread value to set
182         * @throws ApplicationException
183        **/
184        public void setThread(boolean thread) throws ApplicationException       {
185                if(thread) return;
186                DeprecatedUtil.tagAttribute(pageContext,"Log", "thread");
187            throw new ApplicationException("attribute [thread] for tag [log] is deprecated, only the value true is allowed");
188        }
189
190        /** set the value application
191        *  Specifies whether to log the application name if one has been specified in a application tag.
192        * @param application value to set
193        **/
194        public void setApplication(boolean application) {
195                this.application=application;
196        }
197        
198        // old function for backward compatiblity
199        public void setSpoolenable(boolean async){
200                setAsync(async);
201        }
202        
203        public void setAsync(boolean async){
204                this.async=async;
205        }
206
207
208        @Override
209        public int doStartTag() throws PageException    {
210                
211                 if(text==null && exception==null)
212                        throw new ApplicationException("Wrong Context, you must define one of the following attributes [text, exception]");
213                
214                ConfigImpl config =(ConfigImpl) pageContext.getConfig();
215            lucee.commons.io.log.Log logger;
216                if(file==null) {
217                logger=config.getLog(log.toLowerCase(),false);
218                if(logger==null) {
219                        // for backward compatiblity
220                        if("console".equalsIgnoreCase(log))
221                                logger=new LogAdapter(Log4jUtil.getConsoleLog(config, false, "cflog", Level.INFO));
222                        else {
223                                Set<String> set = config.getLoggers().keySet();
224                                Iterator<String> it = set.iterator();
225                                lucee.runtime.type.Collection.Key[] keys=new lucee.runtime.type.Collection.Key[set.size()];
226                                int index=0;
227                                while(it.hasNext()){
228                                        keys[index++]=KeyImpl.init(it.next());
229                                }
230                                
231                                throw new ApplicationException(ExceptionUtil.similarKeyMessage(keys, log, "attribute log", "log names", true));
232                        }
233                }
234            }
235            else {
236                logger=getFileLog(pageContext,file,charset,async);
237            }
238                
239            
240            String contextName = pageContext.getApplicationContext().getName();
241            if(contextName==null || !application)contextName="";
242            if(exception!=null) {
243                if(StringUtil.isEmpty(text)) LogUtil.log(logger, type, contextName, exception);
244                else LogUtil.log(logger, type, contextName, text, exception);
245            }
246            else if(!StringUtil.isEmpty(text)) 
247                logger.log(type,contextName,text);
248            else
249                throw new ApplicationException("you must define attribute text or attribute exception with the tag cflog");
250        //logger.write(toStringType(type),contextName,text);
251                return SKIP_BODY;
252        }
253
254        private static lucee.commons.io.log.Log getFileLog(PageContext pc, String file, Charset charset, boolean async) throws PageException {
255                Config config=pc.getConfig();
256                Resource logDir=config.getConfigDir().getRealResource("logs");
257        if(!logDir.exists())logDir.mkdirs();
258        Resource res = logDir.getRealResource(file);
259                
260                
261                LogAdapter log= FileLogPool.instance.get(res,charset);
262                if(log!=null) return log;
263                
264                if(charset==null) charset=((PageContextImpl)pc).getResourceCharset();
265        
266        try {
267                log=new LogAdapter(Log4jUtil.getResourceLog(config,res,charset , "cflog."+FileLogPool.toKey(file,charset), Level.TRACE,5,new Listener(FileLogPool.instance,res,charset),async));
268            FileLogPool.instance.put(res,charset,log);
269        } 
270        catch (IOException e) {
271            throw Caster.toPageException(e);
272        }
273                return log;
274        }
275
276        /**
277         * @param charset the charset to set
278         */
279        public void setCharset(String charset) {
280                if(StringUtil.isEmpty(charset,true)) return;
281            this.charset = CharsetUtil.toCharset(charset);
282        }
283        
284        private static class FileLogPool {
285                
286                private static Map<String,LogAdapter> logs=new ConcurrentHashMap<String, LogAdapter>();
287                private static FileLogPool instance=new FileLogPool();
288                
289                public void retire(Resource res, Charset charset) {
290                        logs.remove(res.getAbsolutePath());
291                }
292
293                public void put(Resource res, Charset charset, LogAdapter log) {
294                        logs.put(res.getAbsolutePath(),log);
295                }
296
297                public LogAdapter get(Resource res, Charset charset) {
298                        LogAdapter l = logs.get(res.getAbsolutePath());
299                        return l;
300                }
301
302                public static String toKey(String file, Charset charset) {
303                        if(charset==null); charset=CharsetUtil.UTF8;
304                        return StringUtil.toVariableName(file)+"."+StringUtil.toVariableName(charset.name());
305                }
306        }
307        
308        private static class Listener implements RetireListener {
309                
310                private FileLogPool pool;
311                private Resource res;
312                private Charset charset;
313
314                public Listener(FileLogPool pool, Resource res, Charset charset){
315                        this.pool=pool;
316                        this.res=res;
317                        this.charset=charset;
318                }
319                
320                @Override
321                public void retire(RetireOutputStream os) {
322                        pool.retire(res,charset);
323                }
324        }
325}