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}