org.opencms.util.CmsRfsFileViewer Maven / Gradle / Ivy
Show all versions of opencms-core Show documentation
/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* For further information about Alkacon Software GmbH & Co. KG, please see the
* company website: http://www.alkacon.com
*
* For further information about OpenCms, please see the
* project website: http://www.opencms.org
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.opencms.util;
import org.opencms.i18n.CmsEncoder;
import org.opencms.main.CmsIllegalArgumentException;
import org.opencms.main.CmsLog;
import org.opencms.main.CmsRuntimeException;
import org.opencms.main.OpenCms;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
/**
* The representation of a RFS file along with the settings to provide
* access to certain portions (amount of lines) of it.
*
* Most often the underlying file will be the OpenCms logfile.
*
* The portion of the file that is shown is defined by a "window" of "windowSize" lines of text
* at a position "windowPosition" which is an enumeration of windows in ascending order.
*
* @since 6.0.0
*/
public class CmsRfsFileViewer implements Cloneable {
/** The log object for this class. */
protected static final Log LOG = CmsLog.getLog(CmsRfsFileViewer.class);
/** The path to the underlying file. */
protected String m_filePath;
/** The path to the root for all accessible files. */
protected String m_rootPath;
/** The current window (numbered from zero to amount of possible different windows). */
protected int m_windowPos;
/** The amount of lines to show. */
protected int m_windowSize;
/** The additional allowed RFS roots for viewing files. */
private List m_additionalRoots;
/** Decides whether the view onto the underlying file via readFilePortion is enabled. */
private boolean m_enabled;
/** The character encoding of the underlying file. */
private Charset m_fileEncoding;
/**
* If value is true
, all setter methods will throw a
* {@link CmsRuntimeException}
.
*
* Only the method {@link #clone()}
returns a clone that has set this
* member to false
allowing modification to take place.
*/
private boolean m_frozen;
/**
* If true the represented file is a standard OpenCms log file and may be displayed
* in more convenient ways (in future versions) because the format is known.
*/
private boolean m_isLogfile;
/**
* Creates an instance with default settings that tries to use the log file path obtained
* from {@link OpenCms}'s {@link org.opencms.main.CmsSystemInfo}
instance.
*
* If the log file path is invalid or not configured correctly a logging is performed and the
* path remains empty to allow user-specified file selection.
*/
public CmsRfsFileViewer() {
m_rootPath = getLogFolderPath();
m_isLogfile = true;
// system default charset: see http://java.sun.com/j2se/corejava/intl/reference/faqs/index.html#default-encoding
m_fileEncoding = Charset.forName(new OutputStreamWriter(new ByteArrayOutputStream()).getEncoding());
m_enabled = true;
m_windowSize = 1000;
}
/**
* Returns a clone of this file view settings that is not "frozen" and therefore allows modifications.
*
* Every instance that plans to modify settings has to obtain a clone first that may be
* modified. The original instance returned from
* ({@link org.opencms.workplace.CmsWorkplaceManager#getFileViewSettings()}
) will throw
* a {@link CmsRuntimeException}
for each setter invocation.
*
* @return a clone of this file view settings that is not "frozen" and therefore allows modifications
*/
@Override
public Object clone() {
// first run after installation: filePath & rootPath is null:
if (m_filePath == null) {
// below that runlevel the following call will fail (not initialized from config yet):
if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
m_filePath = OpenCms.getSystemInfo().getLogFileRfsPath();
}
}
if (m_rootPath == null) {
m_rootPath = getLogFolderPath();
}
CmsRfsFileViewer clone = new CmsRfsFileViewer();
clone.m_rootPath = m_rootPath;
try {
// strings are immutable: no outside modification possible.
clone.setFilePath(m_filePath);
} catch (CmsRfsException e) {
// will never happen because m_filePath was verified in setFilePath of this instance.
} catch (CmsRuntimeException e) {
// will never happen because m_filePath was verified in setFilePath of this instance.
}
clone.m_fileEncoding = m_fileEncoding;
clone.m_isLogfile = m_isLogfile;
clone.m_enabled = m_enabled;
//clone.m_windowPos = m_windowPos;
clone.setWindowSize(m_windowSize);
// allow clone-modifications.
clone.m_frozen = false;
return clone;
}
/**
* Returns the canonical name of the character encoding of the underlying file.
*
* If no special choice is fed into
* {@link #setFileEncoding(String)}
before this call
* always the system default character encoding is returned.
*
* This value may be ignored outside and will be ignored inside if the
* underlying does not contain textual content.
*
* @return the canonical name of the character encoding of the underlying file
*/
public String getFileEncoding() {
return m_fileEncoding.name();
}
/**
* Returns the path denoting the file that is accessed.
*
* @return the path denoting the file that is accessed
*/
public String getFilePath() {
return m_filePath;
}
/**
* Returns true if the view's internal file path points to a log file in standard OpenCms format.
*
* @return true if the view's internal file path points to a log file in standard OpenCms format
*/
public boolean getIsLogfile() {
// method name is bean-convention of apache.commons.beanutils (unlike eclipse's convention for booleans)
return m_isLogfile;
}
/**
* Returns the path denoting the root folder for all accessible files.
*
* @return the path denoting the root folder for all accessible files
*/
public String getRootPath() {
return m_rootPath;
}
/**
* Returns the start position of the current display.
*
* This is a count of "windows" that
* consist of viewable text with "windowSize" lines of text (for a non-standard log file) or
* log-entries (for a standard log file).
*
* @return the start position of the current display
*/
public int getWindowPos() {
return m_windowPos;
}
/**
* Get the amount of lines (or entries depending on whether a standard log file is shown)
* to display per page.
*
* @return the amount of lines to display per page
*/
public int getWindowSize() {
return m_windowSize;
}
/**
* Returns true if this view upon the underlying file via
* {@link #readFilePortion()}
is enabled.
*
*
* @return true if this view upon the underlying file via
* {@link #readFilePortion()}
is enabled.
*/
public boolean isEnabled() {
return m_enabled;
}
/**
* Return the view portion of lines of text from the underlying file or an
* empty String if {@link #isEnabled()}
returns false
.
*
* @return the view portion of lines of text from the underlying file or an
* empty String if {@link #isEnabled()}
returns false
* @throws CmsRfsException if something goes wrong
*/
public String readFilePortion() throws CmsRfsException {
if (m_enabled) {
// if we want to view the log file we have to set the internal m_windowPos to the last window
// to view the end:
int lines = -1;
int startLine;
if (m_isLogfile) {
lines = scrollToFileEnd();
// for logfile mode we show the last window of window size:
// it could be possible that only 4 lines are in the last window
// (e.g.: 123 lines with windowsize 10 -> last window has 3 lines)
// so we ignore the window semantics and show the n last lines:
startLine = lines - m_windowSize;
} else {
m_windowPos = 0;
startLine = m_windowPos * m_windowSize;
}
LineNumberReader reader = null;
try {
// don't make the buffer too big, just big enough for windowSize lines (estimation: avg. of 200 characters per line)
// to save reading too much (this optimizes to read the first windows, much later windows will be slower...)
reader = new LineNumberReader(
new BufferedReader(new InputStreamReader(new FileInputStream(m_filePath), m_fileEncoding)),
m_windowSize * 200);
int currentLine = 0;
// skip the lines to the current window:
while (startLine > currentLine) {
reader.readLine();
currentLine++;
}
StringBuffer result = new StringBuffer();
String read = reader.readLine();
for (int i = m_windowSize; (i > 0) && (read != null); i--) {
result.append(read);
result.append('\n');
read = reader.readLine();
}
return CmsEncoder.escapeXml(result.toString());
} catch (IOException ioex) {
CmsRfsException ex = new CmsRfsException(
Messages.get().container(Messages.ERR_FILE_ARG_ACCESS_1, m_filePath),
ioex);
throw ex;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
LOG.error(e.getLocalizedMessage(), e);
}
}
}
} else {
return Messages.get().getBundle().key(Messages.GUI_FILE_VIEW_NO_PREVIEW_0);
}
}
/**
* Sets the additional root folders from which files can be viewed.
*
* @param roots the list of additional root folders
*/
public void setAdditionalRoots(List roots) {
List additionalRoots = new ArrayList<>();
// making sure all paths end with the path separator CHAR
for (String path : roots) {
if (path != null) {
if (!path.endsWith(File.separator)) {
path += File.separator;
}
}
additionalRoots.add(path);
}
m_additionalRoots = additionalRoots;
}
/**
* Set the boolean that decides if the view to the underlying file via
* {@link #readFilePortion()}
is enabled.
*
* @param preview the boolean that decides if the view to the underlying file via
* {@link #readFilePortion()}
is enabled
*/
public void setEnabled(boolean preview) {
m_enabled = preview;
}
/**
* Set the character encoding of the underlying file.
*
* The given String has to match a valid char set name (canonical or alias)
* of one of the system's supported {@link Charset}
instances
* (see {@link Charset#forName(java.lang.String)}
).
*
* This setting will be used for reading the file. This enables to correctly
* display files with text in various encodings in UIs.
*
* @param fileEncoding the character encoding of the underlying file to set
*/
public void setFileEncoding(String fileEncoding) {
checkFrozen();
try {
m_fileEncoding = Charset.forName(fileEncoding);
} catch (IllegalCharsetNameException icne) {
throw new CmsIllegalArgumentException(
Messages.get().container(Messages.ERR_CHARSET_ILLEGAL_NAME_1, fileEncoding));
} catch (UnsupportedCharsetException ucse) {
throw new CmsIllegalArgumentException(
Messages.get().container(Messages.ERR_CHARSET_UNSUPPORTED_1, fileEncoding));
}
}
/**
* Set the path in the real file system that points to the file
* that should be displayed.
*
* This method will only success if the file specified by the path
* argument is valid within the file system, no folder and may be read by the
* OpenCms process on the current platform.
*
* @param path the path in the real file system that points to the file that should be displayed to set
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* @throws CmsRfsException if the given path is invalid, does not point to a file or cannot be accessed
*/
public void setFilePath(String path) throws CmsRfsException, CmsRuntimeException {
checkFrozen();
if (path != null) {
// leading whitespace from CmsComboWidget causes exception
path = path.trim();
}
if (CmsStringUtil.isEmpty(path)) {
throw new CmsRfsException(
Messages.get().container(Messages.ERR_FILE_ARG_EMPTY_1, new Object[] {String.valueOf(path)}));
}
try {
// just for validation :
File file = new File(path);
if (file.isDirectory()) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_filePath = null;
m_isLogfile = true;
} else {
throw new CmsRfsException(
Messages.get().container(
Messages.ERR_FILE_ARG_IS_FOLDER_1,
new Object[] {String.valueOf(path)}));
}
} else if (!file.isFile()) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_filePath = null;
m_isLogfile = true;
} else {
throw new CmsRfsException(
Messages.get().container(
Messages.ERR_FILE_ARG_NOT_FOUND_1,
new Object[] {String.valueOf(path)}));
}
} else if (!file.canRead()) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_filePath = null;
m_isLogfile = true;
} else {
throw new CmsRfsException(
Messages.get().container(
Messages.ERR_FILE_ARG_NOT_READ_1,
new Object[] {String.valueOf(path)}));
}
} else if ((m_rootPath != null) && !isInRoots(file.getCanonicalPath())) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_filePath = null;
m_isLogfile = true;
} else {
throw new CmsRfsException(
Messages.get().container(
Messages.ERR_FILE_ARG_NOT_READ_1,
new Object[] {String.valueOf(path)}));
}
} else {
m_filePath = file.getCanonicalPath();
}
} catch (FileNotFoundException fnfe) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_filePath = null;
m_isLogfile = true;
} else {
throw new CmsRfsException(
Messages.get().container(Messages.ERR_FILE_ARG_NOT_FOUND_1, new Object[] {String.valueOf(path)}),
fnfe);
}
} catch (IOException ioex) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_filePath = null;
m_isLogfile = true;
} else {
throw new CmsRfsException(
Messages.get().container(Messages.ERR_FILE_ARG_ACCESS_1, new Object[] {String.valueOf(path)}),
ioex);
}
}
}
/**
* Package friendly access that allows the {@link org.opencms.workplace.CmsWorkplaceManager}
* to "freeze" this instance within the system-wide assignment in it's
* {@link org.opencms.workplace.CmsWorkplaceManager#setFileViewSettings(org.opencms.file.CmsObject, CmsRfsFileViewer)}
method.
*
* @param frozen if true this instance will freeze and throw CmsRuntimeExceptions
upon setter invocations
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* ({@link #setFrozen(boolean)})
*/
public void setFrozen(boolean frozen) throws CmsRuntimeException {
m_frozen = frozen;
}
/**
* Set if the internal file is in standard log file format (true) or not (false).
*
* If set to true the file might be
* treated / displayed in a more convenient format than standard files in future.
* Currently it is only inverted (last lines appear first) and only the last
* 'Window Size' lines of the file are displayed.
*
* Do not activate this (it is possible from the log file viewer settings in the workplace
* administration) if your selected file is no log file: The display will confuse you and
* be more expensive (imaging scrolling a 20 MB file to view the last 200 lines).
*
* @param isLogfile determines if the internal file is in standard log file format (true) or not (false)
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* ({@link #setFrozen(boolean)})
*/
public void setIsLogfile(boolean isLogfile) throws CmsRuntimeException {
checkFrozen();
m_isLogfile = isLogfile;
}
/**
* Set the path in the real file system that points to the folder/tree
* containing the log files.
*
* This method will only success if the folder specified by the path
* argument is valid within the file system.
*
* @param path the path in the real file system that points to the folder containing the log files
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* @throws CmsRfsException if the given path is invalid
*/
public void setRootPath(String path) throws CmsRfsException, CmsRuntimeException {
checkFrozen();
if (path != null) {
// leading whitespace from CmsComboWidget causes exception
path = path.trim();
}
if (CmsStringUtil.isEmpty(path)) {
throw new CmsRfsException(
Messages.get().container(Messages.ERR_FILE_ARG_EMPTY_1, new Object[] {String.valueOf(path)}));
}
try {
// just for validation :
File file = new File(path);
if (file.exists()) {
m_rootPath = file.getCanonicalPath();
} else {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry
m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent();
} else {
throw new CmsRfsException(
Messages.get().container(
Messages.ERR_FILE_ARG_NOT_FOUND_1,
new Object[] {String.valueOf(path)}));
}
}
} catch (IOException ioex) {
// if wrong configuration perform self healing:
if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
// this deletes the illegal entry and will default to the log file path
m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent();
} else {
throw new CmsRfsException(
Messages.get().container(Messages.ERR_FILE_ARG_ACCESS_1, new Object[] {String.valueOf(path)}),
ioex);
}
}
}
/**
* Sets the start position of the current display.
*
* This is a count of "windows" that
* consist of viewable text with "windowSize" lines of text (for a non-standard log file) or
* log-entries (for a standard log file).
*
* @param windowPos the start position of the current display to set
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* ({@link #setFrozen(boolean)})
*/
public void setWindowPos(int windowPos) throws CmsRuntimeException {
checkFrozen();
m_windowPos = windowPos;
}
/**
* Set the amount of lines (or entries depending on whether a standard log file is shown)
* to display per page.
*
* @param windowSize the amount of lines to display per page
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* ({@link #setFrozen(boolean)})
*/
public void setWindowSize(int windowSize) throws CmsRuntimeException {
checkFrozen();
m_windowSize = windowSize;
}
/**
* Internal helper that throws a {@link CmsRuntimeException}
if the
* configuration of this instance has been frozen ({@link #setFrozen(boolean)}).
*
* @throws CmsRuntimeException if the configuration of this instance has been frozen
* ({@link #setFrozen(boolean)})
*/
private void checkFrozen() throws CmsRuntimeException {
if (m_frozen) {
throw new CmsRuntimeException(Messages.get().container(Messages.ERR_FILE_VIEW_SETTINGS_FROZEN_0));
}
}
/**
* Reads the log folder RFS path.
*
* @return the log folder path
*/
private String getLogFolderPath() {
String path = null;
if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
path = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent();
// making sure the path ends with the file separator CHAR
if ((path != null) && !path.endsWith(File.separator)) {
path += File.separator;
}
}
return path;
}
/**
* Check if the given path is below any of the configured roots.
*
* @param canonicalPath the path to check
* @return true if the path is below any of the configured roots
*/
private boolean isInRoots(String canonicalPath) {
if (canonicalPath.startsWith(m_rootPath)) {
return true;
}
if (m_additionalRoots != null) {
for (String root : m_additionalRoots) {
if (canonicalPath.startsWith(root)) {
return true;
}
}
}
return false;
}
/**
* Internally sets the member m_windowPos
to the last available
* window of m_windowSize
windows to let further calls to
* {@link #readFilePortion()}
display the end of the file.
*
* This method is triggered when a new file is chosen
* ({@link #setFilePath(String)}
) because the amount of lines changes.
* This method is also triggered when a different window size is chosen
* ({@link #setWindowSize(int)}
) because the amount of lines to display change.
*
* @return the amount of lines in the file to view
*/
private int scrollToFileEnd() {
int lines = 0;
if (OpenCms.getRunLevel() < OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
// no scrolling if system not yet fully initialized
} else {
LineNumberReader reader = null;
// shift the window position to the end of the file: this is expensive but OK for ocs logfiles as they
// are ltd. to 2 MB
try {
reader = new LineNumberReader(
new BufferedReader(new InputStreamReader(new FileInputStream(m_filePath))));
while (reader.readLine() != null) {
lines++;
}
reader.close();
// if 11.75 windows are available, we don't want to end on window nr. 10
int availWindows = (int)Math.ceil((double)lines / (double)m_windowSize);
// we start with window 0
m_windowPos = availWindows - 1;
} catch (IOException ioex) {
LOG.error("Unable to scroll file " + m_filePath + " to end. Ensure that it exists. ");
} finally {
if (reader != null) {
try {
reader.close();
} catch (Throwable f) {
LOG.info("Unable to close reader of file " + m_filePath, f);
}
}
}
}
return lines;
}
}