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.main.CmsLog;
031import org.opencms.main.CmsRuntimeException;
032import org.opencms.main.OpenCms;
033import org.opencms.ui.A_CmsUI;
034import org.opencms.ui.CmsVaadinUtils;
035import org.opencms.ui.FontOpenCms;
036import org.opencms.ui.apps.A_CmsWorkplaceApp;
037import org.opencms.ui.apps.I_CmsAppUIContext;
038import org.opencms.ui.apps.I_CmsCRUDApp;
039import org.opencms.ui.apps.Messages;
040import org.opencms.ui.components.CmsAppViewLayout;
041import org.opencms.ui.components.CmsBasicDialog;
042import org.opencms.ui.components.CmsToolBar;
043import org.opencms.util.CmsLog4jUtil;
044import org.opencms.util.CmsRfsException;
045import org.opencms.util.CmsRfsFileViewer;
046import org.opencms.util.CmsStringUtil;
047
048import java.io.File;
049import java.lang.reflect.Method;
050import java.util.ArrayList;
051import java.util.LinkedHashMap;
052import java.util.LinkedHashSet;
053import java.util.List;
054import java.util.Map;
055import java.util.Set;
056
057import org.apache.commons.logging.Log;
058import org.apache.logging.log4j.Level;
059import org.apache.logging.log4j.LogManager;
060import org.apache.logging.log4j.core.Appender;
061import org.apache.logging.log4j.core.Layout;
062import org.apache.logging.log4j.core.LogEvent;
063import org.apache.logging.log4j.core.Logger;
064import org.apache.logging.log4j.core.LoggerContext;
065import org.apache.logging.log4j.core.appender.FileAppender;
066import org.apache.logging.log4j.core.appender.FileAppender.Builder;
067import org.apache.logging.log4j.core.config.Configuration;
068import org.apache.logging.log4j.core.config.LoggerConfig;
069import org.apache.logging.log4j.core.impl.Log4jLogEvent;
070import org.apache.logging.log4j.message.SimpleMessage;
071
072import com.google.common.collect.ComparisonChain;
073import com.vaadin.data.HasValue.ValueChangeEvent;
074import com.vaadin.data.HasValue.ValueChangeListener;
075import com.vaadin.server.FontAwesome;
076import com.vaadin.shared.ui.ValueChangeMode;
077import com.vaadin.ui.Button;
078import com.vaadin.ui.Button.ClickEvent;
079import com.vaadin.ui.Button.ClickListener;
080import com.vaadin.ui.Component;
081import com.vaadin.ui.Notification;
082import com.vaadin.ui.TextField;
083import com.vaadin.ui.UI;
084import com.vaadin.ui.Window;
085import com.vaadin.ui.Window.CloseEvent;
086import com.vaadin.ui.Window.CloseListener;
087import com.vaadin.ui.themes.ValoTheme;
088
089/**
090 * Main class of Log management app.<p>
091 */
092public class CmsLogFileApp extends A_CmsWorkplaceApp implements I_CmsCRUDApp<Logger> {
093
094    /**Log folder path.*/
095    protected static final String LOG_FOLDER =
096            OpenCms.getSystemInfo().getLogFileRfsFolder() == null ?
097                    "" : OpenCms.getSystemInfo().getLogFileRfsFolder();
098
099    /**Path to channel settings view.*/
100    protected static String PATH_LOGCHANNEL = "log-channel";
101
102    /**Logger.*/
103    private static Log LOG = CmsLog.getLog(CmsLogFileApp.class);
104
105    /** The prefix of opencms classes. */
106    private static final String OPENCMS_CLASS_PREFIX = "org.opencms";
107
108    /**The log file view layout.*/
109    protected CmsLogFileView m_fileView;
110
111    /**The log-channel table. */
112    protected CmsLogChannelTable m_table;
113
114    /** The file table filter input. */
115    private TextField m_tableFilter;
116
117    /**
118     * Gets the direct log filename of a given logger or null if no file is defined.<p>
119     *
120     * @param logger to be checked
121     * @return Log file name or null
122     */
123    public static String getDirectLogFile(Logger logger) {
124
125        LoggerConfig conf = logger.get();
126        while (conf != null) {
127            for (Appender appender : conf.getAppenders().values()) {
128                if (CmsLogFileApp.isFileAppender(appender)) {
129                    String path = CmsLogFileApp.getFileName(appender);
130                    String name = path.substring(path.lastIndexOf(File.separatorChar) + 1);
131                    return name;
132                }
133            }
134            conf = conf.getParent();
135        }
136        return null;
137    }
138
139    /**
140     * Returns the file name or <code>null</code> associated with the given appender.<p>
141     *
142     * @param app the appender
143     *
144     * @return the file name
145     */
146    public static String getFileName(Appender app) {
147
148        String result = null;
149        Method getFileName;
150        try {
151            getFileName = app.getClass().getDeclaredMethod("getFileName", (Class<?>[])null);
152
153            result = (String)getFileName.invoke(app, (Object[])null);
154        } catch (Exception e) {
155            LOG.warn(e.getLocalizedMessage(), e);
156        }
157        return result;
158    }
159
160    /**
161     * Checks whether the given log appender has a getFileName method to identify file based appenders.<p>
162     * As since log4j 2.0 file appenders don't have one common super class that allows access to the file name,
163     * but they all implement a method 'getFileName'.<p>
164     *
165     * @param appender the appender to check
166     *
167     * @return in case of a file based appender
168     */
169    public static boolean isFileAppender(Appender appender) {
170
171        boolean result = false;
172        try {
173            Method getFileNameMethod = appender.getClass().getDeclaredMethod("getFileName", (Class<?>[])null);
174            result = getFileNameMethod != null;
175
176        } catch (Exception e) {
177            LOG.debug(e.getLocalizedMessage(), e);
178        }
179        return result;
180    }
181
182    /**
183     * Simple check if the logger has the global log file <p> or a single one.
184     *
185     * @param logchannel the channel that has do be checked
186     * @return true if the the log channel has a single log file
187     * */
188    public static boolean isloggingactivated(Logger logchannel) {
189
190        boolean check = false;
191        for (Appender appender : logchannel.getAppenders().values()) {
192            check = appender.getName().equals(logchannel.getName());
193        }
194        return check;
195    }
196
197    /**
198     * Toggles the log file.<p>
199     *
200     * @param logchannel to toggle log file for
201     */
202    public static void toggleOwnFile(Logger logchannel) {
203
204        String filepath = "";
205
206        Layout layout = null;
207        // if the button is activated check the value of the button
208        // the button was active
209        if (isloggingactivated(logchannel)) {
210            // remove the private Appender from logger
211            for (Appender appender : logchannel.getAppenders().values()) {
212                logchannel.removeAppender(appender);
213            }
214            // activate the heredity so the logger get the appender from parent logger
215            logchannel.setAdditive(true);
216
217        }
218        // the button was inactive
219        else {
220            // get the layout and file path from root logger
221            for (Appender appender : ((Logger)LogManager.getRootLogger()).getAppenders().values()) {
222                if (CmsLogFileApp.isFileAppender(appender)) {
223                    String fileName = CmsLogFileApp.getFileName(appender);
224                    filepath = fileName.substring(0, fileName.lastIndexOf(File.separatorChar));
225                    layout = appender.getLayout();
226                    break;
227                }
228            }
229
230            // check if the logger has an Appender get his layout
231            for (Appender appender : logchannel.getAppenders().values()) {
232                if (CmsLogFileApp.isFileAppender(appender)) {
233                    layout = appender.getLayout();
234                    break;
235                }
236            }
237            String logfilename = "";
238            String temp = logchannel.getName();
239            // check if the logger name begins with "org.opencms"
240            if (logchannel.getName().contains(OPENCMS_CLASS_PREFIX)) {
241                // remove the prefix "org.opencms" from logger name to generate the file name
242                temp = temp.replace(OPENCMS_CLASS_PREFIX, "");
243                // if the name has suffix
244                if (temp.length() >= 1) {
245                    logfilename = filepath + File.separator + "opencms-" + temp.substring(1).replace(".", "-") + ".log";
246                }
247                // if the name has no suffix
248                else {
249                    logfilename = filepath + File.separator + "opencms" + temp.replace(".", "-") + ".log";
250                }
251            }
252            // if the logger name not begins with "org.opencms"
253            else {
254                logfilename = filepath + File.separator + "opencms-" + temp.replace(".", "-") + ".log";
255            }
256
257            FileAppender fapp = ((Builder)FileAppender.<FileAppender.Builder> newBuilder().withFileName(
258                logfilename).withLayout(layout).withName(logchannel.getName())).build();
259
260            // deactivate the heredity so the logger get no longer the appender from parent logger
261            logchannel.setAdditive(false);
262            // remove all active Appenders from logger
263            for (Appender appender : logchannel.getAppenders().values()) {
264                logchannel.removeAppender(appender);
265            }
266            // add the new created Appender to the logger
267            logchannel.addAppender(fapp);
268        }
269
270    }
271
272    /**
273     * Adds a marker entry to the currently selected log file.
274     *
275     * @param logFile the log file name
276     */
277    public void addMark(String logFile) {
278
279        LoggerContext context = (LoggerContext)LogManager.getContext(false);
280        List<Logger> loggers = new ArrayList<>(context.getLoggers());
281        // Sort loggers by name to prioritize parent over child loggers
282        loggers.sort((l1, l2) -> ComparisonChain.start().compare(l1.getName(), l2.getName()).result());
283        boolean found = false;
284        loggerLoop: for (Logger logger : loggers) {
285            for (Map.Entry<String, Appender> entry : logger.getAppenders().entrySet()) {
286                Appender appender = entry.getValue();
287                if (logFile.equals(getFileName(entry.getValue()))) {
288                    String message = "---------- Mark created by '"
289                        + A_CmsUI.getCmsObject().getRequestContext().getCurrentUser().getName()
290                        + "' ----------";
291                    LogEvent event = Log4jLogEvent.newBuilder().setLevel(Level.INFO).setLoggerName(
292                        "org.opencms").setIncludeLocation(true).setLoggerFqcn(CmsLogFileApp.class.getName()).setMessage(
293                            new SimpleMessage(message)).build();
294
295                    // break loggerLoop;
296                    appender.append(event);
297                    found = true;
298                    break loggerLoop;
299                }
300            }
301        }
302
303        if (!found) {
304            Notification.show(CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGFILE_NOT_ACTIVE_0));
305        }
306
307    }
308
309    /**
310     * @see org.opencms.ui.apps.I_CmsCRUDApp#createElement(java.lang.Object)
311     */
312    public void createElement(Logger element) {
313
314        return;
315
316    }
317
318    /**
319     * @see org.opencms.ui.apps.I_CmsCRUDApp#defaultAction(java.lang.String)
320     */
321    public void defaultAction(String elementId) {
322
323        return;
324
325    }
326
327    /**
328     * @see org.opencms.ui.apps.I_CmsCRUDApp#deleteElements(java.util.List)
329     */
330    public void deleteElements(List<String> elementId) {
331
332        return;
333
334    }
335
336    /**
337     * @see org.opencms.ui.apps.I_CmsCRUDApp#getAllElements()
338     */
339    public List<Logger> getAllElements() {
340
341        return CmsLog4jUtil.getAllLoggers();
342    }
343
344    /**
345     * Gets the available log file paths.<p>
346     *
347     * @return Set of paths
348     */
349    public Set<String> getAvailableLogFilePaths() {
350
351        Set<File> files = CmsLogFileOptionProvider.getLogFiles();
352        Set<String> res = new LinkedHashSet<String>();
353        for (File f : files) {
354            if (!f.getAbsolutePath().endsWith(".zip") && !f.getAbsolutePath().endsWith(".gz")) {
355                res.add(f.getAbsolutePath());
356            }
357        }
358        return res;
359    }
360
361    /**
362     * Gets the default log file path.<p>
363     *
364     * @param logView logview
365     * @return log file path
366     */
367    public String getDefaultLogFilePath(CmsRfsFileViewer logView) {
368
369        List<Logger> allLogger = CmsLog4jUtil.getAllLoggers();
370        List<Appender> allAppender = new ArrayList<Appender>();
371
372        allLogger.add(0, (Logger)LogManager.getRootLogger());
373
374        for (Logger logger : allLogger) {
375
376            for (Appender appender : logger.getAppenders().values()) {
377                if (CmsLogFileApp.isFileAppender(appender)) {
378                    if (!allAppender.contains(appender)) {
379                        allAppender.add(appender);
380                    }
381
382                }
383            }
384        }
385        for (Appender app : allAppender) {
386
387            String fileName = CmsLogFileApp.getFileName(app);
388            if ((fileName != null) && fileName.equals(logView.getFilePath())) {
389
390                return fileName;
391            }
392        }
393        if (!allAppender.isEmpty()) {
394            Appender app = allAppender.get(0);
395            String fileName = CmsLogFileApp.getFileName(app);
396            return fileName;
397        }
398        return null;
399    }
400
401    /**
402     * @see org.opencms.ui.apps.I_CmsCRUDApp#getElement(java.lang.String)
403     */
404    public Logger getElement(String elementId) {
405
406        return null;
407    }
408
409    /**
410     * Gets the log file for the logger.<p>
411     *
412     * @param logger to get log file for
413     * @return log file
414     */
415    public String getLogFile(Logger logger) {
416
417        return getDirectLogFile(logger);
418
419    }
420
421    /**
422     * Gets the portion of given log file.<p>
423     *
424     * @param logView to get portion with
425     * @param currentFile to read
426     * @return portion of log file
427     * @throws CmsRfsException exception
428     * @throws CmsRuntimeException exception
429     */
430    public String getLogFilePortion(CmsRfsFileViewer logView, String currentFile)
431    throws CmsRfsException, CmsRuntimeException {
432
433        logView.setFilePath(currentFile);
434        return logView.readFilePortion();
435    }
436
437    /**
438     * @see org.opencms.ui.apps.A_CmsWorkplaceApp#initUI(org.opencms.ui.apps.I_CmsAppUIContext)
439     */
440    @Override
441    public void initUI(I_CmsAppUIContext context) {
442
443        super.initUI(context);
444    }
445
446    /**
447     * Toggles if channel has own log file.<p>
448     *
449     * @param logchannel to toggle
450     */
451    public void toggleOwnFileForLogger(Logger logchannel) {
452
453        CmsLogFileApp.toggleOwnFile(logchannel);
454    }
455
456    /**
457     * Updates the log channel table.<p>
458     */
459    public void updateTable() {
460
461        if (m_table != null) {
462            m_table = new CmsLogChannelTable(this);
463            m_table.setSizeFull();
464            m_rootLayout.setMainContent(m_table);
465            m_table.filterTable(m_tableFilter.getValue());
466        }
467    }
468
469    /**
470     * @see org.opencms.ui.apps.I_CmsCRUDApp#writeElement(java.lang.Object)
471     */
472    public void writeElement(Logger logger) {
473
474        @SuppressWarnings("resource")
475        LoggerContext context = logger.getContext();
476        Configuration config = context.getConfiguration();
477        LoggerConfig loggerConfig = config.getLoggerConfig(logger.getName());
478        LoggerConfig specificConfig = loggerConfig;
479        if (!loggerConfig.getName().equals(logger.getName())) {
480            specificConfig = new LoggerConfig(logger.getName(), logger.getLevel(), true);
481            specificConfig.setParent(loggerConfig);
482            config.addLogger(logger.getName(), specificConfig);
483        }
484        specificConfig.setLevel(logger.getLevel());
485        context.updateLoggers();
486
487    }
488
489    /**
490     * Button to open channel settings path.<p>
491     */
492    protected void addChannelButton() {
493
494        Button button = CmsToolBar.createButton(
495            FontOpenCms.LOG,
496            CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGSETTINGS_TOOL_NAME_0));
497        button.addClickListener(new ClickListener() {
498
499            private static final long serialVersionUID = 1L;
500
501            public void buttonClick(ClickEvent event) {
502
503                openSubView(PATH_LOGCHANNEL, true);
504            }
505        });
506        m_uiContext.addToolbarButton(button);
507
508    }
509
510    /**
511     * Adds the download button.
512     *
513     * @param view layout which displays the log file
514     */
515    protected void addDownloadButton(final CmsLogFileView view) {
516
517        Button button = CmsToolBar.createButton(
518            FontOpenCms.DOWNLOAD,
519            CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_DOWNLOAD_0));
520        button.addClickListener(new ClickListener() {
521
522            private static final long serialVersionUID = 1L;
523
524            public void buttonClick(ClickEvent event) {
525
526                Window window = CmsBasicDialog.prepareWindow(CmsBasicDialog.DialogWidth.wide);
527                window.setCaption(CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_DOWNLOAD_0));
528                window.setContent(new CmsLogDownloadDialog(window, view.getCurrentFile(), getLogDownloadProvider()));
529                A_CmsUI.get().addWindow(window);
530            }
531        });
532        m_uiContext.addToolbarButton(button);
533    }
534
535    /**
536     * Button to add a marker to the current log file.
537     */
538    protected void addMarkButton() {
539
540        Button button = CmsToolBar.createButton(
541            FontAwesome.PLUS,
542            CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_ADD_MARK_0));
543        button.addClickListener(new ClickListener() {
544
545            private static final long serialVersionUID = 1L;
546
547            public void buttonClick(ClickEvent event) {
548
549                if ((m_fileView != null) && (m_fileView.getCurrentFile() != null)) {
550                    addMark(m_fileView.getCurrentFile());
551                    m_fileView.updateView();
552                }
553            }
554        });
555        m_uiContext.addToolbarButton(button);
556
557    }
558
559    /**
560     * Adds the publish button.
561     */
562    protected void addPublishButton() {
563
564        m_uiContext.addToolbarButton(CmsAppViewLayout.createPublishButton(ids -> {}));
565    }
566
567    /**
568     * Button to refresh the file view.<p>
569     */
570    protected void addRefreshButton() {
571
572        Button button = CmsToolBar.createButton(
573            FontOpenCms.RESET,
574            CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_REFRESH_FILEVIEW_0));
575        button.addClickListener(new ClickListener() {
576
577            private static final long serialVersionUID = 1L;
578
579            public void buttonClick(ClickEvent event) {
580
581                CmsLog.INIT.info(
582                    "Logfile was reloaded by user "
583                        + A_CmsUI.getCmsObject().getRequestContext().getCurrentUser().getName());
584                m_fileView.updateView();
585
586            }
587        });
588        m_uiContext.addToolbarButton(button);
589    }
590
591    /**
592     * Button to open log file view settings dialog.<p>
593     */
594    protected void addSettingsButton() {
595
596        Button button = CmsToolBar.createButton(
597            FontOpenCms.SETTINGS,
598            CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGSETTINGS_TOOL_NAME_SHORT_0));
599        button.addClickListener(new ClickListener() {
600
601            private static final long serialVersionUID = 1L;
602
603            public void buttonClick(ClickEvent event) {
604
605                Window window = CmsBasicDialog.prepareWindow(CmsBasicDialog.DialogWidth.wide);
606                window.setCaption(CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGVIEW_SETTINGS_SHORT_0));
607                window.setContent(new CmsLogFileViewSettings(window));
608                window.addCloseListener(new CloseListener() {
609
610                    private static final long serialVersionUID = -7058276628732771106L;
611
612                    public void windowClose(CloseEvent e) {
613
614                        m_fileView.updateView();
615                    }
616                });
617                A_CmsUI.get().addWindow(window);
618            }
619        });
620        m_uiContext.addToolbarButton(button);
621    }
622
623    /**
624     * Filters the table.<p>
625     *
626     * @param filter text to be filtered
627     */
628    protected void filterTable(String filter) {
629
630        m_table.filterTable(filter);
631    }
632
633    /**
634     * @see org.opencms.ui.apps.A_CmsWorkplaceApp#getBreadCrumbForState(java.lang.String)
635     */
636    @Override
637    protected LinkedHashMap<String, String> getBreadCrumbForState(String state) {
638
639        LinkedHashMap<String, String> crumbs = new LinkedHashMap<String, String>();
640
641        //Check if state is empty -> start
642        if (CmsStringUtil.isEmptyOrWhitespaceOnly(state)) {
643            crumbs.put("", CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_VIEW_TOOL_NAME_0));
644            return crumbs;
645        }
646        if (state.startsWith(PATH_LOGCHANNEL)) {
647            crumbs.put(
648                CmsLogFileConfiguration.APP_ID,
649                CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_VIEW_TOOL_NAME_0));
650            crumbs.put("", CmsVaadinUtils.getMessageText(Messages.GUI_LOGFILE_LOGSETTINGS_TOOL_NAME_0));
651            return crumbs;
652        }
653
654        return new LinkedHashMap<String, String>();
655    }
656
657    /**
658     * @see org.opencms.ui.apps.A_CmsWorkplaceApp#getComponentForState(java.lang.String)
659     */
660    @Override
661    protected Component getComponentForState(String state) {
662
663        if (m_tableFilter != null) {
664            m_infoLayout.removeComponent(m_tableFilter);
665            m_tableFilter = null;
666        }
667
668        if (!state.startsWith(PATH_LOGCHANNEL)) {
669            m_rootLayout.setMainHeightFull(true);
670            m_fileView = new CmsLogFileView(this);
671            addPublishButton();
672            addDownloadButton(m_fileView);
673            addSettingsButton();
674            addChannelButton();
675            addMarkButton();
676            addRefreshButton();
677            m_table = null;
678            return m_fileView;
679        }
680
681        m_uiContext.clearToolbarButtons();
682
683        m_rootLayout.setMainHeightFull(true);
684        m_table = new CmsLogChannelTable(this);
685        m_tableFilter = new TextField();
686        m_tableFilter.setIcon(FontOpenCms.FILTER);
687        m_tableFilter.setPlaceholder(
688            Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_EXPLORER_FILTER_0));
689        m_tableFilter.addStyleName(ValoTheme.TEXTFIELD_INLINE_ICON);
690        m_tableFilter.setWidth("200px");
691        m_tableFilter.setValueChangeMode(ValueChangeMode.TIMEOUT);
692        m_tableFilter.setValueChangeTimeout(400);
693        m_tableFilter.addValueChangeListener(new ValueChangeListener<String>() {
694
695            private static final long serialVersionUID = 1L;
696
697            public void valueChange(ValueChangeEvent<String> event) {
698
699                filterTable(event.getValue());
700            }
701        });
702
703        m_infoLayout.addComponent(m_tableFilter);
704        return m_table;
705
706    }
707
708    /**
709     * Gets the download provider for the log download dialog.
710     */
711    protected I_CmsLogDownloadProvider getLogDownloadProvider() {
712
713        return new CmsDefaultLogDownloadProvider();
714    }
715
716    /**
717     * @see org.opencms.ui.apps.A_CmsWorkplaceApp#getSubNavEntries(java.lang.String)
718     */
719    @Override
720    protected List<NavEntry> getSubNavEntries(String state) {
721
722        return null;
723    }
724
725}