001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.apps.logfile;
029
030import org.opencms.ui.CmsCssIcon;
031import org.opencms.ui.CmsVaadinUtils;
032import org.opencms.ui.FontOpenCms;
033import org.opencms.ui.apps.Messages;
034import org.opencms.ui.components.OpenCmsTheme;
035import org.opencms.ui.contextmenu.CmsContextMenu;
036import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItem;
037import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItemClickEvent;
038import org.opencms.ui.contextmenu.CmsContextMenu.ContextMenuItemClickListener;
039import org.opencms.util.CmsLog4jUtil;
040import org.opencms.util.CmsStringUtil;
041
042import java.io.File;
043import java.util.ArrayList;
044import java.util.List;
045import java.util.Set;
046
047import org.apache.logging.log4j.Level;
048import org.apache.logging.log4j.LogManager;
049import org.apache.logging.log4j.core.Appender;
050import org.apache.logging.log4j.core.Layout;
051import org.apache.logging.log4j.core.Logger;
052import org.apache.logging.log4j.core.LoggerContext;
053import org.apache.logging.log4j.core.appender.FileAppender;
054import org.apache.logging.log4j.core.appender.FileAppender.Builder;
055import org.apache.logging.log4j.core.config.Configuration;
056import org.apache.logging.log4j.core.config.LoggerConfig;
057
058import com.vaadin.event.MouseEvents;
059import com.vaadin.server.Resource;
060import com.vaadin.shared.MouseEventDetails.MouseButton;
061import com.vaadin.v7.data.Item;
062import com.vaadin.v7.data.util.IndexedContainer;
063import com.vaadin.v7.data.util.filter.Or;
064import com.vaadin.v7.data.util.filter.SimpleStringFilter;
065import com.vaadin.v7.event.ItemClickEvent;
066import com.vaadin.v7.event.ItemClickEvent.ItemClickListener;
067import com.vaadin.v7.ui.Table;
068
069/**
070 * Class for table to display and edit Log channels.<p>
071 */
072public class CmsLogChannelTable extends Table {
073
074    /**
075     * Table column generator for Level-buttons.<p>
076     * */
077    class LevelIcon implements Table.ColumnGenerator {
078
079        /**vaadin serial id. */
080        private static final long serialVersionUID = 7258796583481183276L;
081
082        /**
083         * @see com.vaadin.ui.Table.ColumnGenerator#generateCell(com.vaadin.ui.Table, java.lang.Object, java.lang.Object)
084         */
085        public Object generateCell(Table source, final Object itemId, Object columnId) {
086
087            return ((LoggerLevel)(source.getItem(itemId).getItemProperty(columnId).getValue())).getLevelString();
088        }
089
090    }
091
092    /**
093     * Enumeration of Table Columns.<p>
094     */
095    enum TableColumn {
096
097        /**Channel column.*/
098        Channel(Messages.GUI_LOGFILE_LOGSETTINGS_CHANNEL_0, String.class, ""),
099
100        /**Log file column. */
101        File(Messages.GUI_LOGFILE_LOGSETTINGS_FILE_0, String.class, ""),
102
103        /**Icon column.*/
104        Icon(null, Resource.class, new CmsCssIcon(OpenCmsTheme.ICON_LOG)),
105
106        /**Level column.*/
107        Level(Messages.GUI_LOGFILE_LOGSETTINGS_LEVEL_0, LoggerLevel.class, null),
108
109        /**Parent channel column.*/
110        ParentChannel(Messages.GUI_LOGFILE_LOGSETTINGS_PARENT_CHANNEL_0, String.class, "");
111
112        /**Default value for column.*/
113        private Object m_defaultValue;
114
115        /**Header Message key.*/
116        private String m_headerMessage;
117
118        /**Type of column property.*/
119        private Class<?> m_type;
120
121        /**
122         * constructor.
123         *
124         * @param headerMessage key
125         * @param type to property
126         * @param defaultValue of column
127         */
128        TableColumn(String headerMessage, Class<?> type, Object defaultValue) {
129
130            m_headerMessage = headerMessage;
131            m_type = type;
132            m_defaultValue = defaultValue;
133        }
134
135        /**
136         * Returns list of all properties with non-empty header.<p>
137         *
138         * @return list of properties
139         */
140        static List<TableColumn> withHeader() {
141
142            List<TableColumn> props = new ArrayList<TableColumn>();
143
144            for (TableColumn prop : TableColumn.values()) {
145                if (prop.m_headerMessage != null) {
146                    props.add(prop);
147                }
148            }
149            return props;
150        }
151
152        /**
153         * Returns the default value of property.<p>
154         *
155         * @return object
156         */
157        Object getDefaultValue() {
158
159            return m_defaultValue;
160        }
161
162        /**
163         * Returns localized header.<p>
164         *
165         * @return string for header
166         */
167        String getLocalizedMessage() {
168
169            if (m_headerMessage == null) {
170                return "";
171            }
172            return CmsVaadinUtils.getMessageText(m_headerMessage);
173        }
174
175        /**
176         * Returns tye of value for given property.<p>
177         *
178         * @return type
179         */
180        Class<?> getType() {
181
182            return m_type;
183        }
184
185    }
186
187    /**
188     * Enumeration of Logger Level and corresponging icon paths.<p>
189     */
190    private enum LoggerLevel {
191
192        /**Debug level.*/
193        Debug(Level.DEBUG, OpenCmsTheme.TABLE_COLUMN_BOX_RED, null),
194
195        /**Error level. */
196        Error(Level.ERROR, OpenCmsTheme.TABLE_COLUMN_BOX_CYAN, "Default"),
197
198        /**Fatal level.*/
199        Fatal(Level.FATAL, OpenCmsTheme.TABLE_COLUMN_BOX_BLUE_LIGHT, null),
200
201        /**Info level. */
202        Info(Level.INFO, OpenCmsTheme.TABLE_COLUMN_BOX_ORANGE_DARK, null),
203
204        /**Off level. */
205        Off(Level.OFF, OpenCmsTheme.TABLE_COLUMN_BOX_GRAY, null),
206
207        /**Warning level. */
208        Warn(Level.WARN, OpenCmsTheme.TABLE_COLUMN_BOX_ORANGE, null);
209
210        /**Caption for logger level.*/
211        private String m_caption;
212
213        /**CSS class.*/
214        private String m_css;
215
216        /**Corresponging log4j Level.*/
217        private Level m_level;
218
219        /**
220         * constructor.<p>
221         *
222         * @param level of logger
223         * @param css class
224         * @param caption for the level
225         */
226        private LoggerLevel(Level level, String css, String caption) {
227
228            m_css = css;
229            m_level = level;
230            m_caption = caption;
231        }
232
233        /**
234         * Returns LoggerLevel object from given logger.<p>
235         *
236         * @param logger to fing enumeration object for
237         * @return LoggerLevel
238         */
239        protected static LoggerLevel fromLogger(Logger logger) {
240
241            for (LoggerLevel item : LoggerLevel.values()) {
242                if (item.getLevel().equals(logger.getLevel())) {
243                    return item;
244                }
245            }
246            return null;
247        }
248
249        /**
250         * Returns path to icon.<p>
251         *
252         * @return path to icon
253         */
254        protected String getCssClass() {
255
256            return m_css;
257        }
258
259        /**
260         * Returns level. <p>
261         * @return log4j Level
262         */
263        protected Level getLevel() {
264
265            return m_level;
266        }
267
268        /**
269         * Returns the string representation for level.<p>
270         *
271         * @return string
272         */
273        protected String getLevelString() {
274
275            if (m_caption == null) {
276                String out = m_level.toString();
277                return out.substring(0, 1).toUpperCase() + out.substring(1).toLowerCase();
278            }
279            return m_caption;
280        }
281
282        /**
283         * Returns an extenden string representation with log level name added in case of having caption set.<p>
284         *
285         * @return string
286         */
287        protected String getLevelStringComplet() {
288
289            if (m_caption == null) {
290                return getLevelString();
291            }
292            String level = m_level.toString();
293            level = level.substring(0, 1).toUpperCase() + level.substring(1).toLowerCase();
294            return m_caption + " (" + level + ")";
295        }
296
297    }
298
299    /** The prefix of opencms classes. */
300    private static final String OPENCMS_CLASS_PREFIX = "org.opencms";
301
302    /**vaadin serial id.*/
303    private static final long serialVersionUID = 5467369614234190999L;
304
305    /**Container holding table data. */
306    private IndexedContainer m_container;
307
308    /**Context menu. */
309    private CmsContextMenu m_menu;
310
311    /**
312     * constructor.<p>
313     */
314    protected CmsLogChannelTable() {
315
316        m_container = new IndexedContainer();
317
318        setContainerDataSource(m_container);
319
320        for (TableColumn prop : TableColumn.values()) {
321            m_container.addContainerProperty(prop, prop.getType(), prop.getDefaultValue());
322            setColumnHeader(prop, prop.getLocalizedMessage());
323        }
324
325        setVisibleColumns(TableColumn.Level, TableColumn.Channel, TableColumn.ParentChannel, TableColumn.File);
326
327        setItemIconPropertyId(TableColumn.Icon);
328        setColumnWidth(null, 40);
329        setRowHeaderMode(RowHeaderMode.ICON_ONLY);
330
331        setColumnWidth(TableColumn.Level, 80);
332
333        setSelectable(true);
334        setMultiSelect(true);
335        m_menu = new CmsContextMenu();
336        m_menu.setAsTableContextMenu(this);
337        addItemClickListener(new ItemClickListener() {
338
339            private static final long serialVersionUID = 1L;
340
341            public void itemClick(ItemClickEvent event) {
342
343                onItemClick(event, event.getItemId(), event.getPropertyId());
344            }
345        });
346        setCellStyleGenerator(new CellStyleGenerator() {
347
348            private static final long serialVersionUID = 1L;
349
350            public String getStyle(Table source, Object itemId, Object propertyId) {
351
352                if (TableColumn.Channel.equals(propertyId)) {
353                    return " " + OpenCmsTheme.HOVER_COLUMN;
354                }
355
356                if (TableColumn.Level.equals(propertyId)) {
357                    return ((LoggerLevel)source.getItem(itemId).getItemProperty(propertyId).getValue()).getCssClass();
358                }
359
360                return null;
361            }
362        });
363
364        addGeneratedColumn(TableColumn.Level, new LevelIcon());
365
366        fillTable();
367    }
368
369    /**
370     * Adds a container item for the given logger.<p>
371     *
372     * @param logger the logger for which to generate a container item
373     */
374    public void addItemForLogger(Logger logger) {
375
376        Item item = m_container.addItem(logger);
377        if (item != null) {
378            item.getItemProperty(TableColumn.Channel).setValue(logger.getName());
379            Logger parent = logger.getParent();
380            item.getItemProperty(TableColumn.ParentChannel).setValue(parent != null ? parent.getName() : "none");
381            item.getItemProperty(TableColumn.File).setValue(getLogFiles(logger));
382            item.getItemProperty(TableColumn.Level).setValue(LoggerLevel.fromLogger(logger));
383        }
384    }
385
386    /**
387     * Filters the table according to given search string.<p>
388     *
389     * @param search string to be looked for.
390     */
391    @SuppressWarnings("unchecked")
392    public void filterTable(String search) {
393
394        m_container.removeAllContainerFilters();
395        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(search)) {
396            m_container.addContainerFilter(
397                new Or(
398                    new SimpleStringFilter(TableColumn.Channel, search, true, false),
399                    new SimpleStringFilter(TableColumn.ParentChannel, search, true, false),
400                    new SimpleStringFilter(TableColumn.File, search, true, false)));
401        }
402        if ((getValue() != null) & !((Set<Logger>)getValue()).isEmpty()) {
403            setCurrentPageFirstItemId(((Set<Logger>)getValue()).iterator().next());
404        }
405    }
406
407    /**
408     * Simple check if the logger has the global log file <p> or a single one.
409     *
410     * @param logchannel the channel that has do be checked
411     * @return true if the the log channel has a single log file
412     * */
413    protected boolean isloggingactivated(Logger logchannel) {
414
415        boolean check = false;
416        for (Appender appender : logchannel.getAppenders().values()) {
417            check = appender.getName().equals(logchannel.getName());
418        }
419        return check;
420    }
421
422    /**
423     * Toggles the log file of a given log channel.<p>
424     *
425     * @param logchannel to set or remove log file to
426     */
427    @SuppressWarnings({"rawtypes", "unchecked"})
428    protected void toggleOwnFile(Logger logchannel) {
429
430        String filepath = "";
431
432        Layout layout = null;
433        // if the button is activated check the value of the button
434        // the button was active
435        if (isloggingactivated(logchannel)) {
436            // remove the private Appender from logger
437            for (Appender appender : logchannel.getAppenders().values()) {
438                logchannel.removeAppender(appender);
439            }
440            // activate the heredity so the logger get the appender from parent logger
441            logchannel.setAdditive(true);
442
443        }
444        // the button was inactive
445        else {
446            // get the layout and file path from root logger
447            for (Appender appender : ((Logger)LogManager.getRootLogger()).getAppenders().values()) {
448                if (CmsLogFileApp.isFileAppender(appender)) {
449                    String fileName = CmsLogFileApp.getFileName(appender);
450                    filepath = fileName.substring(0, fileName.lastIndexOf(File.separatorChar));
451                    layout = appender.getLayout();
452                    break;
453                }
454            }
455
456            // check if the logger has an Appender get his layout
457            for (Appender appender : logchannel.getAppenders().values()) {
458                if (CmsLogFileApp.isFileAppender(appender)) {
459                    layout = appender.getLayout();
460                    break;
461                }
462            }
463            String logfilename = "";
464            String temp = logchannel.getName();
465            // check if the logger name begins with "org.opencms"
466            if (logchannel.getName().contains(OPENCMS_CLASS_PREFIX)) {
467                // remove the prefix "org.opencms" from logger name to generate the file name
468                temp = temp.replace(OPENCMS_CLASS_PREFIX, "");
469                // if the name has suffix
470                if (temp.length() >= 1) {
471                    logfilename = filepath + File.separator + "opencms-" + temp.substring(1).replace(".", "-") + ".log";
472                }
473                // if the name has no suffix
474                else {
475                    logfilename = filepath + File.separator + "opencms" + temp.replace(".", "-") + ".log";
476                }
477            }
478            // if the logger name not begins with "org.opencms"
479            else {
480                logfilename = filepath + File.separator + "opencms-" + temp.replace(".", "-") + ".log";
481            }
482
483            FileAppender fapp = ((Builder)FileAppender.<FileAppender.Builder> newBuilder().withFileName(
484                logfilename).withLayout(layout).withName(logchannel.getName())).build();
485
486            // deactivate the heredity so the logger get no longer the appender from parent logger
487            logchannel.setAdditive(false);
488            // remove all active Appenders from logger
489            for (Appender appender : logchannel.getAppenders().values()) {
490                logchannel.removeAppender(appender);
491            }
492            // add the new created Appender to the logger
493            logchannel.addAppender(fapp);
494        }
495        updateFile();
496    }
497
498    /**
499     * Sets a given Level to a Set of Loggers.<p>
500     *
501     * @param clickedLevel to be set
502     * @param clickedLogger to get level changed
503     */
504    void changeLoggerLevel(LoggerLevel clickedLevel, Set<Logger> clickedLogger) {
505
506        for (Logger logger : clickedLogger) {
507            @SuppressWarnings("resource")
508            LoggerContext context = logger.getContext();
509            Configuration config = context.getConfiguration();
510            LoggerConfig loggerConfig = config.getLoggerConfig(logger.getName());
511            LoggerConfig specificConfig = loggerConfig;
512            if (!loggerConfig.getName().equals(logger.getName())) {
513                specificConfig = new LoggerConfig(logger.getName(), clickedLevel.getLevel(), true);
514                specificConfig.setParent(loggerConfig);
515                config.addLogger(logger.getName(), specificConfig);
516            }
517            specificConfig.setLevel(clickedLevel.getLevel());
518            context.updateLoggers();
519        }
520        updateLevel();
521    }
522
523    /**
524     * Handles the table item clicks, including clicks on images inside of a table item.<p>
525     *
526     * @param event the click event
527     * @param itemId of the clicked row
528     * @param propertyId column id
529     */
530    @SuppressWarnings("unchecked")
531    void onItemClick(MouseEvents.ClickEvent event, Object itemId, Object propertyId) {
532
533        if (!event.isCtrlKey() && !event.isShiftKey()) {
534
535            if (event.getButton().equals(MouseButton.LEFT)) {
536                setValue(null);
537            }
538            changeValueIfNotMultiSelect(itemId);
539            // don't interfere with multi-selection using control key
540            if (event.getButton().equals(MouseButton.RIGHT) || (propertyId == null)) {
541                m_menu.removeAllItems();
542                fillContextMenu((Set<Logger>)getValue());
543                m_menu.openForTable(event, itemId, propertyId, this);
544            }
545
546        }
547    }
548
549    /**
550     * Checks value of table and sets it new if needed:<p>
551     * if multiselect: new itemId is in current Value? -> no change of value<p>
552     * no multiselect and multiselect, but new item not selected before: set value to new item<p>
553     *
554     * @param itemId if of clicked item
555     */
556    private void changeValueIfNotMultiSelect(Object itemId) {
557
558        @SuppressWarnings("unchecked")
559        Set<String> value = (Set<String>)getValue();
560        if (value == null) {
561            select(itemId);
562        } else if (!value.contains(itemId)) {
563            setValue(null);
564            select(itemId);
565        }
566    }
567
568    /**
569     * Fills the context menu.<p>
570     *
571     * @param loggerSet Set of logger to open context menu for
572     */
573    private void fillContextMenu(final Set<Logger> loggerSet) {
574
575        for (LoggerLevel level : LoggerLevel.values()) {
576            final LoggerLevel currentLevel = level;
577            ContextMenuItem item = m_menu.addItem(level.getLevelStringComplet());
578            item.setData(loggerSet);
579            item.addItemClickListener(new ContextMenuItemClickListener() {
580
581                public void contextMenuItemClicked(ContextMenuItemClickEvent event) {
582
583                    changeLoggerLevel(currentLevel, loggerSet);
584
585                }
586            });
587            if (loggerSet.size() == 1) {
588                if (level.getLevel().equals(loggerSet.iterator().next().getLevel())) {
589                    item.setIcon(FontOpenCms.CHECK_SMALL);
590                }
591            }
592        }
593        if (loggerSet.size() == 1) {
594            String message = CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGSETTINGS_NEWFILE_0);
595            if (isloggingactivated(loggerSet.iterator().next())) {
596                message = CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGSETTINGS_REMOVEFILE_0);
597            }
598            ContextMenuItem item = m_menu.addItem(message);
599            item.setData(loggerSet);
600            item.addItemClickListener(new ContextMenuItemClickListener() {
601
602                public void contextMenuItemClicked(ContextMenuItemClickEvent event) {
603
604                    toggleOwnFile(loggerSet.iterator().next());
605
606                }
607            });
608        }
609    }
610
611    /**
612     * Populate table.<p>
613     */
614    @SuppressWarnings("deprecation")
615    private void fillTable() {
616
617        removeAllItems();
618        List<Logger> loggerList = CmsLog4jUtil.getAllLoggers();
619        for (Logger logger : loggerList) {
620            addItemForLogger(logger);
621        }
622    }
623
624    /**
625     * Returns log files to given Logger.<p>
626     *
627     * @param logger to read files for
628     * @return path of file
629     */
630    private String getLogFiles(Logger logger) {
631
632        String test = "";
633        int count = 0;
634        // select the Appender from logger
635        for (Appender appender : logger.getAppenders().values()) {
636            // only use file appenders
637            if (CmsLogFileApp.isFileAppender(appender)) {
638                String fileName = CmsLogFileApp.getFileName(appender);
639                String temp = "";
640                temp = fileName.substring(fileName.lastIndexOf(File.separatorChar) + 1);
641                test = test + temp;
642                count++;
643                break;
644            }
645        }
646
647        //iterate all parent loggers until a logger with appender was found
648        while (!logger.equals(LogManager.getRootLogger())) {
649
650            logger = logger.getParent();
651            // if no Appender found from logger, select the Appender from parent logger
652            if (count == 0) {
653                for (Appender appender : logger.getAppenders().values()) {
654                    // only use file appenders
655                    if (CmsLogFileApp.isFileAppender(appender)) {
656                        String fileName = CmsLogFileApp.getFileName(appender);
657                        String temp = "";
658                        temp = fileName.substring(fileName.lastIndexOf(File.separatorChar) + 1);
659                        test = test + temp;
660                        count++;
661                        break;
662                    }
663                }
664            }
665        }
666
667        return test;
668    }
669
670    /**
671     * Updates table after change of file.<p>
672     */
673    private void updateFile() {
674
675        @SuppressWarnings("unchecked")
676        List<Logger> logger = (List<Logger>)m_container.getItemIds();
677
678        for (Logger item : logger) {
679            m_container.getItem(item).getItemProperty(TableColumn.File).setValue(getLogFiles(item));
680        }
681        resetPageBuffer();
682        refreshRenderedCells();
683        refreshRowCache();
684    }
685
686    /**
687     * Updates table after change of Level.<p>
688     */
689    private void updateLevel() {
690
691        @SuppressWarnings("unchecked")
692        List<Logger> logger = (List<Logger>)m_container.getItemIds();
693
694        for (Logger item : logger) {
695            m_container.getItem(item).getItemProperty(TableColumn.Level).setValue(LoggerLevel.fromLogger(item));
696        }
697        resetPageBuffer();
698        refreshRenderedCells();
699        refreshRowCache();
700    }
701}