001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH (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 GmbH, 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.util; 029 030import org.opencms.i18n.CmsEncoder; 031import org.opencms.main.CmsIllegalArgumentException; 032import org.opencms.main.CmsLog; 033import org.opencms.main.CmsRuntimeException; 034import org.opencms.main.OpenCms; 035 036import java.io.BufferedReader; 037import java.io.ByteArrayOutputStream; 038import java.io.File; 039import java.io.FileInputStream; 040import java.io.FileNotFoundException; 041import java.io.IOException; 042import java.io.InputStreamReader; 043import java.io.LineNumberReader; 044import java.io.OutputStreamWriter; 045import java.nio.charset.Charset; 046import java.nio.charset.IllegalCharsetNameException; 047import java.nio.charset.UnsupportedCharsetException; 048import java.util.Stack; 049 050import org.apache.commons.logging.Log; 051 052/** 053 * The representation of a RFS file along with the settings to provide 054 * access to certain portions (amount of lines) of it. <p> 055 * 056 * Most often the underlying file will be the OpenCms logfile. <p> 057 * 058 * The portion of the file that is shown is defined by a "window" of "windowSize" lines of text 059 * at a position "windowPosition" which is an enumeration of windows in ascending order. <p> 060 * 061 * @since 6.0.0 062 */ 063public class CmsRfsFileViewer implements Cloneable { 064 065 /** The log object for this class. */ 066 protected static final Log LOG = CmsLog.getLog(CmsRfsFileViewer.class); 067 068 /** Decides whether the view onto the underlying file via readFilePortion is enabled. */ 069 private boolean m_enabled; 070 071 /** The character encoding of the underlying file. */ 072 private Charset m_fileEncoding; 073 074 /** The path to the underlying file. */ 075 protected String m_filePath; 076 077 /** The path to the root for all accessible files. */ 078 protected String m_rootPath; 079 080 /** 081 * If value is <code>true</code>, all setter methods will throw a 082 * <code>{@link CmsRuntimeException}</code><p>. 083 * 084 * Only the method <code>{@link #clone()}</code> returns a clone that has set this 085 * member to <code>false</code> allowing modification to take place.<p> 086 */ 087 private boolean m_frozen; 088 089 /** 090 * If true the represented file is a standard OpenCms log file and may be displayed 091 * in more convenient ways (in future versions) because the format is known. 092 */ 093 private boolean m_isLogfile; 094 095 /** The current window (numbered from zero to amount of possible different windows). */ 096 protected int m_windowPos; 097 098 /** The amount of lines to show. */ 099 protected int m_windowSize; 100 101 /** 102 * Creates an instance with default settings that tries to use the log file path obtained 103 * from <code>{@link OpenCms}'s {@link org.opencms.main.CmsSystemInfo}</code> instance.<p> 104 * 105 * If the log file path is invalid or not configured correctly a logging is performed and the 106 * path remains empty to allow user-specified file selection.<p> 107 */ 108 public CmsRfsFileViewer() { 109 110 if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) { 111 m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent(); 112 } 113 m_isLogfile = true; 114 // system default charset: see http://java.sun.com/j2se/corejava/intl/reference/faqs/index.html#default-encoding 115 m_fileEncoding = Charset.forName(new OutputStreamWriter(new ByteArrayOutputStream()).getEncoding()); 116 m_enabled = true; 117 m_windowSize = 200; 118 119 } 120 121 /** 122 * Internal helper that throws a <code>{@link CmsRuntimeException}</code> if the 123 * configuration of this instance has been frozen ({@link #setFrozen(boolean)}).<p> 124 * 125 * @throws CmsRuntimeException if the configuration of this instance has been frozen 126 * ({@link #setFrozen(boolean)}) 127 */ 128 private void checkFrozen() throws CmsRuntimeException { 129 130 if (m_frozen) { 131 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_FILE_VIEW_SETTINGS_FROZEN_0)); 132 } 133 } 134 135 /** 136 * Returns a clone of this file view settings that is not "frozen" and therefore allows modifications.<p> 137 * 138 * Every instance that plans to modify settings has to obtain a clone first that may be 139 * modified. The original instance returned from 140 * (<code>{@link org.opencms.workplace.CmsWorkplaceManager#getFileViewSettings()}</code>) will throw 141 * a <code>{@link CmsRuntimeException}</code> for each setter invocation. <p> 142 * 143 * @return a clone of this file view settings that is not "frozen" and therefore allows modifications 144 */ 145 @Override 146 public Object clone() { 147 148 // first run after installation: filePath & rootPath is null: 149 if (m_filePath == null) { 150 // below that runlevel the following call will fail (not initialized from config yet): 151 if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) { 152 m_filePath = OpenCms.getSystemInfo().getLogFileRfsPath(); 153 } 154 } 155 if (m_rootPath == null) { 156 if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) { 157 m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent(); 158 } 159 } 160 CmsRfsFileViewer clone = new CmsRfsFileViewer(); 161 clone.m_rootPath = m_rootPath; 162 try { 163 // strings are immutable: no outside modification possible. 164 clone.setFilePath(m_filePath); 165 } catch (CmsRfsException e) { 166 // will never happen because m_filePath was verified in setFilePath of this instance. 167 } catch (CmsRuntimeException e) { 168 // will never happen because m_filePath was verified in setFilePath of this instance. 169 } 170 clone.m_fileEncoding = m_fileEncoding; 171 clone.m_isLogfile = m_isLogfile; 172 clone.m_enabled = m_enabled; 173 //clone.m_windowPos = m_windowPos; 174 clone.setWindowSize(m_windowSize); 175 // allow clone-modifications. 176 clone.m_frozen = false; 177 return clone; 178 } 179 180 /** 181 * Returns the canonical name of the character encoding of the underlying file.<p> 182 * 183 * If no special choice is fed into 184 * <code>{@link #setFileEncoding(String)}</code> before this call 185 * always the system default character encoding is returned.<p> 186 * 187 * This value may be ignored outside and will be ignored inside if the 188 * underlying does not contain textual content.<p> 189 * 190 * @return the canonical name of the character encoding of the underlying file 191 */ 192 public String getFileEncoding() { 193 194 return m_fileEncoding.name(); 195 } 196 197 /** 198 * Returns the path denoting the file that is accessed.<p> 199 * 200 * @return the path denoting the file that is accessed 201 */ 202 public String getFilePath() { 203 204 return m_filePath; 205 } 206 207 /** 208 * Returns true if the view's internal file path points to a log file in standard OpenCms format.<p> 209 * 210 * @return true if the view's internal file path points to a log file in standard OpenCms format 211 */ 212 public boolean getIsLogfile() { 213 214 // method name is bean-convention of apache.commons.beanutils (unlike eclipse's convention for booleans) 215 return m_isLogfile; 216 } 217 218 /** 219 * Returns the start position of the current display.<p> 220 * 221 * This is a count of "windows" that 222 * consist of viewable text with "windowSize" lines of text (for a non-standard log file) or 223 * log-entries (for a standard log file).<p> 224 * 225 * @return the start position of the current display 226 */ 227 public int getWindowPos() { 228 229 return m_windowPos; 230 } 231 232 /** 233 * Returns the path denoting the root folder for all accessible files.<p> 234 * 235 * @return the path denoting the root folder for all accessible files 236 */ 237 public String getRootPath() { 238 239 return m_rootPath; 240 } 241 242 /** 243 * Get the amount of lines (or entries depending on whether a standard log file is shown) 244 * to display per page. <p> 245 * 246 * @return the amount of lines to display per page 247 */ 248 public int getWindowSize() { 249 250 return m_windowSize; 251 } 252 253 /** 254 * Returns true if this view upon the underlying file via 255 * <code>{@link #readFilePortion()}</code> is enabled.<p> 256 * 257 * 258 * @return true if this view upon the underlying file via 259 * <code>{@link #readFilePortion()}</code> is enabled.<p> 260 */ 261 public boolean isEnabled() { 262 263 return m_enabled; 264 } 265 266 /** 267 * Return the view portion of lines of text from the underlying file or an 268 * empty String if <code>{@link #isEnabled()}</code> returns <code>false</code>.<p> 269 * 270 * @return the view portion of lines of text from the underlying file or an 271 * empty String if <code>{@link #isEnabled()}</code> returns <code>false</code> 272 * @throws CmsRfsException if something goes wrong 273 */ 274 public String readFilePortion() throws CmsRfsException { 275 276 if (m_enabled) { 277 // if we want to view the log file we have to set the internal m_windowPos to the last window 278 // to view the end: 279 int lines = -1; 280 int startLine; 281 if (m_isLogfile) { 282 lines = scrollToFileEnd(); 283 // for logfile mode we show the last window of window size: 284 // it could be possible that only 4 lines are in the last window 285 // (e.g.: 123 lines with windowsize 10 -> last window has 3 lines) 286 // so we ignore the window semantics and show the n last lines: 287 startLine = lines - m_windowSize; 288 } else { 289 m_windowPos = 0; 290 startLine = m_windowPos * m_windowSize; 291 } 292 LineNumberReader reader = null; 293 try { 294 // don't make the buffer too big, just big enough for windowSize lines (estimation: avg. of 200 characters per line) 295 // to save reading too much (this optimizes to read the first windows, much later windows will be slower...) 296 reader = new LineNumberReader( 297 new BufferedReader(new InputStreamReader(new FileInputStream(m_filePath), m_fileEncoding)), 298 m_windowSize * 200); 299 int currentLine = 0; 300 // skip the lines to the current window: 301 while (startLine > currentLine) { 302 reader.readLine(); 303 currentLine++; 304 } 305 StringBuffer result = new StringBuffer(); 306 String read = reader.readLine(); 307 308 // logfile treatment is different 309 // we invert the lines: latest come first 310 if (m_isLogfile) { 311 // stack is java hall of shame member... but standard 312 Stack<String> inverter = new Stack<String>(); 313 for (int i = m_windowSize; (i > 0) && (read != null); i--) { 314 inverter.push(read); 315 read = reader.readLine(); 316 } 317 // pop-off: 318 while (!inverter.isEmpty()) { 319 result.append(inverter.pop()); 320 result.append('\n'); 321 } 322 } else { 323 for (int i = m_windowSize; (i > 0) && (read != null); i--) { 324 result.append(read); 325 result.append('\n'); 326 read = reader.readLine(); 327 } 328 } 329 return CmsEncoder.escapeXml(result.toString()); 330 } catch (IOException ioex) { 331 CmsRfsException ex = new CmsRfsException( 332 Messages.get().container(Messages.ERR_FILE_ARG_ACCESS_1, m_filePath), 333 ioex); 334 throw ex; 335 } finally { 336 if (reader != null) { 337 try { 338 reader.close(); 339 } catch (IOException e) { 340 LOG.error(e.getLocalizedMessage(), e); 341 } 342 } 343 } 344 } else { 345 return Messages.get().getBundle().key(Messages.GUI_FILE_VIEW_NO_PREVIEW_0); 346 } 347 } 348 349 /** 350 * Internally sets the member <code>m_windowPos</code> to the last available 351 * window of <code>m_windowSize</code> windows to let further calls to 352 * <code>{@link #readFilePortion()}</code> display the end of the file. <p> 353 * 354 * This method is triggered when a new file is chosen 355 * (<code>{@link #setFilePath(String)}</code>) because the amount of lines changes. 356 * This method is also triggered when a different window size is chosen 357 * (<code>{@link #setWindowSize(int)}</code>) because the amount of lines to display change. 358 * 359 * @return the amount of lines in the file to view 360 */ 361 private int scrollToFileEnd() { 362 363 int lines = 0; 364 if (OpenCms.getRunLevel() < OpenCms.RUNLEVEL_3_SHELL_ACCESS) { 365 // no scrolling if system not yet fully initialized 366 } else { 367 LineNumberReader reader = null; 368 // shift the window position to the end of the file: this is expensive but OK for ocs logfiles as they 369 // are ltd. to 2 MB 370 try { 371 reader = new LineNumberReader( 372 new BufferedReader(new InputStreamReader(new FileInputStream(m_filePath)))); 373 while (reader.readLine() != null) { 374 lines++; 375 } 376 reader.close(); 377 // if 11.75 windows are available, we don't want to end on window nr. 10 378 int availWindows = (int)Math.ceil((double)lines / (double)m_windowSize); 379 // we start with window 0 380 m_windowPos = availWindows - 1; 381 } catch (IOException ioex) { 382 LOG.error("Unable to scroll file " + m_filePath + " to end. Ensure that it exists. "); 383 } finally { 384 if (reader != null) { 385 try { 386 reader.close(); 387 } catch (Throwable f) { 388 LOG.info("Unable to close reader of file " + m_filePath, f); 389 } 390 } 391 } 392 } 393 return lines; 394 } 395 396 /** 397 * Set the boolean that decides if the view to the underlying file via 398 * <code>{@link #readFilePortion()}</code> is enabled.<p> 399 * 400 * @param preview the boolean that decides if the view to the underlying file via 401 * <code>{@link #readFilePortion()}</code> is enabled 402 */ 403 public void setEnabled(boolean preview) { 404 405 m_enabled = preview; 406 } 407 408 /** 409 * Set the character encoding of the underlying file.<p> 410 * 411 * The given String has to match a valid char set name (canonical or alias) 412 * of one of the system's supported <code>{@link Charset}</code> instances 413 * (see <code>{@link Charset#forName(java.lang.String)}</code>).<p> 414 * 415 * This setting will be used for reading the file. This enables to correctly 416 * display files with text in various encodings in UIs.<p> 417 * 418 * @param fileEncoding the character encoding of the underlying file to set 419 */ 420 public void setFileEncoding(String fileEncoding) { 421 422 checkFrozen(); 423 try { 424 m_fileEncoding = Charset.forName(fileEncoding); 425 } catch (IllegalCharsetNameException icne) { 426 throw new CmsIllegalArgumentException( 427 Messages.get().container(Messages.ERR_CHARSET_ILLEGAL_NAME_1, fileEncoding)); 428 } catch (UnsupportedCharsetException ucse) { 429 throw new CmsIllegalArgumentException( 430 Messages.get().container(Messages.ERR_CHARSET_UNSUPPORTED_1, fileEncoding)); 431 432 } 433 434 } 435 436 /** 437 * Set the path in the real file system that points to the file 438 * that should be displayed.<p> 439 * 440 * This method will only success if the file specified by the <code>path</code> 441 * argument is valid within the file system, no folder and may be read by the 442 * OpenCms process on the current platform.<p> 443 * 444 * @param path the path in the real file system that points to the file that should be displayed to set 445 * 446 * @throws CmsRuntimeException if the configuration of this instance has been frozen 447 * @throws CmsRfsException if the given path is invalid, does not point to a file or cannot be accessed 448 */ 449 public void setFilePath(String path) throws CmsRfsException, CmsRuntimeException { 450 451 checkFrozen(); 452 453 if (path != null) { 454 // leading whitespace from CmsComboWidget causes exception 455 path = path.trim(); 456 } 457 if (CmsStringUtil.isEmpty(path)) { 458 throw new CmsRfsException( 459 Messages.get().container(Messages.ERR_FILE_ARG_EMPTY_1, new Object[] {String.valueOf(path)})); 460 } 461 try { 462 // just for validation : 463 File file = new File(path); 464 if (file.isDirectory()) { 465 // if wrong configuration perform self healing: 466 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 467 // this deletes the illegal entry and will default to the log file path 468 m_filePath = null; 469 m_isLogfile = true; 470 } else { 471 throw new CmsRfsException( 472 Messages.get().container( 473 Messages.ERR_FILE_ARG_IS_FOLDER_1, 474 new Object[] {String.valueOf(path)})); 475 } 476 } else if (!file.isFile()) { 477 // if wrong configuration perform self healing: 478 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 479 // this deletes the illegal entry and will default to the log file path 480 m_filePath = null; 481 m_isLogfile = true; 482 } else { 483 throw new CmsRfsException( 484 Messages.get().container( 485 Messages.ERR_FILE_ARG_NOT_FOUND_1, 486 new Object[] {String.valueOf(path)})); 487 } 488 489 } else if (!file.canRead()) { 490 // if wrong configuration perform self healing: 491 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 492 // this deletes the illegal entry and will default to the log file path 493 m_filePath = null; 494 m_isLogfile = true; 495 } else { 496 throw new CmsRfsException( 497 Messages.get().container( 498 Messages.ERR_FILE_ARG_NOT_READ_1, 499 new Object[] {String.valueOf(path)})); 500 } 501 } else if ((m_rootPath != null) && !file.getCanonicalPath().startsWith(m_rootPath)) { 502 // if wrong configuration perform self healing: 503 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 504 // this deletes the illegal entry and will default to the log file path 505 m_filePath = null; 506 m_isLogfile = true; 507 } else { 508 throw new CmsRfsException( 509 Messages.get().container( 510 Messages.ERR_FILE_ARG_NOT_READ_1, 511 new Object[] {String.valueOf(path)})); 512 } 513 } else { 514 m_filePath = file.getCanonicalPath(); 515 } 516 } catch (FileNotFoundException fnfe) { 517 // if wrong configuration perform self healing: 518 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 519 // this deletes the illegal entry and will default to the log file path 520 m_filePath = null; 521 m_isLogfile = true; 522 } else { 523 throw new CmsRfsException( 524 Messages.get().container(Messages.ERR_FILE_ARG_NOT_FOUND_1, new Object[] {String.valueOf(path)}), 525 fnfe); 526 } 527 } catch (IOException ioex) { 528 // if wrong configuration perform self healing: 529 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 530 // this deletes the illegal entry and will default to the log file path 531 m_filePath = null; 532 m_isLogfile = true; 533 } else { 534 throw new CmsRfsException( 535 Messages.get().container(Messages.ERR_FILE_ARG_ACCESS_1, new Object[] {String.valueOf(path)}), 536 ioex); 537 } 538 539 } 540 } 541 542 /** 543 * Package friendly access that allows the <code>{@link org.opencms.workplace.CmsWorkplaceManager}</code> 544 * to "freeze" this instance within the system-wide assignment in it's 545 * <code>{@link org.opencms.workplace.CmsWorkplaceManager#setFileViewSettings(org.opencms.file.CmsObject, CmsRfsFileViewer)}</code> method.<p> 546 * 547 * @param frozen if true this instance will freeze and throw <code>CmsRuntimeExceptions</code> upon setter invocations 548 * 549 * @throws CmsRuntimeException if the configuration of this instance has been frozen 550 * ({@link #setFrozen(boolean)}) 551 */ 552 public void setFrozen(boolean frozen) throws CmsRuntimeException { 553 554 m_frozen = frozen; 555 } 556 557 /** 558 * Set if the internal file is in standard log file format (true) or not (false).<p> 559 * 560 * If set to true the file might be 561 * treated / displayed in a more convenient format than standard files in future. 562 * Currently it is only inverted (last lines appear first) and only the last 563 * 'Window Size' lines of the file are displayed.<p> 564 * 565 * Do not activate this (it is possible from the log file viewer settings in the workplace 566 * administration) if your selected file is no log file: The display will confuse you and 567 * be more expensive (imaging scrolling a 20 MB file to view the last 200 lines). <p> 568 * 569 * @param isLogfile determines if the internal file is in standard log file format (true) or not (false) 570 * 571 * @throws CmsRuntimeException if the configuration of this instance has been frozen 572 * ({@link #setFrozen(boolean)}) 573 */ 574 public void setIsLogfile(boolean isLogfile) throws CmsRuntimeException { 575 576 checkFrozen(); 577 m_isLogfile = isLogfile; 578 } 579 580 /** 581 * Set the path in the real file system that points to the folder/tree 582 * containing the log files.<p> 583 * 584 * This method will only success if the folder specified by the <code>path</code> 585 * argument is valid within the file system.<p> 586 * 587 * @param path the path in the real file system that points to the folder containing the log files 588 * 589 * @throws CmsRuntimeException if the configuration of this instance has been frozen 590 * @throws CmsRfsException if the given path is invalid 591 */ 592 public void setRootPath(String path) throws CmsRfsException, CmsRuntimeException { 593 594 checkFrozen(); 595 596 if (path != null) { 597 // leading whitespace from CmsComboWidget causes exception 598 path = path.trim(); 599 } 600 if (CmsStringUtil.isEmpty(path)) { 601 throw new CmsRfsException( 602 Messages.get().container(Messages.ERR_FILE_ARG_EMPTY_1, new Object[] {String.valueOf(path)})); 603 } 604 try { 605 // just for validation : 606 File file = new File(path); 607 if (file.exists()) { 608 m_rootPath = file.getCanonicalPath(); 609 } else { 610 // if wrong configuration perform self healing: 611 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 612 // this deletes the illegal entry 613 m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent(); 614 } else { 615 616 throw new CmsRfsException( 617 Messages.get().container( 618 Messages.ERR_FILE_ARG_NOT_FOUND_1, 619 new Object[] {String.valueOf(path)})); 620 } 621 } 622 } catch (IOException ioex) { 623 // if wrong configuration perform self healing: 624 if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) { 625 // this deletes the illegal entry and will default to the log file path 626 m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent(); 627 } else { 628 629 throw new CmsRfsException( 630 Messages.get().container(Messages.ERR_FILE_ARG_ACCESS_1, new Object[] {String.valueOf(path)}), 631 ioex); 632 } 633 } 634 } 635 636 /** 637 * Sets the start position of the current display.<p> 638 * 639 * This is a count of "windows" that 640 * consist of viewable text with "windowSize" lines of text (for a non-standard log file) or 641 * log-entries (for a standard log file).<p> 642 * 643 * @param windowPos the start position of the current display to set 644 * 645 * @throws CmsRuntimeException if the configuration of this instance has been frozen 646 * ({@link #setFrozen(boolean)}) 647 */ 648 public void setWindowPos(int windowPos) throws CmsRuntimeException { 649 650 checkFrozen(); 651 m_windowPos = windowPos; 652 } 653 654 /** 655 * Set the amount of lines (or entries depending on whether a standard log file is shown) 656 * to display per page.<p> 657 * 658 * @param windowSize the amount of lines to display per page 659 * 660 * @throws CmsRuntimeException if the configuration of this instance has been frozen 661 * ({@link #setFrozen(boolean)}) 662 */ 663 public void setWindowSize(int windowSize) throws CmsRuntimeException { 664 665 checkFrozen(); 666 m_windowSize = windowSize; 667 } 668}