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(new BufferedReader(new InputStreamReader(
297                    new FileInputStream(m_filePath),
298                    m_fileEncoding)), 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(Messages.get().container(
332                    Messages.ERR_FILE_ARG_ACCESS_1,
333                    m_filePath), 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(Messages.get().container(
427                Messages.ERR_CHARSET_ILLEGAL_NAME_1,
428                fileEncoding));
429        } catch (UnsupportedCharsetException ucse) {
430            throw new CmsIllegalArgumentException(Messages.get().container(
431                Messages.ERR_CHARSET_UNSUPPORTED_1,
432                fileEncoding));
433
434        }
435
436    }
437
438    /**
439     * Set the path in the real file system that points to the file 
440     * that should be displayed.<p>
441     * 
442     * This method will only success if the file specified by the <code>path</code> 
443     * argument is valid within the file system, no folder and may be read by the 
444     * OpenCms process on the current platform.<p> 
445     * 
446     * @param path the path in the real file system that points to the file that should be displayed to set
447     * 
448     * @throws CmsRuntimeException if the configuration of this instance has been frozen 
449     * @throws CmsRfsException if the given path is invalid, does not point to a file or cannot be accessed
450     */
451    public void setFilePath(String path) throws CmsRfsException, CmsRuntimeException {
452
453        checkFrozen();
454
455        if (path != null) {
456            // leading whitespace from CmsComboWidget causes exception 
457            path = path.trim();
458        }
459        if (CmsStringUtil.isEmpty(path)) {
460            throw new CmsRfsException(Messages.get().container(
461                Messages.ERR_FILE_ARG_EMPTY_1,
462                new Object[] {String.valueOf(path)}));
463        }
464        try {
465            // just for validation :
466            File file = new File(path);
467            if (file.isDirectory()) {
468                // if wrong configuration perform self healing: 
469                if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
470                    // this deletes the illegal entry and will default to the log file path
471                    m_filePath = null;
472                    m_isLogfile = true;
473                } else {
474                    throw new CmsRfsException(Messages.get().container(
475                        Messages.ERR_FILE_ARG_IS_FOLDER_1,
476                        new Object[] {String.valueOf(path)}));
477                }
478            } else if (!file.isFile()) {
479                // if wrong configuration perform self healing: 
480                if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
481                    // this deletes the illegal entry and will default to the log file path
482                    m_filePath = null;
483                    m_isLogfile = true;
484                } else {
485                    throw new CmsRfsException(Messages.get().container(
486                        Messages.ERR_FILE_ARG_NOT_FOUND_1,
487                        new Object[] {String.valueOf(path)}));
488                }
489
490            } else if (!file.canRead()) {
491                // if wrong configuration perform self healing: 
492                if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
493                    // this deletes the illegal entry and will default to the log file path
494                    m_filePath = null;
495                    m_isLogfile = true;
496                } else {
497                    throw new CmsRfsException(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(Messages.get().container(
509                        Messages.ERR_FILE_ARG_NOT_READ_1,
510                        new Object[] {String.valueOf(path)}));
511                }
512            } else {
513                m_filePath = file.getCanonicalPath();
514            }
515        } catch (FileNotFoundException fnfe) {
516            // if wrong configuration perform self healing: 
517            if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
518                // this deletes the illegal entry and will default to the log file path
519                m_filePath = null;
520                m_isLogfile = true;
521            } else {
522                throw new CmsRfsException(Messages.get().container(
523                    Messages.ERR_FILE_ARG_NOT_FOUND_1,
524                    new Object[] {String.valueOf(path)}), fnfe);
525            }
526        } catch (IOException ioex) {
527            // if wrong configuration perform self healing: 
528            if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
529                // this deletes the illegal entry and will default to the log file path
530                m_filePath = null;
531                m_isLogfile = true;
532            } else {
533                throw new CmsRfsException(Messages.get().container(
534                    Messages.ERR_FILE_ARG_ACCESS_1,
535                    new Object[] {String.valueOf(path)}), ioex);
536            }
537
538        }
539    }
540
541    /**
542     * Package friendly access that allows the <code>{@link org.opencms.workplace.CmsWorkplaceManager}</code> 
543     * to "freeze" this instance within the system-wide assignment in it's 
544     * <code>{@link org.opencms.workplace.CmsWorkplaceManager#setFileViewSettings(org.opencms.file.CmsObject, CmsRfsFileViewer)}</code> method.<p>
545     * 
546     * @param frozen if true this instance will freeze and throw <code>CmsRuntimeExceptions</code> upon setter invocations  
547     * 
548     * @throws CmsRuntimeException if the configuration of this instance has been frozen 
549     *                             ({@link #setFrozen(boolean)})
550     */
551    public void setFrozen(boolean frozen) throws CmsRuntimeException {
552
553        m_frozen = frozen;
554    }
555
556    /**
557     * Set if the internal file is in standard log file format (true) or not (false).<p>  
558     * 
559     * If set to true the file might be 
560     * treated / displayed in a more convenient format than standard files in future.
561     * Currently it is only inverted (last lines appear first) and only the last 
562     * 'Window Size' lines of the file are displayed.<p>
563     * 
564     * Do not activate this (it is possible from the log file viewer settings in the workplace 
565     * administration) if your selected file is no log file: The display will confuse you and 
566     * be more expensive (imaging scrolling a 20 MB file to view the last 200 lines). <p>
567     * 
568     * @param isLogfile determines if the internal file is in standard log file format (true) or not (false)
569     * 
570     * @throws CmsRuntimeException if the configuration of this instance has been frozen 
571     *                             ({@link #setFrozen(boolean)})
572     */
573    public void setIsLogfile(boolean isLogfile) throws CmsRuntimeException {
574
575        checkFrozen();
576        m_isLogfile = isLogfile;
577    }
578
579    /**
580     * Set the path in the real file system that points to the folder/tree 
581     * containing the log files.<p>
582     * 
583     * This method will only success if the folder specified by the <code>path</code> 
584     * argument is valid within the file system.<p> 
585     * 
586     * @param path the path in the real file system that points to the folder containing the log files
587     * 
588     * @throws CmsRuntimeException if the configuration of this instance has been frozen 
589     * @throws CmsRfsException if the given path is invalid
590     */
591    public void setRootPath(String path) throws CmsRfsException, CmsRuntimeException {
592
593        checkFrozen();
594
595        if (path != null) {
596            // leading whitespace from CmsComboWidget causes exception 
597            path = path.trim();
598        }
599        if (CmsStringUtil.isEmpty(path)) {
600            throw new CmsRfsException(Messages.get().container(
601                Messages.ERR_FILE_ARG_EMPTY_1,
602                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(Messages.get().container(
617                        Messages.ERR_FILE_ARG_NOT_FOUND_1,
618                        new Object[] {String.valueOf(path)}));
619                }
620            }
621        } catch (IOException ioex) {
622            // if wrong configuration perform self healing: 
623            if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_2_INITIALIZING) {
624                // this deletes the illegal entry and will default to the log file path
625                m_rootPath = new File(OpenCms.getSystemInfo().getLogFileRfsPath()).getParent();
626            } else {
627
628                throw new CmsRfsException(Messages.get().container(
629                    Messages.ERR_FILE_ARG_ACCESS_1,
630                    new Object[] {String.valueOf(path)}), ioex);
631            }
632        }
633    }
634
635    /**
636     * Sets the start position of the current display.<p>
637     * 
638     * This is a count of "windows" that 
639     * consist of viewable text with "windowSize" lines of text (for a non-standard log file) or 
640     * log-entries (for a standard log file).<p>
641     * 
642     * @param windowPos the start position of the current display to set 
643     * 
644     * @throws CmsRuntimeException if the configuration of this instance has been frozen 
645     *                             ({@link #setFrozen(boolean)})
646     */
647    public void setWindowPos(int windowPos) throws CmsRuntimeException {
648
649        checkFrozen();
650        m_windowPos = windowPos;
651    }
652
653    /**
654     * Set the amount of lines (or entries depending on whether a standard log file is shown) 
655     * to display per page.<p>
656     * 
657     * @param windowSize the amount of lines to display per page 
658     * 
659     * @throws CmsRuntimeException if the configuration of this instance has been frozen 
660     *                             ({@link #setFrozen(boolean)})
661     */
662    public void setWindowSize(int windowSize) throws CmsRuntimeException {
663
664        checkFrozen();
665        m_windowSize = windowSize;
666    }
667}