com.rapid.core.Page.java Source code

Java tutorial

Introduction

Here is the source code for com.rapid.core.Page.java

Source

/*
    
Copyright (C) 2015 - Gareth Edwards / Rapid Information Systems
    
gareth.edwards@rapid-is.co.uk
    
    
This file is part of the Rapid Application Platform
    
Rapid is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as 
published by the Free Software Foundation, either version 3 of the 
License, or (at your option) any later version. The terms require you 
to include the original copyright, and the license notice in all redistributions.
    
This program 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 Affero General Public License for more details.
    
You should have received a copy of the GNU Affero General Public License
in a file named "COPYING".  If not, see <http://www.gnu.org/licenses/>.
    
*/

package com.rapid.core;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.rapid.actions.Logic.Condition;
import com.rapid.actions.Logic.Value;
import com.rapid.core.Application.RapidLoadingException;
import com.rapid.core.Application.Resource;
import com.rapid.core.Application.ResourceDependency;
import com.rapid.forms.FormAdapter;
import com.rapid.forms.FormAdapter.FormControlValue;
import com.rapid.forms.FormAdapter.FormPageControlValues;
import com.rapid.forms.FormAdapter.UserFormDetails;
import com.rapid.security.SecurityAdapter;
import com.rapid.security.SecurityAdapter.SecurityAdapaterException;
import com.rapid.security.SecurityAdapter.User;
import com.rapid.server.Rapid;
import com.rapid.server.RapidHttpServlet;
import com.rapid.server.RapidRequest;
import com.rapid.server.filter.RapidFilter;
import com.rapid.utils.Files;
import com.rapid.utils.Html;
import com.rapid.utils.Minify;
import com.rapid.utils.XML;

@XmlRootElement
@XmlType(namespace = "http://rapid-is.co.uk/core")
public class Page {

    // the version of this class's xml structure when marshelled (if we have any significant changes down the line we can upgrade the xml files before unmarshalling)   
    public static final int XML_VERSION = 1;

    // form page types
    public static final int FORM_PAGE_TYPE_NORMAL = 0;
    public static final int FORM_PAGE_TYPE_SUBMITTED = 1;
    public static final int FORM_PAGE_TYPE_ERROR = 2;
    public static final int FORM_PAGE_TYPE_SAVED = 3;

    // a class for retaining page html for a set of user roles   
    public static class RoleHtml {

        // instance variables

        private List<String> _roles;
        private String _html;

        // properties

        public List<String> getRoles() {
            return _roles;
        }

        public void setRoles(List<String> roles) {
            _roles = roles;
        }

        public String getHtml() {
            return _html;
        }

        public void setHtml(String html) {
            _html = html;
        }

        // constructors
        public RoleHtml() {
        }

        public RoleHtml(List<String> roles, String html) {
            _roles = roles;
            _html = html;
        }

    }

    // details of a lock that might be on this page
    public static class Lock {

        private String _userName, _userDescription;
        private Date _dateTime;

        public String getUserName() {
            return _userName;
        }

        public void setUserName(String userName) {
            _userName = userName;
        }

        public String getUserDescription() {
            return _userDescription;
        }

        public void setUserDescription(String userDescription) {
            _userDescription = userDescription;
        }

        public Date getDateTime() {
            return _dateTime;
        }

        public void setDateTime(Date dateTime) {
            _dateTime = dateTime;
        }

        // constructors

        public Lock() {
        }

        public Lock(String userName, String userDescription, Date dateTime) {
            _userName = userName;
            _userDescription = userDescription;
            _dateTime = dateTime;
        }

    }

    // instance variables

    private int _xmlVersion, _formPageType;
    private String _id, _name, _title, _label, _description, _createdBy, _modifiedBy, _htmlBody, _cachedHeadLinks,
            _cachedHeadCSS, _cachedHeadReadyJS, _cachedHeadJS;
    private boolean _simple;
    private Date _createdDate, _modifiedDate;
    private List<Control> _controls;
    private List<Event> _events;
    private List<Style> _styles;
    private List<String> _controlTypes, _actionTypes, _sessionVariables, _roles;
    private List<RoleHtml> _rolesHtml;
    private List<Condition> _visibilityConditions;
    private String _conditionsType;
    private Lock _lock;
    private List<String> _formControlValues;

    // this array is used to collect all of the lines needed in the pageload before sorting them
    private List<String> _pageloadLines;

    // properties

    // the xml version is used to upgrade xml files before unmarshalling (we use a property so it's written ito xml)
    public int getXMLVersion() {
        return _xmlVersion;
    }

    public void setXMLVersion(int xmlVersion) {
        _xmlVersion = xmlVersion;
    }

    // the id uniquely identifies the page (it is quiet short and is concatinated to control id's so more than one page's control's can be working in a document at one time)
    public String getId() {
        return _id;
    }

    public void setId(String id) {
        _id = id;
    }

    // this is expected to be short name, probably even a code that is used by users to simply identify pages (also becomes the file name)
    public String getName() {
        return _name;
    }

    public void setName(String name) {
        _name = name;
    }

    // this is a user-friendly, long title
    public String getTitle() {
        return _title;
    }

    public void setTitle(String title) {
        _title = title;
    }

    // the form page type, most will be normal but we show special pages for after submission, error, and saved
    public int getFormPageType() {
        return _formPageType;
    }

    public void setFormPageType(int formPageType) {
        _formPageType = formPageType;
    }

    // this is a the label to use in the form summary
    public String getLabel() {
        return _label;
    }

    public void setLabel(String label) {
        _label = label;
    }

    // an even longer description of what this page does
    public String getDescription() {
        return _description;
    }

    public void setDescription(String description) {
        _description = description;
    }

    // simple pages do not have any events and can be used in page panels without dynamically loading them via ajax
    public boolean getSimple() {
        return _simple;
    }

    public void setSimple(boolean simple) {
        _simple = simple;
    }

    // the user that created this page (or archived page)
    public String getCreatedBy() {
        return _createdBy;
    }

    public void setCreatedBy(String createdBy) {
        _createdBy = createdBy;
    }

    // the date this page (or archive) was created
    public Date getCreatedDate() {
        return _createdDate;
    }

    public void setCreatedDate(Date createdDate) {
        _createdDate = createdDate;
    }

    // the last user to save this application 
    public String getModifiedBy() {
        return _modifiedBy;
    }

    public void setModifiedBy(String modifiedBy) {
        _modifiedBy = modifiedBy;
    }

    // the date this application was last saved
    public Date getModifiedDate() {
        return _modifiedDate;
    }

    public void setModifiedDate(Date modifiedDate) {
        _modifiedDate = modifiedDate;
    }

    // the html for this page
    public String getHtmlBody() {
        return _htmlBody;
    }

    public void setHtmlBody(String htmlBody) {
        _htmlBody = htmlBody;
    }

    // the child controls of the page
    public List<Control> getControls() {
        return _controls;
    }

    public void setControls(List<Control> controls) {
        _controls = controls;
    }

    // the page events and actions
    public List<Event> getEvents() {
        return _events;
    }

    public void setEvents(List<Event> events) {
        _events = events;
    }

    // the page styles
    public List<Style> getStyles() {
        return _styles;
    }

    public void setStyles(List<Style> styles) {
        _styles = styles;
    }

    // session variables used by this page (navigation actions are expected to pass them in)
    public List<String> getSessionVariables() {
        return _sessionVariables;
    }

    public void setSessionVariables(List<String> sessionVariables) {
        _sessionVariables = sessionVariables;
    }

    // the roles required to view this page
    public List<String> getRoles() {
        return _roles;
    }

    public void setRoles(List<String> roles) {
        _roles = roles;
    }

    // session variables used by this page (navigation actions are expected to pass them in)
    public List<RoleHtml> getRolesHtml() {
        return _rolesHtml;
    }

    public void setRolesHtml(List<RoleHtml> rolesHtml) {
        _rolesHtml = rolesHtml;
    }

    // any lock that might be on this page
    public Lock getLock() {
        return _lock;
    }

    public void setLock(Lock lock) {
        _lock = lock;
    }

    // the page visibility rule conditions
    public List<Condition> getVisibilityConditions() {
        return _visibilityConditions;
    }

    public void setVisibilityConditions(List<Condition> visibilityConditions) {
        _visibilityConditions = visibilityConditions;
    }

    // the type (and/or) of the page visibility conditions - named so can be shared with logic action 
    public String getConditionsType() {
        return _conditionsType;
    }

    public void setConditionsType(String conditionsType) {
        _conditionsType = conditionsType;
    }

    // constructor

    public Page() {
        // set the xml version
        _xmlVersion = XML_VERSION;
    }

    // instance methods

    public String getFile(ServletContext servletContext, Application application) {
        return application.getConfigFolder(servletContext) + "/" + "/pages/" + Files.safeName(_name) + ".page.xml";
    }

    public void addControl(Control control) {
        if (_controls == null)
            _controls = new ArrayList<Control>();
        _controls.add(control);
    }

    public Control getControl(int index) {
        if (_controls == null)
            return null;
        return _controls.get(index);
    }

    // an iterative function for tree-walking child controls when searching for one
    public Control getChildControl(List<Control> controls, String controlId) {
        Control foundControl = null;
        if (controls != null) {
            for (Control control : controls) {
                if (controlId.equals(control.getId())) {
                    foundControl = control;
                    break;
                } else {
                    foundControl = getChildControl(control.getChildControls(), controlId);
                    if (foundControl != null)
                        break;
                }
            }
        }
        return foundControl;
    }

    // uses the tree walking function above to find a particular control
    public Control getControl(String id) {
        return getChildControl(_controls, id);
    }

    public void getChildControls(List<Control> controls, List<Control> childControls) {
        if (controls != null) {
            for (Control control : controls) {
                childControls.add(control);
                getChildControls(control.getChildControls(), childControls);
            }
        }
    }

    public List<Control> getAllControls() {
        ArrayList<Control> controls = new ArrayList<Control>();
        getChildControls(_controls, controls);
        return controls;
    }

    public Action getChildAction(List<Action> actions, String actionId) {
        Action foundAction = null;
        if (actions != null) {
            for (Action action : actions) {
                if (action != null) {
                    if (actionId.equals(action.getId()))
                        return action;
                    foundAction = getChildAction(action.getChildActions(), actionId);
                    if (foundAction != null)
                        return foundAction;
                }
            }
        }
        return foundAction;
    }

    // an iterative function for tree-walking child controls when searching for a specific action
    public Action getChildControlAction(List<Control> controls, String actionId) {
        Action foundAction = null;
        if (controls != null) {
            for (Control control : controls) {
                if (control.getEvents() != null) {
                    for (Event event : control.getEvents()) {
                        if (event.getActions() != null) {
                            foundAction = getChildAction(event.getActions(), actionId);
                            if (foundAction != null)
                                return foundAction;
                        }
                    }
                }
                foundAction = getChildControlAction(control.getChildControls(), actionId);
                if (foundAction != null)
                    break;
            }
        }
        return foundAction;
    }

    // find an action in the page by its id
    public Action getAction(String id) {
        // check the page actions first
        if (_events != null) {
            for (Event event : _events) {
                if (event.getActions() != null) {
                    Action action = getChildAction(event.getActions(), id);
                    if (action != null)
                        return action;
                }
            }
        }
        // uses the tree walking function above to the find a particular action
        return getChildControlAction(_controls, id);
    }

    // recursively append to a list of actions from an action and it's children
    public void getChildActions(List<Action> actions, Action action) {
        // add this one action
        actions.add(action);
        // check there are child actions
        if (action.getChildActions() != null) {
            // loop them
            for (Action childAction : action.getChildActions()) {
                // add their actions too
                getChildActions(actions, childAction);
            }
        }
    }

    // recursively append to a list of actions from a control and it's children
    public void getChildActions(List<Action> actions, Control control) {

        // check this control has events
        if (control.getEvents() != null) {
            for (Event event : control.getEvents()) {
                // add any actions to the list
                if (event.getActions() != null) {
                    // loop the actions
                    for (Action action : event.getActions()) {
                        // add any child actions too
                        getChildActions(actions, action);
                    }
                }
            }
        }
        // check if we have any child controls
        if (control.getChildControls() != null) {
            // loop the child controls
            for (Control childControl : control.getChildControls()) {
                // add their actions too
                getChildActions(actions, childControl);
            }
        }
    }

    // get all actions in the page
    public List<Action> getAllActions() {
        // instantiate the list we're going to return
        List<Action> actions = new ArrayList<Action>();
        // check the page events first
        if (_events != null) {
            for (Event event : _events) {
                // add all event actions if not null
                if (event.getActions() != null)
                    actions.addAll(event.getActions());
            }
        }
        // uses the tree walking function above to add all actions
        if (_controls != null) {
            for (Control control : _controls) {
                getChildActions(actions, control);
            }
        }
        // sort them by action id
        Collections.sort(actions, new Comparator<Action>() {
            @Override
            public int compare(Action obj1, Action obj2) {
                if (obj1 == null)
                    return -1;
                if (obj2 == null)
                    return 1;
                if (obj1.equals(obj2))
                    return 0;
                String id1 = obj1.getId();
                String id2 = obj2.getId();
                if (id1 == null)
                    return -1;
                if (id2 == null)
                    return -1;
                id1 = id1.replace("_", "");
                id2 = id2.replace("_", "");
                int pos = id1.indexOf("A");
                if (pos < 0)
                    return -1;
                id1 = id1.substring(pos + 1);
                pos = id2.indexOf("A");
                if (pos < 0)
                    return 1;
                id2 = id2.substring(pos + 1);
                return (Integer.parseInt(id1) - Integer.parseInt(id2));
            }
        });
        return actions;
    }

    // an iterative function for tree-walking child controls when searching for a specific action's control
    public Control getChildControlActionControl(List<Control> controls, String actionId) {
        Control foundControl = null;
        if (controls != null) {
            for (Control control : controls) {
                if (control.getEvents() != null) {
                    for (Event event : control.getEvents()) {
                        if (event.getActions() != null) {
                            for (Action action : event.getActions()) {
                                if (actionId.equals(action.getId()))
                                    return control;
                            }
                        }
                    }
                }
                foundControl = getChildControlActionControl(control.getChildControls(), actionId);
                if (foundControl != null)
                    break;
            }
        }
        return foundControl;
    }

    // find an action in the page by its id
    public Control getActionControl(String actionId) {
        // uses the tree walking function above to the find a particular action
        return getChildControlActionControl(_controls, actionId);
    }

    // an iterative function for tree-walking child controls when searching for a specific action's control
    public Event getChildControlActionEvent(List<Control> controls, String actionId) {
        Event foundEvent = null;
        if (controls != null) {
            for (Control control : controls) {
                if (control.getEvents() != null) {
                    for (Event event : control.getEvents()) {
                        if (event.getActions() != null) {
                            for (Action action : event.getActions()) {
                                if (actionId.equals(action.getId()))
                                    return event;
                            }
                        }
                    }
                }
                foundEvent = getChildControlActionEvent(control.getChildControls(), actionId);
                if (foundEvent != null)
                    break;
            }
        }
        return foundEvent;
    }

    // find an action in the page by its id
    public Event getActionEvent(String actionId) {
        // check the page actions first
        if (_events != null) {
            for (Event event : _events) {
                if (event.getActions() != null) {
                    for (Action action : event.getActions()) {
                        if (actionId.equals(action.getId()))
                            return event;
                    }
                }
            }
        }
        // uses the tree walking function above to the find a particular action
        return getChildControlActionEvent(_controls, actionId);
    }

    // iterative function for building a flat JSONArray of controls that can be used on other pages
    private void getOtherPageChildControls(RapidHttpServlet rapidServlet, JSONArray jsonControls,
            List<Control> controls, boolean includePageVisibiltyControls) throws JSONException {
        // check we were given some controls
        if (controls != null) {
            // loop the controls
            for (Control control : controls) {
                // get if this control can be used from other pages
                boolean canBeUsedFromOtherPages = control.getCanBeUsedFromOtherPages();
                boolean canBeUsedForFormPageVisibilty = control.getCanBeUsedForFormPageVisibilty()
                        && includePageVisibiltyControls;
                // if this control can be used from other pages
                if (canBeUsedFromOtherPages || canBeUsedForFormPageVisibilty) {

                    // get the control details
                    JSONObject jsonControlClass = rapidServlet.getJsonControl(control.getType());

                    // check we got one
                    if (jsonControlClass != null) {

                        // get the name - no need to include if we don't have one
                        String controlName = control.getName();

                        if (controlName != null) {

                            // make a JSON object with what we need about this control
                            JSONObject jsonControl = new JSONObject();
                            jsonControl.put("id", control.getId());
                            jsonControl.put("type", control.getType());
                            jsonControl.put("name", controlName);
                            if (jsonControlClass.optString("getDataFunction", null) != null)
                                jsonControl.put("input", true);
                            if (jsonControlClass.optString("setDataJavaScript", null) != null)
                                jsonControl.put("output", true);
                            if (canBeUsedFromOtherPages)
                                jsonControl.put("otherPages", true);
                            if (canBeUsedForFormPageVisibilty)
                                jsonControl.put("pageVisibility", true);

                            // look for any runtimeProperties
                            JSONObject jsonProperty = jsonControlClass.optJSONObject("runtimeProperties");
                            // if we got some
                            if (jsonProperty != null) {
                                // create an array to hold the properties
                                JSONArray jsonRunTimeProperties = new JSONArray();
                                // look for an array too
                                JSONArray jsonProperties = jsonProperty.optJSONArray("runtimeProperty");
                                // assume
                                int index = 0;
                                int count = 0;
                                // if an array 
                                if (jsonProperties != null) {
                                    // get the first item
                                    jsonProperty = jsonProperties.getJSONObject(index);
                                    // set the count
                                    count = jsonProperties.length();
                                }
                                // look for a single object
                                JSONObject jsonPropertySingle = jsonProperty.optJSONObject("runtimeProperty");
                                // assume this one if not null
                                if (jsonPropertySingle != null)
                                    jsonProperty = jsonPropertySingle;

                                // do once and loop until no more left 
                                do {

                                    // create a json object for this runtime property
                                    JSONObject jsonRuntimeProperty = new JSONObject();
                                    jsonRuntimeProperty.put("type", jsonProperty.get("type"));
                                    jsonRuntimeProperty.put("name", jsonProperty.get("name"));
                                    if (jsonProperty.optString("getPropertyFunction", null) != null)
                                        jsonRuntimeProperty.put("input", true);
                                    if (jsonProperty.optString("setPropertyJavaScript", null) != null)
                                        jsonRuntimeProperty.put("output", true);
                                    if (jsonProperty.optBoolean("canBeUsedForFormPageVisibilty"))
                                        jsonRuntimeProperty.put("visibility", true);

                                    // add to the collection
                                    jsonRunTimeProperties.put(jsonRuntimeProperty);

                                    // increment the index
                                    index++;

                                    // get the next item if there's one there
                                    if (index < count)
                                        jsonProperty = jsonProperties.getJSONObject(index);

                                } while (index < count);
                                // add the properties to what we're returning
                                jsonControl.put("runtimeProperties", jsonRunTimeProperties);
                            }

                            // add it to the collection we are returning
                            jsonControls.put(jsonControl);

                        } // name check                                                   
                    } // control class check
                } // other page or visibility check
                  // run for any child controls
                getOtherPageChildControls(rapidServlet, jsonControls, control.getChildControls(),
                        includePageVisibiltyControls);
            }
        }
    }

    // uses the above iterative method to return a flat array of controls in this page that can be used from other pages, for use in the designer
    public JSONArray getOtherPageControls(RapidHttpServlet rapidServlet, boolean includePageVisibiltyControls)
            throws JSONException {
        // the array we're about to return
        JSONArray jsonControls = new JSONArray();
        // start building the array using the page controls
        getOtherPageChildControls(rapidServlet, jsonControls, _controls, includePageVisibiltyControls);
        // return the controls
        return jsonControls;
    }

    // used to turn either a page or control style into text for the css file
    public String getStyleCSS(Style style) {
        // start the text we are going to return
        String css = "";
        // get the style rules
        ArrayList<String> rules = style.getRules();
        // check we have some
        if (rules != null) {
            if (rules.size() > 0) {
                // add the style
                css = style.getAppliesTo().trim() + " {\n";
                // check we have 
                // loop and add the rules
                for (String rule : rules) {
                    css += "\t" + rule.trim() + "\n";
                }
                css += "}\n\n";
            }
        }
        // return the css
        return css;
    }

    // an iterative function for tree-walking child controls when building the page styles
    public void getChildControlStyles(List<Control> controls, StringBuilder stringBuilder) {
        if (controls != null) {
            for (Control control : controls) {
                // look for styles
                ArrayList<Style> controlStyles = control.getStyles();
                if (controlStyles != null) {
                    // loop the styles
                    for (Style style : controlStyles) {
                        // get some nice text for the css
                        stringBuilder.append(getStyleCSS(style));
                    }
                }
                // try and call on any child controls
                getChildControlStyles(control.getChildControls(), stringBuilder);
            }
        }
    }

    public String getAllCSS(ServletContext servletContext, Application application) {
        // the stringbuilder we're going to use
        StringBuilder stringBuilder = new StringBuilder();
        // check if the page has styles
        if (_styles != null) {
            // loop
            for (Style style : _styles) {
                stringBuilder.append(getStyleCSS(style));
            }
        }
        // use the iterative tree-walking function to add all of the control styles
        getChildControlStyles(_controls, stringBuilder);
        // return it with inserted parameters
        return application.insertParameters(servletContext, stringBuilder.toString());
    }

    public List<String> getAllActionTypes() {
        List<String> actionTypes = new ArrayList<String>();
        List<Action> actions = getAllActions();
        if (actions != null) {
            for (Action action : actions) {
                String actionType = action.getType();
                if (!actionTypes.contains(actionType))
                    actionTypes.add(actionType);
            }
        }
        return actionTypes;
    }

    public List<String> getAllControlTypes() {
        List<String> controlTypes = new ArrayList<String>();
        controlTypes.add("page");
        List<Control> controls = getAllControls();
        if (controls != null) {
            for (Control control : controls) {
                String controlType = control.getType();
                if (!controlTypes.contains(controlType))
                    controlTypes.add(controlType);
            }
        }
        return controlTypes;
    }

    // removes the page lock if it is more than 1 hour old
    public void checkLock() {
        // only check if there is one present
        if (_lock != null) {
            // get the time now
            Date now = new Date();
            // get the time an hour after the lock time
            Date lockExpiry = new Date(_lock.getDateTime().getTime() + 1000 * 60 * 60);
            // if the lock expiry has passed set the lock to null;
            if (now.after(lockExpiry))
                _lock = null;
        }
    }

    public void backup(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, Application application,
            File pageFile, boolean delete) throws IOException {

        // get the user name
        String userName = Files.safeName(rapidRequest.getUserName());

        // create folders to archive the pages
        String archivePath = application.getBackupFolder(rapidServlet.getServletContext(), delete);
        File archiveFolder = new File(archivePath);
        if (!archiveFolder.exists())
            archiveFolder.mkdirs();

        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String dateString = formatter.format(new Date());

        // create a file object for the archive file
        File archiveFile = new File(
                archivePath + "/" + Files.safeName(_name) + "_" + dateString + "_" + userName + ".page.xml");

        // copy the existing new file to the archive file
        Files.copyFile(pageFile, archiveFile);

    }

    public void deleteBackup(RapidHttpServlet rapidServlet, Application application, String backupId) {

        // create the path
        String backupPath = application.getBackupFolder(rapidServlet.getServletContext(), false) + "/" + backupId;
        // create the file
        File backupFile = new File(backupPath);
        // delete 
        Files.deleteRecurring(backupFile);

    }

    public void save(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, Application application,
            boolean backup) throws JAXBException, IOException {

        // create folders to save the pages
        String pagePath = application.getConfigFolder(rapidServlet.getServletContext()) + "/pages";
        File pageFolder = new File(pagePath);
        if (!pageFolder.exists())
            pageFolder.mkdirs();

        // create a file object for the new file
        File newFile = new File(pagePath + "/" + Files.safeName(getName()) + ".page.xml");

        // if we want a backup and the new file already exists it needs archiving
        if (backup && newFile.exists())
            backup(rapidServlet, rapidRequest, application, newFile, false);

        // create a file for the temp file
        File tempFile = new File(pagePath + "/" + Files.safeName(getName()) + "-saving.page.xml");

        // update the modified by and date
        _modifiedBy = rapidRequest.getUserName();
        _modifiedDate = new Date();

        // get a buffered writer for our page with UTF-8 file format
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile), "UTF-8"));

        try {
            // marshall the page object into the temp file
            rapidServlet.getMarshaller().marshal(this, bw);
        } catch (JAXBException ex) {
            // close the file writer
            bw.close();
            // re throw the exception
            throw ex;
        }

        // close the file writer
        bw.close();

        // copy the tempFile to the newFile
        Files.copyFile(tempFile, newFile);

        // delete the temp file
        tempFile.delete();

        // replace the old page with the new page
        application.getPages().addPage(this, newFile);

        // empty the cached page html
        _cachedHeadLinks = null;
        _cachedHeadCSS = null;
        _cachedHeadReadyJS = null;
        _cachedHeadJS = null;
        // empty the cached action types
        _actionTypes = null;
        // empty the cached control types
        _controlTypes = null;

        // empty the page variables so they are rebuilt the next time
        application.emptyPageVariables();

    }

    public void delete(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, Application application)
            throws JAXBException, IOException {

        // create folders to delete the page
        String pagePath = application.getConfigFolder(rapidServlet.getServletContext()) + "/pages";

        // create a file object for the delete file
        File delFile = new File(pagePath + "/" + Files.safeName(getName()) + ".page.xml");

        // if the new file already exists it needs archiving
        if (delFile.exists()) {
            // archive the page file
            backup(rapidServlet, rapidRequest, application, delFile, false);
            // delete the page file
            delFile.delete();
            // remove it from the current list of pages
            application.getPages().removePage(_id);
        }

        // get the resources path
        String resourcesPath = application.getWebFolder(rapidServlet.getServletContext());

        // create a file object for deleting the page css file
        File delCssFile = new File(resourcesPath + "/" + Files.safeName(getName()) + ".css");

        // delete if it exists
        if (delCssFile.exists())
            delCssFile.delete();

        // create a file object for deleting the page css file
        File delCssFileMin = new File(resourcesPath + "/" + Files.safeName(getName()) + ".min.css");

        // delete if it exists
        if (delCssFileMin.exists())
            delCssFileMin.delete();

    }

    // this includes functions to iteratively call any control initJavaScript and set up any event listeners
    private void getPageLoadLines(List<String> pageloadLines, List<Control> controls) throws JSONException {
        // if we have controls
        if (controls != null) {
            // loop controls
            for (Control control : controls) {
                // check for any initJavaScript to call
                if (control.hasInitJavaScript()) {
                    // get any details we may have
                    String details = control.getDetails();
                    // set to empty string or clean up
                    if (details == null) {
                        details = "";
                    } else {
                        details = ", " + control.getId() + "details";
                    }
                    // write an init call method
                    pageloadLines
                            .add("Init_" + control.getType() + "('" + control.getId() + "'" + details + ");\n");
                }
                // check event actions
                if (control.getEvents() != null) {
                    // loop events
                    for (Event event : control.getEvents()) {
                        // only if event is non-custom and there are actually some actions to invoke
                        if (!event.isCustomType() && event.getActions() != null) {
                            if (event.getActions().size() > 0) {
                                // add any page load lines from this 
                                pageloadLines.add(event.getPageLoadJavaScript(control));
                            }
                        }
                    }
                }
                // now call iteratively for child controls (of this [child] control, etc.)
                if (control.getChildControls() != null)
                    getPageLoadLines(pageloadLines, control.getChildControls());
            }
        }
    }

    // the html for a specific resource
    public String getResourceHtml(Application application, Resource resource) {

        // assume we couldn't make the resource html
        String resourceHtml = null;

        // set the link according to the type
        switch (resource.getType()) {
        case Resource.JAVASCRIPT:
            if (application.getStatus() == Application.STATUS_LIVE) {
                try {
                    resourceHtml = "    <script type='text/javascript'>"
                            + Minify.toString(resource.getContent(), Minify.JAVASCRIPT) + "</script>";
                } catch (IOException ex) {
                    resourceHtml = "    <script type='text/javascript'>/* Failed to minify resource "
                            + resource.getName() + " JavaScript : " + ex.getMessage() + "*/</script>";
                }
            } else {
                resourceHtml = "    <script type='text/javascript'>\n" + resource.getContent() + "\n    </script>";
            }
            break;
        case Resource.CSS:
            if (application.getStatus() == Application.STATUS_LIVE) {
                try {
                    resourceHtml = "    <style>" + Minify.toString(resource.getContent(), Minify.CSS) + "<style>";
                } catch (IOException ex) {
                    resourceHtml = "    <style>/* Failed to minify resource " + resource.getName() + " CSS : "
                            + ex.getMessage() + "*/<style>";
                }
            } else {
                resourceHtml = "    <style>" + resource.getContent() + "<style>";
            }
            break;
        case Resource.JAVASCRIPTFILE:
        case Resource.JAVASCRIPTLINK:
            resourceHtml = "    <script type='text/javascript' src='" + resource.getContent() + "'></script>";
            break;
        case Resource.CSSFILE:
        case Resource.CSSLINK:
            resourceHtml = "    <link rel='stylesheet' type='text/css' href='" + resource.getContent()
                    + "'></link>";
            break;
        }
        // return it
        return resourceHtml;

    }

    // the resources for the page
    public String getResourcesHtml(Application application, boolean allResources) {

        StringBuilder stringBuilder = new StringBuilder();

        // get all action types used in this page
        if (_actionTypes == null)
            _actionTypes = getAllActionTypes();
        // get all control types used in this page
        if (_controlTypes == null)
            _controlTypes = getAllControlTypes();

        // manage the resources links added already so we don't add twice
        ArrayList<String> addedResources = new ArrayList<String>();

        // if this application has resources add during initialisation
        if (application.getResources() != null) {
            // loop and add the resources required by this application's controls and actions (created when application loads)
            for (Resource resource : application.getResources()) {
                // if we want all the resources (for the designer) or there is a dependency for this resource
                if (allResources || resource.hasDependency(ResourceDependency.RAPID)
                        || resource.hasDependency(ResourceDependency.ACTION, _actionTypes)
                        || resource.hasDependency(ResourceDependency.CONTROL, _controlTypes)) {
                    // the html we're hoping to get
                    String resourceHtml = getResourceHtml(application, resource);
                    // if we got some html and don't have it already 
                    if (resourceHtml != null && !addedResources.contains(resourceHtml)) {
                        // append it
                        stringBuilder.append(resourceHtml + "\n");
                        // remember we've added it
                        addedResources.add(resourceHtml);
                    }

                } // dependency check

            } // resource loop

        } // has resources

        return stringBuilder.toString();

    }

    private void getEventJavaScriptFunction(RapidRequest rapidRequest, StringBuilder stringBuilder,
            Application application, Control control, Event event) {
        // check actions are initialised
        if (event.getActions() != null) {
            // check there are some to loop
            if (event.getActions().size() > 0) {

                // create actions separately to avoid redundancy
                StringBuilder actionStringBuilder = new StringBuilder();
                StringBuilder eventStringBuilder = new StringBuilder();

                // start the function name
                String functionName = "Event_" + event.getType() + "_";
                // if this is the page (no control) use the page id, otherwise use the controlId
                if (control == null) {
                    // append the page id
                    functionName += _id;
                } else {
                    // append the control id
                    functionName += control.getId();
                }

                // create a function for running the actions for this controls events
                eventStringBuilder.append("function " + functionName + "(ev) {\n");
                // open a try/catch
                eventStringBuilder.append("  try {\n");

                // get any filter javascript
                String filter = event.getFilter();
                // if we have any add it now
                if (filter != null) {
                    // only bother if not an empty string
                    if (!"".equals(filter)) {
                        eventStringBuilder.append("    " + filter.trim().replace("\n", "\n    ") + "\n");
                    }
                }

                // loop the actions and produce the handling JavaScript
                for (Action action : event.getActions()) {

                    try {
                        // get the action client-side java script from the action object (it's generated there as it can contain values stored in the object on the server side)
                        String actionJavaScript = action.getJavaScript(rapidRequest, application, this, control,
                                null);
                        // if non null
                        if (actionJavaScript != null) {
                            // trim it to avoid tabs and line breaks that might sneak in
                            actionJavaScript = actionJavaScript.trim();
                            // only if what we got is not an empty string
                            if (!("").equals(actionJavaScript)) {
                                // if this action has been marked for redundancy avoidance 
                                if (action.getAvoidRedundancy()) {
                                    // add the action function to the action stringbuilder so it's before the event
                                    actionStringBuilder.append("function Action_" + action.getId() + "(ev) {\n"
                                            + "  " + actionJavaScript.trim().replace("\n", "\n  ") + "\n"
                                            + "  return true;\n" + "}\n\n");
                                    // add an action function call to the event string builder
                                    eventStringBuilder
                                            .append("    if (!Action_" + action.getId() + "(ev)) return false;\n");
                                    //eventStringBuilder.append("    Action_" + action.getId() + "(ev);\n");
                                } else {
                                    // go straight into the event
                                    eventStringBuilder.append(
                                            "    " + actionJavaScript.trim().replace("\n", "\n    ") + "\n");
                                }
                            }

                        }

                    } catch (Exception ex) {

                        // print a commented message
                        eventStringBuilder.append("//    Error creating JavaScript for action " + action.getId()
                                + " : " + ex.getMessage() + "\n");

                    }

                }
                // close the try/catch
                if (control == null) {
                    // page
                    eventStringBuilder
                            .append("  } catch(ex) { Event_error('" + event.getType() + "',null,ex); }\n");
                } else {
                    // control
                    eventStringBuilder.append("  } catch(ex) { Event_error('" + event.getType() + "','"
                            + control.getId() + "',ex); }\n");
                }
                // close event function
                eventStringBuilder.append("}\n\n");

                // add the action functions
                stringBuilder.append(actionStringBuilder);

                // add the event function
                stringBuilder.append(eventStringBuilder);
            }
        }
    }

    // build the event handling page JavaScript iteratively
    private void getEventHandlersJavaScript(RapidRequest rapidRequest, StringBuilder stringBuilder,
            Application application, List<Control> controls) throws JSONException {
        // check there are some controls             
        if (controls != null) {
            // if we're at the root of the page
            if (controls.equals(_controls)) {
                // check for page events
                if (_events != null) {
                    // loop page events and get js functions
                    for (Event event : _events)
                        getEventJavaScriptFunction(rapidRequest, stringBuilder, application, null, event);
                }
            }
            for (Control control : controls) {
                // check event actions
                if (control.getEvents() != null) {
                    // loop page events and get js functions
                    for (Event event : control.getEvents())
                        getEventJavaScriptFunction(rapidRequest, stringBuilder, application, control, event);
                }
                // now call iteratively for child controls (of this [child] control, etc.)
                if (control.getChildControls() != null)
                    getEventHandlersJavaScript(rapidRequest, stringBuilder, application,
                            control.getChildControls());
            }
        }
    }

    // this method produces the start of the head (which is shared by the no permission respone)
    private String getHeadStart(Application application) {
        return "  <head>\n" + "    <title>" + _title + " - by Rapid</title>\n"
                + "    <meta description=\"Created using Rapid - www.rapid-is.co.uk\"/>\n"
                + "    <meta charset=\"utf-8\"/>\n"
                + "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n"
                + (application != null
                        ? "    <meta name=\"theme-color\" content=\"" + application.getStatusBarColour() + "\" />\n"
                        : "")
                + "    <link rel=\"icon\" href=\"favicon.ico\"></link>\n";
    }

    // this private method produces the head of the page which is often cached, if resourcesOnly is true only page resources are included which is used when sending no permission
    private String getHeadLinks(RapidHttpServlet rapidServlet, Application application, boolean isDialogue)
            throws JSONException {

        // create a string builder containing the head links
        StringBuilder stringBuilder = new StringBuilder(getHeadStart(application));

        // if you're looking for where the jquery link is added it's the first resource in the page.control.xml file   
        stringBuilder.append("    " + getResourcesHtml(application, false).trim() + "\n");

        // add a JavaScript block with important global variables - this removed by the pagePanel loader and navigation action when showing dialogues, by matching to the various variables so be careful changing anything below         
        stringBuilder.append("    <script type='text/javascript'>\n");
        if (application != null) {
            stringBuilder.append("var _appId = '" + application.getId() + "';\n");
            stringBuilder.append("var _appVersion = '" + application.getVersion() + "';\n");
        }
        stringBuilder.append("var _pageId = '" + _id + "';\n");
        stringBuilder.append("var _mobileResume = false;\n");
        stringBuilder.append("    </script>\n");

        return stringBuilder.toString();

    }

    // this private method writes JS specific to the user
    private void writeUserJS(Writer writer, RapidRequest rapidRequest, Application application, User user)
            throws RapidLoadingException, IOException {

        // open js
        writer.write("    <script type='text/javascript'>\n");
        // print user
        if (user != null)
            writer.write("var _userName = '" + user.getName() + "';\n");
        // get app page variables
        List<String> pageVariables = application
                .getPageVariables(rapidRequest.getRapidServlet().getServletContext());
        // if we got some
        if (pageVariables != null) {
            // loop them
            for (String pageVariable : pageVariables) {
                // look for a value in the session
                String value = (String) rapidRequest.getSessionAttribute(pageVariable);
                // if we got one print it as escaped html
                if (value != null)
                    writer.write("var _pageVariable_" + pageVariable + " = '" + Html.escape(value) + "';\n");
            }
        }
        // close js
        writer.write("    </script>\n");
    }

    // this private method produces the head of the page which is often cached, if resourcesOnly is true only page resources are included which is used when sending no permission
    private String getHeadCSS(RapidRequest rapidRequest, Application application, boolean isDialogue)
            throws JSONException {

        // create an empty string builder
        StringBuilder stringBuilder = new StringBuilder();

        // fetch all page control styles
        String pageCss = getAllCSS(rapidRequest.getRapidServlet().getServletContext(), application);

        // only if there is some
        if (pageCss.length() > 0) {

            // open style blocks         
            stringBuilder.append("    <style>\n");

            // if live we're going to try and minify
            if (application.getStatus() == Application.STATUS_LIVE) {
                try {
                    // get string to itself minified
                    pageCss = Minify.toString(pageCss, Minify.CSS);
                } catch (IOException ex) {
                    // add error and resort to unminified
                    pageCss = "\n/*\n\n Failed to minify the css : " + ex.getMessage() + "\n\n*/\n\n" + pageCss;
                }
            } else {
                // prefix with minify message
                pageCss = "\n/* The code below is minified for live applications */\n\n" + pageCss;

            }
            // add it to the page
            stringBuilder.append(pageCss);
            // close the style block
            stringBuilder.append("    </style>\n");

        }

        // return it
        return stringBuilder.toString();

    }

    // this private method produces the head of the page which is often cached, if resourcesOnly is true only page resources are included which is used when sending no permission
    private String getHeadReadyJS(RapidRequest rapidRequest, Application application, boolean isDialogue,
            FormAdapter formAdapter) throws JSONException {

        // make a new string builder just for the js (so we can minify it independently)
        StringBuilder jsStringBuilder = new StringBuilder();

        // add an extra line break for non-live applications
        if (application.getStatus() != Application.STATUS_LIVE)
            jsStringBuilder.append("\n");

        // get all controls
        List<Control> pageControls = getAllControls();

        // if we got some
        if (pageControls != null) {
            // loop them
            for (Control control : pageControls) {
                // get the details
                String details = control.getDetails();
                // check if null
                if (details != null) {
                    // create a gloabl variable for it's details
                    jsStringBuilder.append("var " + control.getId() + "details = " + details + ";\n");
                }
            }
            // add a line break again if we printed anything
            if (jsStringBuilder.length() > 0)
                jsStringBuilder.append("\n");
        }

        // initialise the form controls that need their values added in the dynamic part of the page script
        _formControlValues = new ArrayList<String>();
        // get all actions
        List<Action> actions = getAllActions();
        // loop them
        for (Action action : actions) {
            // if form type
            if ("form".equals(action.getType())) {
                // get the action type
                String type = action.getProperty("actionType");
                // if value copy
                if ("val".equals(type)) {
                    // get control id
                    String controlId = action.getProperty("dataSource");
                    // add to collection if all in order
                    if (controlId != null)
                        _formControlValues.add(controlId);
                } else if ("sub".equals(type)) {
                    // add sub as id
                    _formControlValues.add("sub");
                } else if ("err".equals(type)) {
                    // add err as id
                    _formControlValues.add("err");
                } else if ("res".equals(type)) {
                    // add res as id
                    _formControlValues.add("res");
                }
            }
        }

        // initialise our pageload lines collections
        _pageloadLines = new ArrayList<String>();

        // get any control initJavaScript event listeners into he pageloadLine (goes into $(document).ready function)
        getPageLoadLines(_pageloadLines, _controls);

        // sort the page load lines
        Collections.sort(_pageloadLines, new Comparator<String>() {
            @Override
            public int compare(String l1, String l2) {
                if (l1.isEmpty())
                    return -1;
                if (l2.isEmpty())
                    return 1;
                char i1 = l1.charAt(0);
                char i2 = l2.charAt(0);
                return i2 - i1;
            }
        });

        // if there is a form adapter in place
        if (formAdapter != null) {
            // add a line to set any form values before the load event is run
            _pageloadLines.add("Event_setFormValues($.Event('setValues'));\n");
            // add an init form function - in extras.js
            _pageloadLines.add("Event_initForm('" + _id + "');\n");
        }

        // check for page events (this is here so all listeners are registered by now) and controls (there should not be none but nothing happens without them)
        if (_events != null && _controls != null) {
            // loop page events
            for (Event event : _events) {
                // only if there are actually some actions to invoke
                if (event.getActions() != null) {
                    if (event.getActions().size() > 0) {
                        // page is a special animal so we need to do each of it's event types differently
                        if ("pageload".equals(event.getType())) {
                            _pageloadLines
                                    .add("if (!_mobileResume) Event_pageload_" + _id + "($.Event('pageload'));\n");
                        }
                        // resume is also a special animal
                        if ("resume".equals(event.getType())) {
                            // fire the resume event immediately if there is no rapidMobile (it will be done by the Rapid Mobile app if present)
                            _pageloadLines.add(
                                    "if (!window['_rapidmobile']) Event_resume_" + _id + "($.Event('resume'));\n");
                        }
                        // reusable action is only invoked via reusable actions on other events - there is no listener
                    }
                }
            }
        }

        // if there is a form adapter in place
        if (formAdapter != null) {
            // add a line to check the form now all load events have been run
            _pageloadLines.add("Event_checkForm();\n");
        }

        // if this is not a dialogue or there are any load lines
        if (!isDialogue || _pageloadLines.size() > 0) {

            // open the page loaded function
            jsStringBuilder.append("$(document).ready( function() {\n");

            // add a try
            jsStringBuilder.append("  try {\n");

            // print any page load lines such as initialising controls
            for (String line : _pageloadLines)
                jsStringBuilder.append("    " + line);

            // close the try
            jsStringBuilder.append("  } catch(ex) { $('body').html(ex); }\n");

            // after 200 milliseconds show and trigger a window resize for any controls that might be listening (this also cuts out any flicker), we also call focus on the elements we marked for focus while invisible (in extras.js)
            jsStringBuilder.append(
                    "  window.setTimeout( function() {\n    $(window).resize();\n    $('body').css('visibility','visible');\n    $('[data-focus]').focus();\n  }, 200);\n");

            // end of page loaded function
            jsStringBuilder.append("});\n\n");

        }

        // return it
        return jsStringBuilder.toString();

    }

    // this private method produces the head of the page which is often cached, if resourcesOnly is true only page resources are included which is used when sending no permission
    private String getHeadJS(RapidRequest rapidRequest, Application application, boolean isDialogue)
            throws JSONException {

        // a string builder
        StringBuilder stringBuilder = new StringBuilder();

        // make a new string builder just for the js (so we can minify it independently)
        StringBuilder jsStringBuilder = new StringBuilder();

        // get all actions in the page
        List<Action> pageActions = getAllActions();

        // only proceed if there are actions in this page
        if (pageActions != null) {

            // loop the list of actions 
            for (Action action : pageActions) {
                // if this is a form action
                // indentify potential redundancies before we create all the event handling JavaScript
                try {
                    // look for any page javascript that this action may have
                    String actionPageJavaScript = action.getPageJavaScript(rapidRequest, application, this, null);
                    // print it here if so
                    if (actionPageJavaScript != null)
                        jsStringBuilder.append(actionPageJavaScript.trim() + "\n\n");
                    // if this action adds redundancy to any others 
                    if (action.getRedundantActions() != null) {
                        // loop them
                        for (String actionId : action.getRedundantActions()) {
                            // try and find the action
                            Action redundantAction = getAction(actionId);
                            // if we got one
                            if (redundantAction != null) {
                                // update the redundancy avoidance flag
                                redundantAction.avoidRedundancy(true);
                            }
                        }
                    } // redundantActions != null
                } catch (Exception ex) {
                    // print the exception as a comment
                    jsStringBuilder.append("// Error producing page JavaScript : " + ex.getMessage() + "\n\n");
                }

            } // action loop      

            // add event handlers, staring at the root controls
            getEventHandlersJavaScript(rapidRequest, jsStringBuilder, application, _controls);
        }

        // if there was any js
        if (jsStringBuilder.length() > 0) {

            // check the application status
            if (application.getStatus() == Application.STATUS_LIVE) {
                try {
                    // minify the js before adding
                    stringBuilder.append(Minify.toString(jsStringBuilder.toString(), Minify.JAVASCRIPT));
                } catch (IOException ex) {
                    // add the error
                    stringBuilder.append("\n\n/* Failed to minify JavaScript : " + ex.getMessage() + " */\n\n");
                    // add the js as is
                    stringBuilder.append(jsStringBuilder);
                }
            } else {
                // add the js as is
                stringBuilder.append("/* The code below is minified for live applications */\n\n"
                        + jsStringBuilder.toString().trim() + "\n");
            }

        }

        // get it into a string and insert any parameters
        String headJS = application.insertParameters(rapidRequest.getRapidServlet().getServletContext(),
                stringBuilder.toString());

        // return it
        return headJS;

    }

    // this routine will write the no page permission - used by both page permission and if control permission permutations fail to result in any html
    public void writeMessage(Writer writer, String title, String message) throws IOException {

        // write the head html without the JavaScript and CSS (index.css is substituted for us)
        writer.write(getHeadStart(null));

        // add the icon
        writer.write("    <link rel='icon' href='favicon.ico'></link>\n");

        // add the jQuery link
        writer.write("    <script type='text/javascript' src='scripts/jquery-1.10.2.js'></script>\n");

        // add the index.css
        writer.write("    <link rel='stylesheet' type='text/css' href='styles/index.css'></link>\n");

        // close the head
        writer.write("</head>\n");

        // open the body
        writer.write("  <body>\n");

        // write no permission (body is closed at the end of this method)
        writer.write(
                "<div class=\"image\"><img src=\"images/RapidLogo_200x134.png\" /></div><div class=\"title\"><span>"
                        + title + "</span></div><div class=\"info\"><p>" + message + "</p></div>\n");

    }

    // this routine produces the entire page
    public void writeHtml(RapidHttpServlet rapidServlet, HttpServletResponse response, RapidRequest rapidRequest,
            Application application, User user, Writer writer, boolean designerLink)
            throws JSONException, IOException, RapidLoadingException {

        // this doctype is necessary (amongst other things) to stop the "user agent stylesheet" overriding styles
        writer.write("<!DOCTYPE html>\n");

        // open the html
        writer.write("<html>\n");

        // check for undermaintenance status
        if (application.getStatus() == Application.STATUS_MAINTENANCE) {

            writeMessage(writer, "Rapid - Under maintenance",
                    "This application is currently under maintenance. Please try again in a few minutes.");

        } else {

            // get the security
            SecurityAdapter security = application.getSecurityAdapter();

            // get any form adapter
            FormAdapter formAdapter = application.getFormAdapter();

            // assume the user has permission to access the page
            boolean gotPagePermission = true;

            try {

                // if this page has roles
                if (_roles != null) {
                    if (_roles.size() > 0) {
                        // check if the user has any of them
                        gotPagePermission = security.checkUserRole(rapidRequest, _roles);
                    }
                }

            } catch (SecurityAdapaterException ex) {

                rapidServlet.getLogger().error("Error checking for page roles", ex);

            }

            // check that there's permission
            if (gotPagePermission) {

                // whether we're rebulding the page for each request
                boolean rebuildPages = Boolean
                        .parseBoolean(rapidServlet.getServletContext().getInitParameter("rebuildPages"));

                // check whether or not we rebuild
                if (rebuildPages) {
                    // get fresh head links
                    writer.write(getHeadLinks(rapidServlet, application, !designerLink));
                    // write the user-specific JS
                    writeUserJS(writer, rapidRequest, application, user);
                    // get fresh js and css
                    writer.write(getHeadCSS(rapidRequest, application, !designerLink));
                    // open the script
                    writer.write("    <script  type='text/javascript'>\n");
                    // write the ready JS
                    writer.write(getHeadReadyJS(rapidRequest, application, !designerLink, formAdapter));
                } else {
                    // rebuild any uncached 
                    if (_cachedHeadLinks == null)
                        _cachedHeadLinks = getHeadLinks(rapidServlet, application, !designerLink);
                    if (_cachedHeadCSS == null)
                        _cachedHeadCSS = getHeadCSS(rapidRequest, application, !designerLink);
                    if (_cachedHeadReadyJS == null)
                        _cachedHeadReadyJS = getHeadReadyJS(rapidRequest, application, !designerLink, formAdapter);
                    // get the cached head links
                    writer.write(_cachedHeadLinks);
                    // write the user-specific JS
                    writeUserJS(writer, rapidRequest, application, user);
                    // get the cached head js and css
                    writer.write(_cachedHeadCSS);
                    // open the script
                    writer.write("    <script  type='text/javascript'>\n");
                    // write the ready JS
                    writer.write(_cachedHeadReadyJS);
                }

                // if there is a form
                if (formAdapter != null) {

                    // set no cache on this page
                    RapidFilter.noCache(response);

                    // a placeholder for any form id
                    String formId = null;
                    // a placeholder for any form values
                    StringBuilder formValues = null;

                    // first do the actions that could result in an exception
                    try {

                        // get the form details
                        UserFormDetails formDetails = formAdapter.getUserFormDetails(rapidRequest);

                        // if we got some
                        if (formDetails != null) {

                            // set the form id
                            formId = formDetails.getId();

                            // create the values string builder
                            formValues = new StringBuilder();

                            // set whether submitted
                            formValues.append("var _formSubmitted = " + formDetails.getSubmitted() + ";\n\n");

                            // start the form values object (to supply previous form values)
                            formValues.append("var _formValues = {");

                            // if form control values to set
                            if (_formControlValues != null) {

                                // loop then
                                for (int i = 0; i < _formControlValues.size(); i++) {

                                    // get the control id
                                    String id = _formControlValues.get(i);

                                    // place holder for the value
                                    String value = null;

                                    // some id's are special
                                    if ("id".equals(id)) {
                                        // the submission message
                                        value = formDetails.getId();
                                    } else if ("sub".equals(id)) {
                                        // the submission message
                                        value = formDetails.getSubmitMessage();
                                    } else if ("err".equals(id)) {
                                        // the submission message
                                        value = formDetails.getErrorMessage();
                                    } else if ("res".equals(id)) {
                                        // the submission message
                                        value = formDetails.getPassword();
                                    } else {
                                        // lookup the value
                                        value = formAdapter.getFormControlValue(rapidRequest, formId, id, false);
                                    }

                                    // if we got one
                                    if (value != null) {
                                        // escape it and enclose it
                                        value = value.replace("\\", "\\\\").replace("'", "\\'")
                                                .replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "");
                                        // add to object
                                        formValues.append("'" + id + "':'" + value + "'");
                                        // add comma 
                                        formValues.append(",");
                                    }
                                }
                            }

                            // close it
                            formValues.append("'id':_formId};\n\n");

                            // start the set form values function
                            formValues.append("function Event_setFormValues(ev) {");

                            // get any form page values
                            FormPageControlValues formControlValues = formAdapter
                                    .getFormPageControlValues(rapidRequest, formId, _id);

                            // if there are any
                            if (formControlValues != null) {
                                if (formControlValues.size() > 0) {

                                    // add a line break
                                    formValues.append("\n");

                                    // loop the values
                                    for (FormControlValue formControlValue : formControlValues) {

                                        // get the control
                                        Control pageControl = getControl(formControlValue.getId());

                                        // if we got one
                                        if (pageControl != null) {

                                            // get the value
                                            String value = formControlValue.getValue();
                                            // assume no field
                                            String field = "null";
                                            // the dropdown control needs a little help
                                            if ("dropdown".equals(pageControl.getType()))
                                                field = "'x'";
                                            // get any control details
                                            String details = pageControl.getDetailsJavaScript(application, this);
                                            // if null update to string
                                            if (details == null)
                                                details = null;
                                            // if there is a value use the standard setData for it (this might change to something more sophisticated at some point)
                                            if (value != null)
                                                formValues.append("  if (window[\"setData_" + pageControl.getType()
                                                        + "\"]) setData_" + pageControl.getType() + "(ev, '"
                                                        + pageControl.getId() + "', " + field + ", " + details
                                                        + ", '"
                                                        + value.replace("\\", "\\\\").replace("'", "\\'")
                                                                .replace("\r\n", "\\n").replace("\n", "\\n")
                                                                .replace("\r", "")
                                                        + "');\n");
                                        }
                                    }
                                }
                            }
                            // close the function
                            formValues.append("};\n\n");

                        }

                        // write the form id into the page - not necessary for dialogues
                        if (designerLink)
                            writer.write("var _formId = '" + formId + "';\n\n");

                        // write the form values
                        writer.write(formValues.toString());

                        // now the page has been printed invalidate the form if this was a submission page
                        if (_formPageType == FORM_PAGE_TYPE_SUBMITTED)
                            formAdapter.setUserFormDetails(rapidRequest, null);

                    } catch (Exception ex) {
                        // log the error
                        rapidServlet.getLogger().error("Error create page form values", ex);
                    }

                }

                if (rebuildPages) {
                    // write the ready JS
                    writer.write(getHeadJS(rapidRequest, application, !designerLink));
                } else {
                    // get the rest of the cached JS
                    if (_cachedHeadJS == null)
                        _cachedHeadJS = getHeadJS(rapidRequest, application, !designerLink);
                    // write the ready JS
                    writer.write(_cachedHeadJS);
                }

                // close the script
                writer.write("\n    </script>\n");

                // close the head
                writer.write("  </head>\n");

                // start the body      
                writer.write("  <body id='" + _id + "' style='visibility:hidden;'>\n");

                // start the form if in use (but not for dialogues and other cases where the page is partial)      
                if (formAdapter != null && designerLink) {
                    writer.write("    <form id='" + _id + "_form' action='~?a=" + application.getId() + "&v="
                            + application.getVersion() + "&p=" + _id + "' method='POST'>\n");
                    writer.write("      <input type='hidden' id='" + _id + "_hiddenControls' name='" + _id
                            + "_hiddenControls' />\n");
                }

                // a reference for the body html
                String bodyHtml = null;

                // get the users roles
                List<String> userRoles = user.getRoles();

                // check we have userRoles and htmlRoles
                if (userRoles != null && _rolesHtml != null) {

                    // loop each roles html entry
                    for (RoleHtml roleHtml : _rolesHtml) {

                        // get the roles from this combination
                        List<String> roles = roleHtml.getRoles();

                        // assume not roles are required (this will be updated if roles are present)
                        int rolesRequired = 0;

                        // keep a running count for the roles we have
                        int gotRoleCount = 0;

                        // if there are roles to check
                        if (roles != null) {

                            // update how many roles we need our user to have
                            rolesRequired = roles.size();

                            // check whether we need any roles and that our user has any at all
                            if (rolesRequired > 0) {
                                // check the user has as many roles as this combination requires
                                if (userRoles.size() >= rolesRequired) {
                                    // loop the roles we need for this combination
                                    for (String role : roleHtml.getRoles()) {
                                        // check this role
                                        if (userRoles.contains(role)) {
                                            // increment the got role count
                                            gotRoleCount++;
                                        } // increment the count of required roles

                                    } // loop roles

                                } // user has enough roles to bother checking this combination

                            } // if any roles are required

                        } // add roles to check

                        // if we have all the roles we need
                        if (gotRoleCount == rolesRequired) {
                            // use this html
                            bodyHtml = roleHtml.getHtml();
                            // no need to check any further
                            break;
                        }

                    } // html role combo loop

                } // if our users have roles and we have different html for roles

                // check if we got any body html via the roles
                if (bodyHtml == null) {

                    // didn't get any body html, show no permission
                    writeMessage(writer, "Rapid - No permission", "You do not have permssion to view this page");

                } else {

                    // check the status of the application
                    if (application.getStatus() == Application.STATUS_DEVELOPMENT) {
                        // pretty print
                        writer.write(Html.getPrettyHtml(bodyHtml.trim()));
                    } else {
                        // no pretty print
                        writer.write(bodyHtml.trim());
                    }

                    // close the form
                    if (formAdapter != null && designerLink)
                        writer.write("    </form>\n");

                } // got body html check

            } else {

                // no page permission
                writeMessage(writer, "Rapid - No permission", "You do not have permssion to view this page");

            } // page permission check      

            try {

                // whether to include the designer link - dialogues and files in the .zip do not so no need to even check permission
                if (designerLink) {

                    // assume not admin link
                    boolean adminLinkPermission = false;

                    // check for the design role, super is required as well if the rapid app
                    if ("rapid".equals(application.getId())) {
                        if (security.checkUserRole(rapidRequest, Rapid.DESIGN_ROLE)
                                && security.checkUserRole(rapidRequest, Rapid.SUPER_ROLE))
                            adminLinkPermission = true;
                    } else {
                        if (security.checkUserRole(rapidRequest, Rapid.DESIGN_ROLE))
                            adminLinkPermission = true;
                    }

                    // if we had the admin link
                    if (adminLinkPermission) {

                        // create string builder for the links
                        StringBuilder designLinkStringBuilder = new StringBuilder();
                        // create a string builder for the jquery
                        StringBuilder designLinkJQueryStringBuilder = new StringBuilder();
                        // loop all of the controls
                        for (Control control : getAllControls()) {
                            // get the json control definition
                            JSONObject jsonControl = rapidServlet.getJsonControl(control.getType());
                            // definition check
                            if (jsonControl != null) {
                                // look for the design link jquery
                                String designLinkJQuery = jsonControl.optString("designLinkJQuery", null);
                                // if we got any design link jquery
                                if (designLinkJQuery != null) {
                                    // get the image title from the control name
                                    String title = control.getName();
                                    // escape any apostrophes
                                    if (title != null)
                                        title = title.replace("'", "&apos;");
                                    // add the link into the string builder
                                    designLinkStringBuilder.append("<a id='designLink_" + control.getId()
                                            + "' data-id='" + control.getId() + "' href='#'><img src='"
                                            + jsonControl.optString("image", "images/penknife_24x24.png")
                                            + "' title='" + title + "'/></a>\n");
                                    // trim the JQuery
                                    designLinkJQuery = designLinkJQuery.trim();
                                    // start with a . if not
                                    if (!designLinkJQuery.startsWith("."))
                                        designLinkJQuery = "." + designLinkJQuery;
                                    // end with ; if not
                                    if (!designLinkJQuery.endsWith(";"))
                                        designLinkJQuery += ";";
                                    // add the jquery after the object reference
                                    designLinkJQueryStringBuilder.append("  $('#designLink_" + control.getId()
                                            + "')" + designLinkJQuery.replace("\n", "\n  ") + "\n");
                                }
                            }
                        }

                        // using attr href was the weirdest thing. Some part of jQuery seemed to be setting the url back to v=1&p=P1 when v=2&p=P2 was printed in the html
                        writer.write(" <link rel='stylesheet' type='text/css' href='styles/designlinks.css'></link>"
                                + " <script type='text/javascript' src='scripts/designlinks.js'></script>"
                                + "<div id='designShow' style='position:fixed;left:0;bottom:0;width:30px;height:30px;z-index:10000;'></div>\n"
                                + "<div id='designLinks' style='position:fixed;left:0;bottom:0;z-index:10001;padding:5px;display:none;'>"
                                + "<a id='designLink' href='#'><img src='images/gear_24x24.png' title='Open Rapid Design'/></a>\n"
                                + "<a id='designLinkNewTab' style='margin-left:-3px;' href='#'><img src='images/triangleRight_8x8.png' title='Open Rapid Design in a new tab'/></a>\n"
                                + designLinkStringBuilder.toString() + "</div>"
                                + "<script type='text/javascript'>\n" + "/* designLink */\n"
                                + "$(document).ready( function() {\n"
                                + "  $('#designShow').mouseover ( function(ev) {\n     $('#designLink').attr('href','design.jsp?a="
                                + application.getId() + "&v=" + application.getVersion() + "&p=" + _id
                                + "'); $('#designLinkNewTab').attr('target','_blank').attr('href','design.jsp?a="
                                + application.getId() + "&v=" + application.getVersion() + "&p=" + _id
                                + "'); $('#designLinks').show();\n  });\n"
                                + "  $('#designLinks').mouseleave ( function(ev) {\n     $('#designLinks').hide();\n  });\n"
                                + designLinkJQueryStringBuilder.toString() + "});\n" + "</script>\n");

                    }

                }

            } catch (SecurityAdapaterException ex) {

                rapidServlet.getLogger().error("Error checking for the designer link", ex);

            }

        }

        // add the remaining elements
        writer.write("  </body>\n</html>");

    }

    // gets the value of a condition used in the page visibility rules
    private String getConditionValue(RapidRequest rapidRequest, String formId, FormAdapter formAdapter,
            Application application, Value value) throws Exception {
        String[] idParts = value.getId().split("\\.");
        if (idParts[0].equals("System")) {
            // just check that there is a type
            if (idParts.length > 1) {
                // get the type from the second part
                String type = idParts[1];
                // the available system values are specified above getDataOptions in designer.js
                if ("app id".equals(type)) {
                    // whether rapid mobile is present
                    return application.getId();
                } else if ("app version".equals(type)) {
                    // whether rapid mobile is present
                    return application.getVersion();
                } else if ("page id".equals(type)) {
                    // the page
                    return _id;
                } else if ("mobile".equals(type)) {
                    // whether rapid mobile is present
                    return "false";
                } else if ("online".equals(type)) {
                    // whether we are online (presumed true if no rapid mobile)
                    return "true";
                } else if ("user".equals(type) || "user name".equals(idParts[1])) {
                    // pass the field as a value
                    return rapidRequest.getUserName();
                } else if ("field".equals(type)) {
                    // pass the field as a value
                    return value.getField();
                } else {
                    // pass through as literal 
                    return idParts[1];
                }
            } else {
                // return null
                return null;
            }
        } else if (idParts[0].equals("Session")) {
            // if there are enough if parts
            if (idParts.length > 1) {
                return (String) rapidRequest.getSessionAttribute(idParts[1]);
            } else {
                return null;
            }
        } else {
            // get the id of the value object (should be a control Id)
            String valueId = value.getId();
            // retrieve and return it from the form adapater, but not if it's hidden
            return formAdapter.getFormControlValue(rapidRequest, formId, valueId, true);
        }
    }

    // return a boolean for page visibility
    public boolean isVisible(RapidRequest rapidRequest, Application application, UserFormDetails userFormDetails)
            throws Exception {

        // get a logger
        Logger logger = rapidRequest.getRapidServlet().getLogger();

        // get the form adapter
        FormAdapter formAdapter = application.getFormAdapter();

        // if we have a form adapter and visibility conditions
        if (formAdapter == null) {

            // no form adapter always visible
            logger.debug("Page " + _id + " no form adapter, always visibe ");

            return true;

        } else {

            if (userFormDetails == null) {

                // no user form details
                logger.debug("No user form details");

                return false;

            } else if (_simple) {

                // simple page always invisble on forms
                logger.debug("Page " + _id + " is a simple page, always hidden on forms");

                return false;

            } else if (_formPageType == FORM_PAGE_TYPE_SUBMITTED && !userFormDetails.getShowSubmitPage()) {

                // requests for sumbitted page are denied if show submission is not true
                logger.debug("Page " + _id + " is a submitted page but the form has not been submitted yet");

                return false;

            } else if (_formPageType == FORM_PAGE_TYPE_ERROR && !userFormDetails.getError()) {

                // requests for sumbiited page are denied if not submitted
                logger.debug("Page " + _id + " is an error page but the form has not had an error yet");

                return false;

            } else if (_formPageType == FORM_PAGE_TYPE_SAVED && !userFormDetails.getSaved()) {

                // requests for submitted page are denied if not submitted
                logger.debug("Page " + _id + " is a saved page but the form has not been saved yet");

                return false;

            } else if (_visibilityConditions == null) {

                // no _visibilityConditions always visible
                logger.debug("Page " + _id + " _visibilityConditions is null, always visible on forms");

                return true;

            } else if (_visibilityConditions.size() == 0) {

                // no _visibilityConditions always visible
                logger.debug("Page " + _id + " _visibilityConditions size is zero, always visible on forms");
                return true;

            } else {

                // log
                logger.trace("Page " + _id + " " + _visibilityConditions.size() + " visibility condition(s) "
                        + " : " + _conditionsType);

                // assume we have failed all conditions
                boolean pass = false;

                // loop them
                for (Condition condition : _visibilityConditions) {

                    // assume we have failed this condition
                    pass = false;

                    logger.trace("Page " + _id + " visibility condition " + " : " + condition);

                    String value1 = getConditionValue(rapidRequest, userFormDetails.getId(), formAdapter,
                            application, condition.getValue1());

                    logger.trace("Value 1 = " + value1);

                    String value2 = getConditionValue(rapidRequest, userFormDetails.getId(), formAdapter,
                            application, condition.getValue2());

                    logger.trace("Value 2 = " + value2);

                    String operation = condition.getOperation();

                    if (value1 == null)
                        value1 = "";
                    if (value2 == null)
                        value2 = "";

                    // pass is updated from false to true if conditions match
                    if ("==".equals(operation)) {
                        if (value1.equals(value2))
                            pass = true;
                    } else if ("!=".equals(operation)) {
                        if (!value1.equals(value2))
                            pass = true;
                    } else {
                        // the remaining conditions all work with numbers and must not be empty strings
                        if (value1.length() > 0 && value2.length() > 0) {
                            try {
                                // convert to floats
                                float num1 = Float.parseFloat(value1);
                                float num2 = Float.parseFloat(value2);
                                // check the conditions
                                if (">".equals(operation)) {
                                    if ((num1 > num2))
                                        pass = true;
                                } else if (">=".equals(operation)) {
                                    if ((num1 >= num2))
                                        pass = true;
                                } else if ("<".equals(operation)) {
                                    if ((num1 < num2))
                                        pass = true;
                                } else if ("<=".equals(operation)) {
                                    if ((num1 <= num2))
                                        pass = true;
                                }
                            } catch (Exception ex) {
                                // something went wrong - generally in the conversion - return false
                                logger.error("Error assessing page visibility page " + _id + " " + condition);
                            } // try                   
                        } // empty string check                                       
                    } // operation check               

                    // log
                    logger.debug("Visibility condition for page " + _id + " : " + value1 + " "
                            + condition.getOperation() + " " + value2 + " , (" + condition + ") result is " + pass);

                    // for the fast fail check whether we have an or
                    if ("or".equals(_conditionsType)) {
                        // if the conditions are or and we've just passed, we can stop checking further as we've passed in total
                        if (pass)
                            break;
                    } else {
                        // if the conditions are and and we've just failed, we can stop checking further as we've failed in total
                        if (!pass)
                            break;
                    }

                } // condition loop

                // log result
                logger.debug("Page " + _id + " visibility check, " + _visibilityConditions.size()
                        + " conditions, pass = " + pass);

                // if we failed set the page values to null
                if (!pass)
                    formAdapter.setFormPageControlValues(rapidRequest, userFormDetails.getId(), _id, null);

                // return the pass
                return pass;

            } // simple, conditions, condition checks

        } // form adapter check

    }

    // overrides

    @Override
    public String toString() {
        return "Page " + _id + " " + _name + " - " + _title;
    }

    // static methods

    // static function to load a new page
    public static Page load(ServletContext servletContext, File file)
            throws JAXBException, ParserConfigurationException, SAXException, IOException,
            TransformerFactoryConfigurationError, TransformerException {

        // get the logger
        Logger logger = (Logger) servletContext.getAttribute("logger");

        // trace log that we're about to load a page
        logger.trace("Loading page from " + file);

        // open the xml file into a document
        Document pageDocument = XML.openDocument(file);

        // specify the xmlVersion as -1
        int xmlVersion = -1;

        // look for a version node
        Node xmlVersionNode = XML.getChildElement(pageDocument.getFirstChild(), "XMLVersion");

        // if we got one update the version
        if (xmlVersionNode != null)
            xmlVersion = Integer.parseInt(xmlVersionNode.getTextContent());

        // if the version of this xml isn't the same as this class we have some work to do!
        if (xmlVersion != XML_VERSION) {

            // get the page name
            String name = XML.getChildElementValue(pageDocument.getFirstChild(), "name");

            // log the difference
            logger.debug("Page " + name + " with version " + xmlVersion + ", current version is " + XML_VERSION);

            //
            // Here we would have code to update from known versions of the file to the current version
            //

            // check whether there was a version node in the file to start with
            if (xmlVersionNode == null) {
                // create the version node
                xmlVersionNode = pageDocument.createElement("XMLVersion");
                // add it to the root of the document
                pageDocument.getFirstChild().appendChild(xmlVersionNode);
            }

            // set the xml to the latest version
            xmlVersionNode.setTextContent(Integer.toString(XML_VERSION));

            //
            // Here we would use xpath to find all controls and run the Control.upgrade method
            //

            //
            // Here we would use xpath to find all actions, each class has it's own upgrade method so
            // we need to identify the class, instantiate it and call it's upgrade method
            // it's probably worthwhile maintaining a map of instantiated classes to avoid unnecessary re-instantiation   
            //

            // save it
            XML.saveDocument(pageDocument, file);

            logger.debug("Updated " + name + " page version to " + XML_VERSION);

        }

        // get the unmarshaller from the context
        Unmarshaller unmarshaller = RapidHttpServlet.getUnmarshaller();

        // get a buffered reader for our page with UTF-8 file format
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));

        // try the unmarshalling
        try {

            // unmarshall the page
            Page page = (Page) unmarshaller.unmarshal(br);

            // log that the page was loaded
            logger.debug("Loaded page " + page.getId() + " - " + page.getName() + " from " + file);

            // close the buffered reader
            br.close();

            // return the page      
            return page;

        } catch (JAXBException ex) {

            // close the buffered reader
            br.close();

            // log that the page had an error
            logger.error("Error loading page from " + file);

            // re-throw
            throw ex;

        }

    }

}