org.meshcms.core.WebSite.java Source code

Java tutorial

Introduction

Here is the source code for org.meshcms.core.WebSite.java

Source

/*
 * Copyright 2004-2009 Luciano Vernaschi
 *
 * This file is part of MeshCMS.
 *
 * MeshCMS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MeshCMS 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MeshCMS.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.meshcms.core;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.StreamException;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Date;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload.FileItem;
import org.meshcms.util.DirectoryRemover;
import org.meshcms.util.Path;
import org.meshcms.util.Utils;
import org.meshcms.util.XStreamPathConverter;
import org.meshcms.util.ZipArchiver;

public class WebSite {
    public static final String APP_NAME = "MeshCMS";
    public static final String VERSION_ID = "3.6";

    /**
     * A prefix to be used for every backup file.
     */
    public static final String BACKUP_PREFIX = "_bak_";

    /**
     * A prefix to be used for every backup dir.
     */
    public static final String BACKUP_DIR_PREFIX = "_dirbak_";

    /**
     * A prefix to be used for every temporary file created in the repository.
     */
    public static final String TEMP_PREFIX = "_tmp_";

    /**
     * Name of the default admin theme folder.
     */
    public static final String ADMIN_THEME = "theme";

    public static final String CMS_ID_FILE = "meshcms_id";

    public static final String ADMIN_ID_FILE = "meshcms_admin_id";

    protected ServletContext sc;
    protected String[] welcomeFiles;

    protected File rootFile;
    protected long lastAdminThemeBlock;
    protected long statsZero;
    protected int statsLength;
    protected Configuration configuration;
    protected SiteInfo siteInfo;
    protected SiteMap siteMap;
    protected Path rootPath;

    protected Path cmsPath;
    protected Path adminPath;
    protected Path adminThemePath;
    protected Path adminModulesPath;
    protected Path adminThemesPath;
    protected Path adminScriptsPath;
    protected Path privatePath;
    protected Path usersPath;
    protected Path virtualSitesPath;
    protected Path repositoryPath;
    protected Path customModulesPath;
    protected Path generatedFilesPath;
    protected Path moduleDataPath;
    protected Path customThemesPath;

    protected Path configFilePath;
    protected Path propertiesFilePath;
    protected Path sitesFilePath;

    protected WebSite() {
    }

    protected static WebSite create(ServletContext sc, String[] welcomeFiles, File rootFile, Path rootPath,
            Path cmsPath) {
        WebSite webSite = new WebSite();
        webSite.init(sc, welcomeFiles, rootFile, rootPath, cmsPath);
        return webSite;
    }

    protected void init(ServletContext sc, String[] welcomeFiles, File rootFile, Path rootPath, Path cmsPath) {
        this.sc = sc;
        this.welcomeFiles = welcomeFiles;
        this.rootFile = rootFile;
        this.rootPath = rootPath;
        this.cmsPath = cmsPath;

        // PLEASE NOTE: the initialization order is important.
        if (cmsPath != null) {
            adminPath = cmsPath.add("admin");
            adminThemePath = adminPath.add(ADMIN_THEME);
            adminModulesPath = adminPath.add("modules");
            adminThemesPath = adminPath.add("themes");
            adminScriptsPath = adminPath.add("scripts");
            privatePath = cmsPath.add("private");
            createDir(usersPath = privatePath.add("users"));
            virtualSitesPath = cmsPath.add("sites");
            createDir(repositoryPath = privatePath.add("repository"));
            createDir(customModulesPath = cmsPath.add("modules"));
            createDir(generatedFilesPath = cmsPath.add("generated"));
            createDir(moduleDataPath = cmsPath.add("moduledata"));
            createDir(customThemesPath = cmsPath.add("themes"));
            configFilePath = privatePath.add("configuration.xml");
            propertiesFilePath = privatePath.add("siteinfo.xml");
            sitesFilePath = privatePath.add("sites.xml");

            readConfig();
            statsLength = configuration.getStatsLength();
            updateSiteMap(true);
        }
    }

    public void readConfig() {
        configuration = Configuration.load(this);
        siteInfo = SiteInfo.load(this);
    }

    protected ServletContext getServletContext() {
        return sc;
    }

    /**
     * Creates another instance of <code>SiteMap</code>. If <code>force</code>
     * is true, a new site map is always created and the method
     * returns after the new site map is completed. If it is false, a new site map
     * is created only if the current one is too old. In this case, the site map
     * is created asynchronously and the method returns immediately. The
     * repository will be cleaned too.
     *
     * @param force it to force the SiteMap creation.
     */
    public void updateSiteMap(boolean force) {
        if (force) {
            new SiteMap(this).process();
        } else if (System.currentTimeMillis() - siteMap.getLastModified() > configuration
                .getUpdateIntervalMillis()) {
            new SiteMap(this).start();
            new DirectoryCleaner(getFile(repositoryPath), configuration.getBackupLifeMillis()).start();
            new DirectoryCleaner(getFile(generatedFilesPath), configuration.getBackupLifeMillis()).start();
            new DirectoryCleaner(getFile(moduleDataPath)).start();
        }
    }

    void setSiteMap(SiteMap siteMap) {
        this.siteMap = siteMap;
    }

    /**
     * Returns the instance of the <code>SiteMap</code> that is currently manage
     * the site map. Since this object can be replaced with a new one at any
     * moment, a class that wants to use it should store it in a local variable
     * and use it for all the operation/method.
     *
     * @return the current instance of SiteMap
     */
    public SiteMap getSiteMap() {
        return siteMap;
    }

    /**
     * Returns the current configuration of the web application.
     */
    public Configuration getConfiguration() {
        return configuration;
    }

    /**
     * Sets the <code>SiteInfo</code> object related to this website.
     *
     * @param siteInfo the site info for this website
     */
    void setSiteInfo(SiteInfo siteInfo) {
        this.siteInfo = siteInfo;
    }

    /**
     * Returns the instance of the <code>SiteInfo</code> class that is managing
     * the site information.
     *
     * @see SiteInfo
     */
    public SiteInfo getSiteInfo() {
        return siteInfo;
    }

    /**
     * Returns the index of the current day in the array of stats included in any
     * PageInfo instance.
     */
    public int getStatsIndex() {
        long now = System.currentTimeMillis();
        long days = (now - statsZero) / Configuration.LENGTH_OF_DAY;

        if (days >= statsLength) {
            statsZero = now;
            return 0;
        } else {
            return (int) days;
        }
    }

    /**
     * Returns the length of stats (hit counts) measured in days.
     */
    public int getStatsLength() {
        return statsLength;
    }

    /**
     * Creates a new directory.
     *
     * @param user the user that requests the creation of the directory
     * @param dirPath the path of the new directory
     *
     * @return true if the directory has been created or already
     * existed, false otherwise
     */
    public boolean createDirectory(UserInfo user, Path dirPath) {
        File newDir = getFile(dirPath);

        if (newDir.isDirectory()) {
            return true;
        }

        return user != null && user.canWrite(this, dirPath) && newDir.mkdir();
    }

    /**
     * Creates a new file. If the extension of the file denotes a web page, the
     * basic template is copied into the file, otherwise an empty file is created.
     *
     * @param user the user that requests the file creation
     * @param filePath the path of the new file
     *
     * @return true if the new file has been created or already existed,
     * false otherwise
     */
    public boolean createFile(UserInfo user, Path filePath) {
        if (user == null || !user.canWrite(this, filePath)) {
            return false;
        }

        File newFile = getFile(filePath);

        if (newFile.exists()) {
            return !newFile.isDirectory();
        }

        try {
            if (FileTypes.isPage(filePath.getLastElement())) {
                if (newFile.exists()) {
                    return false;
                }

                return saveToFile(user, getHTMLTemplate(null), filePath);
            } else {
                return newFile.createNewFile();
            }
        } catch (IOException ex) {
            sc.log("Can't create file " + newFile, ex);
        }

        return false;
    }

    /**
     * Copies a file to another file in the same directory. An existing file won't
     * be overwritten.
     *
     * @param user the user that requests the operation
     * @param filePath the path of the old file
     * @param newName the name of the new file
     *
     * @return true if the new file has been copied, false otherwise
     */
    public boolean copyFile(UserInfo user, Path filePath, String newName) {
        return copyFile(user, filePath, filePath.getParent().add(newName));
    }

    /**
     * Copies a file (or directory) to another file (or directory).
     * Existing files won't be overwritten.
     *
     * @param user the user that requests the operation
     * @param oldPath the location of the existing file
     * @param newPath the location of the new copy of the file
     *
     * @return true if the new file has been copied, false otherwise
     */
    public boolean copyFile(UserInfo user, Path oldPath, Path newPath) {
        File oldFile = getFile(oldPath);

        if (!oldFile.exists()) {
            return false;
        }

        File newFile = getFile(newPath);

        if (user == null || !user.canWrite(this, newPath)) {
            return false;
        }

        if (oldFile.isDirectory()) {
            return Utils.copyDirectory(oldFile, newFile, false, false, false);
        } else {
            try {
                return Utils.copyFile(oldFile, newFile, false, false);
            } catch (IOException ex) {
                sc.log("Can't copy file " + oldFile + " to file " + newFile, ex);
            }
        }

        return false;
    }

    /**
     * Renames a file.
     *
     * @param user the user that requests the operation
     * @param filePath the path of the file
     * @param newName the name of the new file
     *
     * @return true if the new file has been renamed, false otherwise
     */
    public boolean rename(UserInfo user, Path filePath, String newName) {
        return move(user, filePath, filePath.getParent().add(newName));
    }

    /**
     * Moves (or renames) a file.
     *
     * @param user the user that requests the operation
     * @param oldPath the current location of the file
     * @param newPath the new location of the file
     *
     * @return true if the new file has been moved, false otherwise
     */
    public boolean move(UserInfo user, Path oldPath, Path newPath) {
        File oldFile = getFile(oldPath);

        if (!oldFile.exists()) {
            return false;
        }

        if (newPath.isContainedIn(oldPath)) {
            return false;
        }

        if (user == null || !(user.canWrite(this, oldPath) && user.canWrite(this, newPath))) {
            return false;
        }

        File newFile = getFile(newPath);

        if (Utils.forceRenameTo(oldFile, newFile, false)) {
            return true;
        } else {
            sc.log("Can't move file " + oldFile + " to file " + newFile);
        }

        return false;
    }

    /**
     * Deletes a file or directory.
     *
     * @param user the user that requests the operation
     * @param filePath the path of the file
     * @param deleteNonEmptyDirectories if true, non-empty directories will be
     * deleted too
     *
     * @return true if the file has been deleted, false otherwise
     */
    public boolean delete(UserInfo user, Path filePath, boolean deleteNonEmptyDirectories) {
        if (user == null || !user.canWrite(this, filePath)) {
            return false;
        }

        // avoid deletion of mandatory CMS items
        if (cmsPath.isContainedIn(filePath) || filePath.isChildOf(cmsPath) || filePath.isContainedIn(adminPath)
                || filePath.isChildOf(privatePath)) {
            return false;
        }

        File file = getFile(filePath);

        if (!file.exists()) {
            return false;
        }

        if (file.isDirectory()) {
            /* First try to delete a directory as if it were empty. If not, and if
               deletion of non-empty directories is allowed, backup and delete with backupDir */
            return file.delete() || (deleteNonEmptyDirectories && backupDir(user, filePath));
        } else {
            return backupFile(user, filePath);
        }
    }

    /**
     * Sets the last modified date of the file to the current time.
     *
     * @param user the user that requests the operation
     * @param filePath the path of the file
     *
     * @return true if the date has been changed, false otherwise
     */
    public boolean touch(UserInfo user, Path filePath) {
        return setFileTime(user, filePath, System.currentTimeMillis());
    }

    public boolean setFileTime(UserInfo user, Path filePath, long time) {
        if (user == null || !user.canWrite(this, filePath)) {
            return false;
        }

        File file = getFile(filePath);

        if (!file.exists()) {
            return false;
        }

        file.setLastModified(time);
        return true;
    }

    /**
     * Stores an object into a file. Supported objects are:
     *
     * <ul>
     *  <li>byte arrays</li>
     *  <li>input streams</li>
     *  <li><code>org.apache.commons.fileupload.FileItem</code>
     *      (uploaded files)</li>
     *  <li>generic objects. The <code>toString()</code> method is used in this
     *      cases. This is compatible with many kinds of objects: strings,
     *      string buffers and so on.</li>
     * </ul>
     *
     * @param user the user that requests the operation
     * @param saveThis the object to be stored in the file
     * @param filePath the path of the file to be written. If the file exists, it
     * will be backed up and overwritten
     *
     * @return true if the operation has been completed successfully,
     * false otherwise
     */
    public boolean saveToFile(UserInfo user, Object saveThis, Path filePath) {
        if (user == null || !user.canWrite(this, filePath)) {
            return false;
        }

        File file = getFile(filePath);
        File dir = file.getParentFile();
        dir.mkdirs();
        File tempFile = null;

        String fileName = file.getName();
        int dot = fileName.lastIndexOf('.');
        String fileExt = (dot == -1) ? ".bak" : fileName.substring(dot);

        if (file.exists()) {
            tempFile = getRepositoryFile(filePath, TEMP_PREFIX + System.currentTimeMillis() + fileExt);
        }

        File writeTo = tempFile == null ? file : tempFile;

        if (saveThis instanceof byte[]) {
            byte[] b = (byte[]) saveThis;
            FileOutputStream fos = null;

            try {
                fos = new FileOutputStream(writeTo);
                fos.write(b);
            } catch (IOException ex) {
                sc.log("Can't write byte array to file " + writeTo, ex);
                return false;
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException ex) {
                        sc.log("Can't close file " + writeTo, ex);
                    }
                }
            }
        } else if (saveThis instanceof InputStream) {
            try {
                InputStream is = (InputStream) saveThis;
                Utils.copyStream(is, new FileOutputStream(writeTo), true);
                is.close();
            } catch (Exception ex) {
                sc.log("Can't write stream to file " + writeTo, ex);
                return false;
            }
        } else if (saveThis instanceof FileItem) {
            try {
                ((FileItem) saveThis).write(writeTo);
            } catch (Exception ex) {
                sc.log("Can't write uploaded file to file " + writeTo, ex);
                return false;
            }
        } else {
            Writer writer = null;

            try {
                writer = new BufferedWriter(
                        new OutputStreamWriter(new FileOutputStream(writeTo), Utils.SYSTEM_CHARSET));
                writer.write(saveThis.toString());
            } catch (IOException ex) {
                sc.log("Can't write generic object to file " + writeTo, ex);
                return false;
            } finally {
                if (writer != null) {
                    try {
                        writer.close();
                    } catch (IOException ex) {
                        sc.log("Can't close file " + writeTo, ex);
                    }
                }
            }
        }

        if (tempFile != null) {
            if (!backupFile(user, filePath)) {
                return false;
            }

            if (!Utils.forceRenameTo(tempFile, file, true)) {
                sc.log("Can't rename temporary file " + tempFile + " to file " + file);
                return false;
            }
        }

        return true;
    }

    /**
     * Returns the correct file in the repository. For example, if you need to
     * create a temporary copy of /somedir/index.html, you could use
     * <code>filePath = /somedir/index.html</code> and
     * <code>fileName = tmp.html</code>.
     *
     * @param filePath the path where to search
     * @param fileName the file name
     *
     * @return the searched file
     */
    public File getRepositoryFile(Path filePath, String fileName) {
        File repoDir = getFile(repositoryPath.add(filePath));
        repoDir.mkdirs();
        return new File(repoDir, fileName);
    }

    private boolean backupFile(UserInfo user, Path filePath) {
        /* permissions already checked in methods that call this one
        if (user == null || !user.canWrite(this, filePath)) {
          return false;
        }
        */

        File file = getFile(filePath);
        String fileName = file.getName();
        int dot = fileName.lastIndexOf('.');
        String fileExt = (dot == -1) ? ".bak" : fileName.substring(dot);

        File bakFile = getRepositoryFile(filePath, BACKUP_PREFIX + user.getUsername() + "_"
                + WebUtils.numericDateFormatter.format(new Date()) + fileExt);

        if (Utils.forceRenameTo(file, bakFile, true)) {
            return true;
        }

        sc.log("Can't backup path " + filePath);
        return false;
    }

    private boolean backupDir(UserInfo user, Path dirPath) {
        /* permissions to be checked by the caller */

        File dir = getFile(dirPath);
        File bakFile = getRepositoryFile(dirPath, BACKUP_DIR_PREFIX + user.getUsername() + "_"
                + WebUtils.numericDateFormatter.format(new Date()) + ".zip");
        OutputStream os;

        try {
            os = new BufferedOutputStream(new FileOutputStream(bakFile));
        } catch (FileNotFoundException ex) {
            sc.log("can't create stream on " + bakFile, ex);
            return false;
        }

        new ZipArchiver(dir, os).process();

        try {
            os.close();
        } catch (IOException ex) {
            sc.log("Can't close file " + bakFile, ex);
            return false;
        }

        DirectoryRemover dr = new DirectoryRemover(dir);
        dr.process();
        return dr.getResult();
    }

    public File createDir(Path path) {
        File dir = getFile(path);
        dir.mkdirs();
        return dir.isDirectory() ? dir : null;
    }

    /**
     * Returns the file object for a given path in the web application. The file
     * is not checked for existance.
     *
     * @param path the path representation of the file
     *
     * @return the file object for this path, or null if it's not found
     */
    public File getFile(Path path) {
        return (path == null || path.isRelative()) ? null : new File(rootFile, path.toString());
    }

    public File getRootFile() {
        return rootFile;
    }

    /**
     * Returns the site root path.
     */
    public Path getRootPath() {
        return rootPath;
    }

    /**
     * Returns the <code>Path</code> of a file in the context.
     *
     * @see #getFile
     */
    public Path getPath(File file) {
        return new Path(Utils.getRelativePath(rootFile, file, "/"));
    }

    public Path getRequestedPath(HttpServletRequest request) {
        return new Path(request.getServletPath());
    }

    public Path getServedPath(HttpServletRequest request) {
        return new Path(request.getServletPath());
    }

    public Path getServedPath(Path requestedPath) {
        return requestedPath;
    }

    /**
     * Checks if the given path is a directory in the file system.
     *
     * @param path the Path to check
     *
     * @return true if the path is a directory.
     */
    public boolean isDirectory(Path path) {
        return getFile(path).isDirectory();
    }

    /**
     * Returns the directory that contains the given path. This is different from
     * {@link org.meshcms.util.Path#getParent}, since if the path is known to
     * be a directory in the web application, the path itself is returned.
     *
     * @param path the Path to check
     *
     * @return a directory(that contains the given path) as a Path object.
     */
    public Path getDirectory(Path path) {
        // PageInfo pageInfo = getPageInfo(path);
        // return pageInfo.isDirectory() ? path : pageInfo.getPath().getParent();
        if (path == null) {
            return null;
        }

        if (getFile(path).isDirectory()) {
            return path;
        }

        path = path.getParent();

        if (!path.isRelative() && getFile(path).isDirectory()) {
            return path;
        }

        return null;
    }

    public String[] getLinkList(PageInfo[] pages, Path pagePath, String target, String style) {
        if (pages == null) {
            return null;
        }

        String[] links = new String[pages.length];
        target = Utils.isNullOrEmpty(target) ? "" : " target=\"" + target + "\"";
        style = Utils.isNullOrEmpty(style) ? "" : " class=\"" + style + "\"";

        for (int i = 0; i < pages.length; i++) {
            if (pages[i] == null) {
                links[i] = "...";
            } else {
                links[i] = "<a href=\"" + getLink(pages[i], pagePath) + "\"" + target + style + ">"
                        + siteInfo.getPageTitle(pages[i]) + "</a>";
            }
        }

        return links;
    }

    public String getAbsoluteLink(Path path) {
        return getFile(path).isDirectory() ? path.getAsLink() + '/' : path.getAsLink();
    }

    public String getAbsoluteLink(PageInfo pageInfo) {
        return getAbsoluteLink(pageInfo.getPath());
    }

    public Path getLink(Path path, Path pagePath) {
        Path link = path.getRelativeTo(getDirectory(pagePath));

        if (link.getElementCount() == 0 && isDirectory(path)) {
            Path welcome = getSiteMap().getCurrentWelcome(path);

            if (welcome != null) {
                link = welcome.getRelativeTo(getDirectory(pagePath));
            }
        }

        return link;
    }

    public Path getLink(PageInfo pageInfo, Path pagePath) {
        return getLink(pageInfo.getPath(), pagePath);
    }

    /**
     * Returns an array of menu titles for the given pages.
     * {@link SiteInfo} is used to get the titles.
     *
     * @param pages an array of pages to be processed
     *
     * @return the array of titles for the given pages.
     */
    public String[] getTitles(PageInfo[] pages) {
        if (pages == null) {
            return null;
        }

        String[] titles = new String[pages.length];

        for (int i = 0; i < pages.length; i++) {
            titles[i] = siteInfo.getPageTitle(pages[i]);
        }

        return titles;
    }

    /**
     * Determines if the given path is a system directory, or is contained in a
     * system directory. System directories are:
     *
     * <ul>
     *  <li>the WEB-INF directory (/WEB-INF)</li>
     *  <li>the META-INF directory (/META-INF)</li>
     *  <li>the standard CGI-BIN directory (/cgi-bin)</li>
     *  <li>the MeshCMS admin directory (if <code>checkAdmin</code> is true</li>
     * </ul>
     *
     * @param path the given path(directory) to be checked
     *
     * @return true if it is a system directory, false otherwise
     */
    public boolean isSystem(Path path) {
        if (path == null || path.isRoot()) {
            return false;
        }

        if (path.isRelative()) {
            return true;
        }

        if (adminPath != null && (path.isContainedIn(adminPath) || path.isContainedIn(privatePath)
                || path.equals(cmsPath.add(CMS_ID_FILE)))) {
            return true;
        }

        String level1 = path.getElementAt(0).toLowerCase();

        return level1.equals("web-inf") || level1.equals("meta-inf") || level1.equals("cgi-bin");
    }

    /**
     * Returns true if the extension of the path is known to denote a type of
     * file that can be edited using the wysiwyg editor.
     */
    public boolean isVisuallyEditable(Path path) {
        return Utils.searchString(configuration.getVisualExtensions(), Utils.getExtension(path, false), true) != -1;
    }

    /**
     * Returns the path of the module file with the given name.
     */
    public Path getModulePath(String moduleName) {
        if (moduleName.endsWith(".jsp")) {
            // old module names were in the form module_name.jsp
            moduleName = moduleName.substring(0, moduleName.length() - 4);
        }

        return (Path) siteMap.getModulesMap().get(moduleName);
    }

    /**
     * Returns the path of the admin directory.
     */
    public Path getAdminPath() {
        return adminPath;
    }

    public Path getCMSPath() {
        return cmsPath;
    }

    public boolean isVirtual() {
        return false;
    }

    /**
     * Returns the complete tag used by pages in the admin folder. This way those
     * pages can set to be themed according to the site preferences (i.e. using
     * a custom theme or the default admin theme).
     */
    public String getAdminMetaThemeTag() {
        Path themePath = getThemePath(adminPath);
        return "<meta name=\"decorator\" content=\"/" + getServedPath(themePath) + "/" + SiteMap.THEME_DECORATOR
                + "\" />";
    }

    public String getDummyMetaThemeTag() {
        return "<meta name=\"decorator\" content=\"/" + adminPath + "/theme/dummy.jsp\" />";
    }

    /**
     * Returns a string containing a basic HTML page.
     *
     * @param pageTitle the content of the &lt;title&gt; tag (if null, the title
     * will be &quot;New Page&quot;)
     *
     * @return a basic "empty" HTML page
     */
    public String getHTMLTemplate(String pageTitle) throws IOException {
        String text = Utils.readFully(getFile(getAdminPath().add("template.html")));

        if (pageTitle != null) {
            int idx = text.indexOf("New Page");

            if (idx >= 0) {
                text = text.substring(0, idx) + pageTitle + text.substring(idx + 8);
            }
        }

        return text;
    }

    public boolean isInsideModules(Path path) {
        return path.isContainedIn(customModulesPath) || path.isContainedIn(adminModulesPath);
    }

    public boolean isInsideThemes(Path path) {
        return path.isContainedIn(customThemesPath) || path.isContainedIn(adminThemesPath);
    }

    /**
     * Logs a string by calling <code>ServletContext.log(s)</code>
     */
    protected void log(String s) {
        sc.log(s);
    }

    /**
     * Logs an exception by calling
     * <code>ServletContext.log(message, throwable)</code>
     */
    public void log(String message, Throwable throwable) {
        sc.log(message, throwable);
    }

    /**
     * Checks if the file name is one of the welcome files.
     *
     * @param fileName the file name to check
     * @return true if the given file name is known to be a welcome file name.
     */
    public boolean isWelcomeFileName(String fileName) {
        return Utils.searchString(welcomeFiles, fileName, false) != -1;
    }

    /**
     * Returns the array of welcome file names.
     *
     * @return an array of welcome file names for the current web application.
     * Values are fetched from the web.xml file.
     */
    public String[] getWelcomeFileNames() {
        return welcomeFiles;
    }

    /**
     * Returns the current welcome file path for the given folder. If there is no
     * welcome file in that folder, this method returns null.
     *
     * @param dirPath the folder where to search the welcome file
     *
     * @return the welcome file as a Path object of null if not found.
     */
    public Path findCurrentWelcome(Path dirPath) {
        File dirFile = getFile(dirPath);

        if (dirFile.isDirectory()) {
            for (int i = 0; i < welcomeFiles.length; i++) {
                Path wPath = dirPath.add(welcomeFiles[i]);

                if (getFile(wPath).exists()) {
                    return wPath;
                }
            }
        }

        return null;
    }

    public WebSite getWebSite(ServletRequest request) {
        return this;
    }

    public HttpServletRequest wrapRequest(ServletRequest request) {
        return (HttpServletRequest) request;
    }

    public String getTypeDescription() {
        return "single web site";
    }

    public String toString() {
        return "Type: " + getTypeDescription() + "; path: /" + rootPath + "; CMS: "
                + (cmsPath == null ? "disabled" : "enabled on path /" + cmsPath);
    }

    public Object loadFromXML(Path path) {
        File file = getFile(path);

        if (file.exists()) {
            InputStream is = null;

            try {
                is = new BufferedInputStream(new FileInputStream(file));
                XStream xStream = new XStream(new DomDriver());
                XStreamPathConverter pConv = new XStreamPathConverter();
                pConv.setPrependSlash(true);
                xStream.registerConverter(pConv);

                try {
                    return xStream.fromXML(new InputStreamReader(is, "UTF-8"));
                } catch (StreamException ex) {
                    return xStream.fromXML(is);
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException ex) {
                        sc.log("Can't close file " + file, ex);
                    }
                }
            }
        }

        return null;
    }

    public boolean storeToXML(Object o, Path path) {
        File file = getFile(path);
        OutputStreamWriter osw = null;

        try {
            osw = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(file)), "UTF-8");
            XStream xStream = new XStream(new DomDriver());
            XStreamPathConverter pConv = new XStreamPathConverter();
            pConv.setPrependSlash(true);
            xStream.registerConverter(pConv);
            xStream.toXML(o, osw);
            return true;
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (osw != null) {
                try {
                    osw.close();
                } catch (IOException ex) {
                    sc.log("Can't close file " + file, ex);
                }
            }
        }

        return false;
    }

    /**
     * Returns the path of the theme to be applied to the given path. This depends
     * on the stored values and on the option to use the default theme for the
     * admin pages. This method returns null if no theme is found.
     *
     * @param pagePath the path of the page who's theme is searched.
     *
     * @return the theme of this page as a Path object
     */
    public Path getThemePath(Path pagePath) {
        Path themePath = siteInfo.getThemePath(pagePath);

        if (pagePath.isContainedIn(adminPath)) {
            if (configuration.isUseAdminTheme() || themePath == null
                    || System.currentTimeMillis() - lastAdminThemeBlock < 300000L || // 5 minutes
                    !getFile(themePath.add(SiteMap.THEME_DECORATOR)).exists()) {
                themePath = adminThemePath;
            }
        }

        return themePath;
    }

    public Path getAdminThemePath() {
        return adminThemePath;
    }

    public Path getAdminModulesPath() {
        return adminModulesPath;
    }

    public Path getModuleDataPath() {
        return moduleDataPath;
    }

    public Path getAdminThemesPath() {
        return adminThemesPath;
    }

    public Path getAdminScriptsPath() {
        return adminScriptsPath;
    }

    public Path getPrivatePath() {
        return privatePath;
    }

    public Path getUsersPath() {
        return usersPath;
    }

    public Path getVirtualSitesPath() {
        return virtualSitesPath;
    }

    public Path getRepositoryPath() {
        return repositoryPath;
    }

    public Path getCustomModulesPath() {
        return customModulesPath;
    }

    public Path getGeneratedFilesPath() {
        return generatedFilesPath;
    }

    public Path getCustomThemesPath() {
        return customThemesPath;
    }

    public Path getConfigFilePath() {
        return configFilePath;
    }

    public Path getPropertiesFilePath() {
        return propertiesFilePath;
    }

    public Path getSitesFilePath() {
        return sitesFilePath;
    }

    public long getLastAdminThemeBlock() {
        return lastAdminThemeBlock;
    }

    public void setLastAdminThemeBlock(long lastAdminThemeBlock) {
        this.lastAdminThemeBlock = lastAdminThemeBlock;
    }
}