com.rapid.core.Application.java Source code

Java tutorial

Introduction

Here is the source code for com.rapid.core.Application.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.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
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.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.xpath.XPathExpressionException;

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.core.Page.Lock;
import com.rapid.core.Pages.PageHeader;
import com.rapid.data.ConnectionAdapter;
import com.rapid.forms.FormAdapter;
import com.rapid.forms.RapidFormAdapter;
import com.rapid.security.RapidSecurityAdapter;
import com.rapid.security.SecurityAdapter;
import com.rapid.security.SecurityAdapter.User;
import com.rapid.server.Rapid;
import com.rapid.server.RapidHttpServlet;
import com.rapid.server.RapidRequest;
import com.rapid.soa.Webservice;
import com.rapid.utils.Files;
import com.rapid.utils.JAXB.EncryptedXmlAdapter;
import com.rapid.utils.Minify;
import com.rapid.utils.Strings;
import com.rapid.utils.XML;
import com.rapid.utils.ZipFile;
import com.rapid.utils.ZipFile.ZipSource;
import com.rapid.utils.ZipFile.ZipSources;

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

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

    // application version statuses
    public static final int STATUS_DEVELOPMENT = 0;
    public static final int STATUS_LIVE = 1;
    public static final int STATUS_MAINTENANCE = 2;

    // the name of the folder in which to store backups
    public static final String BACKUP_FOLDER = "_backups";

    // public static classes

    // an exception class when loading
    public static class RapidLoadingException extends Exception {

        private static final long serialVersionUID = 5010L;

        private String _message;
        private Exception _exception;
        private Throwable _cause;

        public RapidLoadingException(String message, Exception ex) {
            _message = message;
            _cause = ex.getCause();
            if (ex.getMessage() != null)
                _message += " - " + ex.getMessage();
            if (ex.getCause() != null)
                _message += " - " + ex.getCause().getMessage();
            // clean up the jaxb suggestion
            if (_message.contains(". Did you mean")) {
                _message = _message.substring(0, _message.indexOf(". Did you mean"));
            }
        }

        @Override
        public String getMessage() {
            return _message;
        }

        @Override
        public StackTraceElement[] getStackTrace() {
            if (_exception == null) {
                return null;
            } else {
                return _exception.getStackTrace();
            }
        }

        @Override
        public Throwable getCause() {
            return _cause;
        }

    }

    // the details of a database connection (WebService is defined in its own class as its extendable)
    public static class DatabaseConnection {

        // instance variables

        String _name, _driverClass, _connectionString, _connectionAdapterClass, _userName, _password;
        ConnectionAdapter _connectionAdapter;

        // properties

        public String getName() {
            return _name;
        }

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

        public String getDriverClass() {
            return _driverClass;
        }

        public void setDriverClass(String driverClass) {
            _driverClass = driverClass;
        }

        public String getConnectionString() {
            return _connectionString;
        }

        public void setConnectionString(String connectionString) {
            _connectionString = connectionString;
        }

        public String getConnectionAdapterClass() {
            return _connectionAdapterClass;
        }

        public void setConnectionAdapterClass(String connectionAdapterClass) {
            _connectionAdapterClass = connectionAdapterClass;
        }

        public String getUserName() {
            return _userName;
        }

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

        @XmlJavaTypeAdapter(EncryptedXmlAdapter.class)
        public String getPassword() {
            return _password;
        }

        public void setPassword(String password) {
            _password = password;
        }

        // constructors
        public DatabaseConnection() {
        };

        public DatabaseConnection(ServletContext servletContext, Application application, String name,
                String driverClass, String connectionString, String connectionAdapterClass, String userName,
                String password) {
            _name = name;
            _driverClass = driverClass;
            _connectionString = application.insertParameters(servletContext, connectionString);
            _connectionAdapterClass = connectionAdapterClass;
            _userName = userName;
            _password = password;
        }

        // instance methods

        // get the connection adapter, instantiating only if null as this is quite expensive
        public synchronized ConnectionAdapter getConnectionAdapter(ServletContext servletContext,
                Application application)
                throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException,
                InstantiationException, IllegalAccessException, InvocationTargetException {

            // only if the connection adapter has not already been initialised
            if (_connectionAdapter == null) {
                // get our class
                Class classClass = Class.forName(_connectionAdapterClass);
                // initialise a constructor
                Constructor constructor = classClass.getConstructor(ServletContext.class, String.class,
                        String.class, String.class, String.class);
                // initialise the class
                _connectionAdapter = (ConnectionAdapter) constructor.newInstance(servletContext, _driverClass,
                        application.insertParameters(servletContext, _connectionString), _userName, _password);
            }

            return _connectionAdapter;

        }

        // set the connection adapter to null to for it to be re-initialised
        public synchronized void reset() throws SQLException {
            // close it first
            close();
            // set it to null
            _connectionAdapter = null;
        }

        // set the connection adapter to null to for it to be re-initialised
        public synchronized void close() throws SQLException {
            if (_connectionAdapter != null)
                _connectionAdapter.close();
        }

    }

    // application parameters which will can access in some of our actions
    @XmlType(namespace = "http://rapid-is.co.uk/core")
    public static class Value {

        // private instance variables      

        private String _text, _value;

        // properties      

        public String getText() {
            return _text;
        }

        public void setText(String text) {
            _text = text;
        }

        public String getValue() {
            return _value;
        }

        public void setValue(String value) {
            _value = value;
        }

        // constructors

        public Value() {
        }

        public Value(String text) {
            _text = text;
        }

        public Value(String text, String value) {
            _text = text;
            _value = value;
        }

        // overrides

        @Override
        public String toString() {
            if (_value == null) {
                return _text;
            } else {
                return _text + " (" + _value + ")";
            }
        }

    }

    // a value list of which there will be many - we can't extend list as jaxb does not handle list of list
    public static class ValueList {

        // private instance variables

        private String _name;
        private boolean _usesCodes;
        private List<Value> _values;

        // properties      

        public String getName() {
            return _name;
        }

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

        public boolean getUsesCodes() {
            return _usesCodes;
        }

        public void setUsesCodes(boolean usesCodes) {
            _usesCodes = usesCodes;
        }

        public List<Value> getValues() {
            return _values;
        }

        public void setValues(List<Value> values) {
            _values = values;
        }

        // constructors

        public ValueList() {
        }

        public ValueList(String name, boolean usesCodes) {
            _name = name;
            _usesCodes = usesCodes;
        }

        // overrides

        @Override
        public String toString() {
            String s = _name;
            if (_values == null) {
                s += "  values is null";
            } else {
                s += "  " + _values.size() + " values";
            }
            return s + " " + (_usesCodes ? "uses codes" : "no codes");
        }

    }

    // application parameters which will can access in some of our actions
    @XmlType(namespace = "http://rapid-is.co.uk/core")
    public static class Parameter {

        // private instance variables

        private String _name, _value;

        // properties

        public String getName() {
            return _name;
        }

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

        public String getValue() {
            return _value;
        }

        public void setValue(String value) {
            _value = value;
        }

        // constructor
        public Parameter() {
            _name = "";
            _value = "";
        }

    }

    // application and page backups
    public static class Backup {

        private String _id, _name, _user, _size;
        private Date _date;

        public String getId() {
            return _id;
        }

        public String getName() {
            return _name;
        }

        public Date getDate() {
            return _date;
        }

        public String getUser() {
            return _user;
        }

        public String getSize() {
            return _size;
        }

        public Backup(String id, Date date, String user, String size) {
            _id = id;
            _date = date;
            _user = user;
            _size = size;
        }

        public Backup(String id, String name, Date date, String user, String size) {
            _id = id;
            _name = name;
            _date = date;
            _user = user;
            _size = size;
        }

    }

    // the resource dependency is the control or action dependent on the resource
    public static class ResourceDependency {

        // the types that require resources
        public static final int RAPID = 0;
        public static final int ACTION = 1;
        public static final int CONTROL = 2;
        public static final int THEME = 3;

        // private instance variables
        private int _typeClass;
        private String _type;

        // properties
        public int getTypeClass() {
            return _typeClass;
        }

        public void setTypeClass(int typeClass) {
            _typeClass = typeClass;
        }

        public String getType() {
            return _type;
        }

        public void setType(String type) {
            _type = type;
        }

        // constructors
        public ResourceDependency() {
        };

        public ResourceDependency(int typeClass) {
            _typeClass = typeClass;
        }

        public ResourceDependency(int typeClass, String type) {
            _typeClass = typeClass;
            _type = type;
        }

    }

    // the resource is specified in the action, control, or theme xml files
    public static class Resource {

        // these are the types defined in the control and action .xsd files
        public static final int JAVASCRIPT = 1;
        public static final int CSS = 2;
        public static final int JAVASCRIPTFILE = 3;
        public static final int CSSFILE = 4;
        public static final int JAVASCRIPTLINK = 5; // links are not minified
        public static final int CSSLINK = 6; // links are not minified
        public static final int FILE = 7;

        // source types
        public static final int ACTION = 101;
        public static final int CONTROL = 102;

        // private instance variables
        private int _type;
        private String _name, _content;
        private List<ResourceDependency> _dependancies;

        // properties
        public String getName() {
            return _name;
        }

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

        public int getType() {
            return _type;
        }

        public void setType(int type) {
            _type = type;
        }

        public String getContent() {
            return _content;
        }

        public void setContent(String content) {
            _content = content;
        }

        public List<ResourceDependency> getDependencies() {
            return _dependancies;
        }

        public void setDependencies(List<ResourceDependency> dependancies) {
            _dependancies = dependancies;
        }

        // constructors
        public Resource() {
        };

        public Resource(int type, String content, int dependencyTypeClass) {
            _type = type;
            _content = content;
            _dependancies = new ArrayList<ResourceDependency>();
            _dependancies.add(new ResourceDependency(dependencyTypeClass));
        }

        public Resource(int type, String content, int dependencyTypeClass, String dependencyType) {
            _type = type;
            _content = content;
            _dependancies = new ArrayList<ResourceDependency>();
            _dependancies.add(new ResourceDependency(dependencyTypeClass, dependencyType));
        }

        public Resource(String name, int type, String content) {
            _name = name;
            _type = type;
            _content = content;
        }

        // methods
        public void addDependency(ResourceDependency dependency) {
            if (_dependancies == null)
                _dependancies = new ArrayList<ResourceDependency>();
            _dependancies.add(dependency);
        }

        // check for dependencies on a single type (usually Rapid)
        public boolean hasDependency(int typeClass) {
            // assume no dependency
            boolean hasDependency = false;
            // if there are some to check
            if (_dependancies != null) {
                // loop them
                for (ResourceDependency dependency : _dependancies) {
                    // check and return immediately
                    if (typeClass == dependency.getTypeClass())
                        return true;
                }
            }
            return hasDependency;
        }

        // check for dependencies for a type class and list of types 
        public boolean hasDependency(int typeClass, List<String> types) {
            // assume no dependency
            boolean hasDependency = false;
            // if there are some to check
            if (types != null && _dependancies != null) {
                // loop them
                for (ResourceDependency dependency : _dependancies) {
                    // check and return immediately
                    if (typeClass == dependency.getTypeClass() && types.contains(dependency.getType()))
                        return true;
                }
            }
            return hasDependency;
        }

    }

    // some overridden methods for the Resource collection
    public static class Resources extends ArrayList<Resource> {

        private static final long serialVersionUID = 1025L;

        @Override
        public boolean contains(Object o) {
            if (o.getClass() == Resource.class) {
                Resource r = (Resource) o;
                for (Resource resource : this) {
                    if (r.getType() == resource.getType() && r.getContent() == resource.getContent())
                        return true;
                }
            }
            return false;
        }

        @Override
        public boolean add(Resource resource) {
            if (contains(resource)) {
                return false;
            } else {
                return super.add(resource);
            }
        }

        @Override
        public void add(int index, Resource resource) {
            if (!contains(resource)) {
                super.add(index, resource);
            }
        }

        public void add(int type, String content, int dependencyTypeClass, String dependencyType) {
            // assume we can't find the resource
            Resource resource = null;
            // loop all resources
            for (Resource r : this) {
                // if we can match the type and content
                if (r.getType() == type && r.getContent() == content) {
                    // retain this resource
                    resource = r;
                    // we're done with this loop
                    break;
                }
            }
            // check for an existing resource
            if (resource == null) {
                // didn't find one so create
                resource = new Resource(type, content, dependencyTypeClass, dependencyType);
                // add to this collection
                this.add(resource);
                // if this is a theme resource
                if (dependencyTypeClass == ResourceDependency.THEME) {
                    // add rapid as a dependency so it appears in all pages
                    resource.addDependency(new ResourceDependency(ResourceDependency.RAPID, "rapid"));
                }
            } else {
                // add the dependency to the resource
                resource.addDependency(new ResourceDependency(dependencyTypeClass, dependencyType));
            }

        }

    }

    // instance variables   
    private int _xmlVersion, _status, _applicationBackupsMaxSize, _pageBackupsMaxSize;
    private String _id, _version, _name, _title, _description, _startPageId, _themeType, _styles, _statusBarColour,
            _statusBarHighlightColour, _statusBarTextColour, _statusBarIconColour, _functions, _securityAdapterType,
            _formAdapterType, _createdBy, _modifiedBy;
    private boolean _showConrolIds, _showActionIds, _deviceSecurity, _noRetainPassword;
    private Date _createdDate, _modifiedDate;
    private Map<String, Integer> _pageOrders;
    private SecurityAdapter _securityAdapter;
    private FormAdapter _formAdapter;
    private List<DatabaseConnection> _databaseConnections;
    private List<Webservice> _webservices;
    private List<ValueList> _valueLists;
    private List<Parameter> _parameters;
    private List<String> _controlTypes, _actionTypes;
    private Pages _pages;
    private Resources _appResources, _resources;
    private List<String> _styleClasses;
    private List<String> _pageVariables;

    // 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 produced by taking all unsafe characters out of the name)
    public String getId() {
        return _id;
    }

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

    // the version is used for Rapid Mobile's offline files to work with different published versions of the app
    public String getVersion() {
        return _version;
    }

    public void setVersion(String version) {
        _version = version;
    }

    // the status is used for Rapid Mobile's offline files to work with different published versions of the app
    public int getStatus() {
        return _status;
    }

    public void setStatus(int status) {
        _status = status;
    }

    // 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;
    }

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

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

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

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

    // the date this application 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 page orders if they are overridden
    public Map<String, Integer> getPageOrders() {
        return _pageOrders;
    }

    public void setPageOrders(Map<String, Integer> pageOrders) {
        _pageOrders = pageOrders;
        _pages.clearCachedOrder();
    }

    // whether control ids should be shown when designing this app
    public boolean getShowControlIds() {
        return _showConrolIds;
    }

    public void setShowControlIds(boolean showConrolIds) {
        _showConrolIds = showConrolIds;
    }

    // whether action ids should be shown when designing this app
    public boolean getShowActionIds() {
        return _showActionIds;
    }

    public void setShowActionIds(boolean showActionIds) {
        _showActionIds = showActionIds;
    }

    // the application start page which will be supplied if no page is explicitly provided
    public String getStartPageId() {
        return _startPageId;
    }

    public void setStartPageId(String startPageId) {
        _startPageId = startPageId;
    }

    // the CSS theme type which we'll look up and add to the rapid.css file
    public String getThemeType() {
        return _themeType;
    }

    public void setThemeType(String themeType) {
        _themeType = themeType;
    }

    // the CSS styles added to the generated application rapid.css file
    public String getStyles() {
        return _styles;
    }

    public void setStyles(String styles) {
        _styles = styles;
    }

    // colour of the status bar in Rapid Mobile
    public String getStatusBarColour() {
        return _statusBarColour;
    }

    public void setStatusBarColour(String statusBarColour) {
        _statusBarColour = statusBarColour;
    }

    // colour of the status bar highlight in Rapid Mobile
    public String getStatusBarHighlightColour() {
        return _statusBarHighlightColour;
    }

    public void setStatusBarHighlightColour(String statusBarHighlightColour) {
        _statusBarHighlightColour = statusBarHighlightColour;
    }

    // colour of the status bar text in Rapid Mobile
    public String getStatusBarTextColour() {
        return _statusBarTextColour;
    }

    public void setStatusBarTextColour(String statusBarTextColour) {
        _statusBarTextColour = statusBarTextColour;
    }

    // colour of icons in Rapid Mobile
    public String getStatusBarIconColour() {
        return _statusBarIconColour;
    }

    public void setStatusBarIconColour(String statusBarIconColour) {
        _statusBarIconColour = statusBarIconColour;
    }

    // the JavaScript functions added to the generated application rapid.js file (this has been replaced by application resources)
    public String getFunctions() {
        return _functions;
    }

    public void setFunctions(String functions) {
        _functions = functions;
    }

    // a collection of database connections used via the connection adapter class to produce database connections 
    public List<DatabaseConnection> getDatabaseConnections() {
        return _databaseConnections;
    }

    public void setDatabaseConnections(List<DatabaseConnection> databaseConnections) {
        _databaseConnections = databaseConnections;
    }

    // a collection of webservices for this application
    public List<Webservice> getWebservices() {
        return _webservices;
    }

    public void setWebservices(List<Webservice> webservices) {
        _webservices = webservices;
    }

    // the type name of the security adapter this application uses
    public String getSecurityAdapterType() {
        return _securityAdapterType;
    }

    public void setSecurityAdapterType(String securityAdapterType) {
        _securityAdapterType = securityAdapterType;
    }

    // whether to apply device security to this application
    public boolean getDeviceSecurity() {
        return _deviceSecurity;
    }

    public void setDeviceSecurity(boolean deviceSecurity) {
        _deviceSecurity = deviceSecurity;
    }

    // whether to not retain the password in Rapid Mobile - note that it's in the negative
    public boolean getNoRetainPassword() {
        return _noRetainPassword;
    }

    public void setNoRetainPassword(boolean noRetainPassword) {
        _noRetainPassword = noRetainPassword;
    }

    // the type name of the form adapter this application uses (if any)
    public String getFormAdapterType() {
        return _formAdapterType;
    }

    public void setFormAdapterType(String formAdapterType) {
        _formAdapterType = formAdapterType;
    }

    // a collection of parameters for this application
    public List<ValueList> getValueLists() {
        return _valueLists;
    }

    public void setValueLists(List<ValueList> valueLists) {
        _valueLists = valueLists;
    }

    // a collection of parameters for this application
    public List<Parameter> getParameters() {
        return _parameters;
    }

    public void setParameters(List<Parameter> parameters) {
        _parameters = parameters;
    }

    // control types used in this application
    public List<String> getControlTypes() {
        return _controlTypes;
    }

    public void setControlTypes(List<String> controlTypes) {
        _controlTypes = controlTypes;
    }

    // control types used in this application
    public List<String> getActionTypes() {
        return _actionTypes;
    }

    public void setActionTypes(List<String> actionTypes) {
        _actionTypes = actionTypes;
    }

    // number of application backups to keep
    public int getApplicationBackupsMaxSize() {
        return _applicationBackupsMaxSize;
    }

    public void setApplicationBackupMaxSize(int applicationBackupsMaxSize) {
        _applicationBackupsMaxSize = applicationBackupsMaxSize;
    }

    // number of page backups to keep
    public int getPageBackupsMaxSize() {
        return _pageBackupsMaxSize;
    }

    public void setPageBackupsMaxSize(int pageBackupsMaxSize) {
        _pageBackupsMaxSize = pageBackupsMaxSize;
    }

    // these are app resources which are marshalled to the application.xml file and add to all pages
    public Resources getAppResources() {
        return _appResources;
    }

    public void setAppResources(Resources appResources) {
        _appResources = appResources;
    }

    // constructors

    public Application() throws ParserConfigurationException, XPathExpressionException, RapidLoadingException,
            SAXException, IOException {
        _xmlVersion = XML_VERSION;
        _pages = new Pages(this);
        _pageOrders = new HashMap<String, Integer>();
        _statusBarColour = "#aaaaaa";
        _statusBarHighlightColour = "#999999";
        _statusBarTextColour = "#ffffff";
        _statusBarIconColour = "white";
        _databaseConnections = new ArrayList<DatabaseConnection>();
        _webservices = new ArrayList<Webservice>();
        _parameters = new ArrayList<Parameter>();
        _applicationBackupsMaxSize = 3;
        _pageBackupsMaxSize = 3;
    };

    // instance methods

    // this is where the application configuration will be stored
    public String getConfigFolder(ServletContext servletContext) {
        return getConfigFolder(servletContext, _id, _version);
    }

    // this is the web folder with the full system path
    public String getWebFolder(ServletContext servletContext) {
        return getWebFolder(servletContext, _id, _version);
    }

    // this is the backup folder
    public String getBackupFolder(ServletContext servletContext, boolean allVersions) {
        return getBackupFolder(servletContext, _id, _version, allVersions);
    }

    // this replaces [[xxx]] in a string where xxx is a known system or application parameter
    public String insertParameters(ServletContext servletContext, String string) {
        // check for non-null
        if (string != null) {
            // get pos of [[
            int pos = string.indexOf("[[");
            // check string contains [[ 
            if (pos > -1) {
                // if it has ]] thereafter
                if (string.indexOf("]]") > pos) {
                    // webfolder is the client web facing resources
                    if (string.contains("[[webfolder]]"))
                        string = string.replace("[[webfolder]]", getWebFolder(this));
                    // appfolder and configfolder are the hidden server app resources
                    if (string.contains("[[appfolder]]"))
                        string = string.replace("[[appfolder]]", getConfigFolder(servletContext, _id, _version));
                    if (string.contains("[[configfolder]]"))
                        string = string.replace("[[configfolder]]", getConfigFolder(servletContext, _id, _version));
                    // root folder is WEB-INF
                    if (string.contains("[[rootfolder]]"))
                        string = string.replace("[[rootfolder]]", servletContext.getRealPath("WEB-INF/"));
                    // if we have parameters
                    if (_parameters != null) {
                        // loop them
                        for (Parameter parameter : _parameters) {
                            // define the match string
                            String matchString = "[[" + parameter.getName() + "]]";
                            // if the match string is present replace it with the value
                            if (string.contains(matchString))
                                string = string.replace(matchString, parameter.getValue());
                        }
                    }
                }
            }
        }
        // return it
        return string;
    }

    // used by the function below if no specified start page or if the specified one can't be found
    private Page getStartPageUsingOrder(ServletContext servletContext) throws RapidLoadingException {
        if (_pages.size() > 0) {
            // get the id of the first page alphabetically
            String firstPageId = _pages.getSortedPages().get(0).getId();
            // get this page
            return _pages.getPage(servletContext, firstPageId);
        } else {
            return null;
        }
    }

    // get the first page the users want to see (set in Rapid Admin on first save)
    public Page getStartPage(ServletContext servletContext) throws RapidLoadingException {
        // retain an instance to the page we are about to return
        Page startPage = null;
        // check whether we have a _startPageId set 
        if (_startPageId == null) {
            // get the first page by order
            startPage = getStartPageUsingOrder(servletContext);
        } else {
            // get the start page from the id
            startPage = _pages.getPage(servletContext, _startPageId);
            // if it's null the start page has probably been deleted without updating the application object
            if (startPage == null)
                startPage = getStartPageUsingOrder(servletContext);
        }
        // return
        return startPage;
    }

    // loop the collection of database connections looking for a named one
    public DatabaseConnection getDatabaseConnection(String name) {
        if (_databaseConnections != null) {
            for (DatabaseConnection databaseConnection : _databaseConnections)
                if (name.equals(databaseConnection.getName()))
                    return databaseConnection;
        }
        return null;
    }

    // add a single database connection 
    public void addDatabaseConnection(DatabaseConnection databaseConnection) {
        _databaseConnections.add(databaseConnection);
    }

    // remove a single database connection
    public void removeDatabaseConnection(String name) {
        DatabaseConnection databaseConnection = getDatabaseConnection(name);
        if (databaseConnection != null)
            _databaseConnections.remove(databaseConnection);
    }

    // get a parameter value by name
    public String getParameterValue(String parameterName) {
        // if there are parameters
        if (_parameters != null) {
            // loop them
            for (Parameter parameter : _parameters) {
                // check the name and return if match
                if (parameterName.equals(parameter.getName()))
                    return parameter.getValue();
            }
        }
        // return null if nothing found
        return null;
    }

    // get pages
    public Pages getPages() {
        return _pages;
    }

    // get a control by it's id
    public Control getControl(ServletContext servletContext, String id) {
        Control control = null;
        // check we have pages and an id
        if (_pages != null && id != null) {
            // if the id is not a zero length string
            if (id.length() > 0) {
                // split the id parts on the underscore
                String[] idParts = id.split("_");
                // get the first part into a page id
                String pageId = idParts[0];
                try {
                    // get the specified page
                    Page page = _pages.getPage(servletContext, pageId);
                    // check we got a page
                    if (page == null) {
                        // no page matching this control id prefix so just loop all pages
                        for (String loopPageId : _pages.getPageIds()) {
                            // fetch this page
                            page = _pages.getPage(servletContext, loopPageId);
                            // look for the control
                            control = page.getControl(id);
                            // if we found it return it!
                            if (control != null)
                                return control;
                        }
                    } else {
                        // look for the control in the page according to its prefix
                        control = page.getControl(id);
                        // return it if we found it!
                        if (control != null)
                            return control;
                    }
                } catch (Exception ex) {
                    // get the logger
                    Logger logger = (Logger) servletContext.getAttribute("logger");
                    // log this exception
                    logger.error("Error loading page when getting control", ex);
                }
            } // id length > 0 check               
        } // id and page non-null check
          // couldn't find it either in specified page, or all pages
        return null;
    }

    // get a webservice by it's id
    public Webservice getWebserviceById(String id) {
        if (_webservices != null) {
            for (Webservice webservice : _webservices) {
                if (id.equals(webservice.getId()))
                    return webservice;
            }
        }
        return null;
    }

    // get a webservice by it's name
    public Webservice getWebserviceByName(String name) {
        if (_webservices != null) {
            for (Webservice webservice : _webservices) {
                if (name.equals(webservice.getName()))
                    return webservice;
            }
        }
        return null;
    }

    // return the list of style classes
    public List<String> getStyleClasses() {
        return _styleClasses;
    }

    // return the list of page variables used in the application
    public List<String> getPageVariables(ServletContext servletContext) throws RapidLoadingException {
        // if not set yet
        if (_pageVariables == null) {
            // make the collection of pages
            _pageVariables = new ArrayList<String>();
            // loop the pages
            for (String pageId : _pages.getPageIds()) {
                // get the page
                Page page = _pages.getPage(servletContext, pageId);
                // get any variables
                List<String> pageVariables = page.getSessionVariables();
                // if we got some
                if (pageVariables != null) {
                    // loop them
                    for (String pageVariable : pageVariables) {
                        // add if we don't have already
                        if (!_pageVariables.contains(pageVariable))
                            _pageVariables.add(pageVariable);
                    }
                }
            }
        }
        return _pageVariables;
    }

    // different name from above to stop jaxb writing it to xml
    public void emptyPageVariables() {
        _pageVariables = null;
    }

    // an instance of the security adapter used by this object
    public SecurityAdapter getSecurityAdapter() {
        return _securityAdapter;
    }

    // set the security to a given type
    public void setSecurityAdapter(ServletContext servletContext, String securityAdapterType)
            throws IllegalArgumentException, InstantiationException, IllegalAccessException,
            InvocationTargetException, IOException {
        // set the security adaper type from the incoming parameter
        _securityAdapterType = securityAdapterType;
        // if it was null update to rapid
        if (_securityAdapterType == null)
            _securityAdapterType = "rapid";
        // get a map of the security adapter constructors
        HashMap<String, Constructor> constructors = (HashMap<String, Constructor>) servletContext
                .getAttribute("securityConstructors");
        // get the constructor for our type
        Constructor<SecurityAdapter> constructor = constructors.get(_securityAdapterType);
        // if we couldn't find a constructor for the specified type
        if (constructor == null) {
            // set the type to rapid
            _securityAdapterType = "rapid";
            // instantiate a rapid security adapter
            _securityAdapter = new RapidSecurityAdapter(servletContext, this);
        } else {
            // instantiate the specified security adapter
            _securityAdapter = constructor.newInstance(servletContext, this);
        }
    }

    // an instance of the form adapter used by this object
    public FormAdapter getFormAdapter() {
        return _formAdapter;
    }

    // set the security to a given type
    public void setFormAdapter(ServletContext servletContext, String formAdapterType)
            throws IllegalArgumentException, InstantiationException, IllegalAccessException,
            InvocationTargetException, IOException {
        // set the security adaper type from the incoming parameter
        _formAdapterType = formAdapterType;
        // if it was null
        if (_formAdapterType == null || "".equals(_formAdapterType)) {
            // clear the current one
            _formAdapter = null;
        } else {
            // get a map of the form adapter constructors
            HashMap<String, Constructor> constructors = (HashMap<String, Constructor>) servletContext
                    .getAttribute("formConstructors");
            // get the constructor for our type
            Constructor<FormAdapter> constructor = constructors.get(_formAdapterType);
            // if we got this constructor
            if (constructor != null) {
                // instantiate the specified form adapter
                _formAdapter = constructor.newInstance(servletContext, this);
            } else {
                // revert to rapid form adapter
                _formAdapter = new RapidFormAdapter(servletContext, this);
            }
        }
    }

    // this is a list of elements to go in the head section of the page for any resources the applications controls or actions may require
    public List<Resource> getResources() {
        return _resources;
    }

    // scan the css for classes
    private List<String> scanStyleClasses(String css, List<String> classes) {

        // only if we got something we can use
        if (css != null) {

            // find the first .
            int startPos = css.indexOf(".");

            // if we got one
            while (startPos >= 0) {

                // find the start of the next style
                int styleStartPos = css.indexOf("{", startPos);

                // find the end of the next style
                int styleEndPos = css.indexOf("}", startPos);

                // only if we are in front of a completed style and there is a . starting before the style 
                if (styleStartPos < styleEndPos && startPos < styleStartPos) {

                    // find the end of the class by the first space
                    int endPos = css.indexOf(" ", startPos);
                    // adjust back to the start of the class if we went past some how
                    if (styleStartPos < endPos)
                        endPos = styleStartPos;
                    // if it works out               
                    if (endPos > startPos) {
                        // fetch the class
                        String styleClass = css.substring(startPos + 1, endPos).trim();
                        // remove any closing brackets
                        if (styleClass.indexOf(")") > 0)
                            styleClass = styleClass.substring(0, styleClass.indexOf(")"));
                        // remove any colons
                        if (styleClass.indexOf(":") > 0)
                            styleClass = styleClass.substring(0, styleClass.indexOf(":"));
                        // check we don't have it already and add it if ok
                        if (!classes.contains(styleClass))
                            classes.add(styleClass);
                    }

                }

                // exit here if styleEndPos is going to cause problems
                if (styleEndPos == -1)
                    break;

                // find the next .
                startPos = css.indexOf(".", styleEndPos);

            }

        }

        // sort the classes into alphabetical order
        Collections.sort(classes);

        return classes;

    }

    // this adds resources from either a control or action, they are added to the resources collection for printing in the top of each page if they are files, or amended to the application .js or .css files
    private void addResources(JSONObject jsonObject, String jsonObjectType, StringBuilder js, StringBuilder css)
            throws JSONException {

        // look for a resources object
        JSONObject jsonResourcesObject = jsonObject.optJSONObject("resources");

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

            // get a name for the jsonObject
            String name = jsonObject.optString("name");

            // get the dependency type
            String dependencyType = jsonObject.getString("type");

            // use the override below
            addResources(jsonResourcesObject, jsonObjectType, name, dependencyType, js, css);

        }

    }

    // this adds resources from either a control or action, they are added to the resources collection for printing in the top of each page if they are files, or amended to the application .js or .css files
    private void addResources(JSONObject jsonResourcesObject, String jsonObjectType, String name,
            String dependencyType, StringBuilder js, StringBuilder css) throws JSONException {

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

            // get the resource into an array (which is how the jaxb is passed the json)
            JSONArray jsonResources = jsonResourcesObject.optJSONArray("resource");

            // if we didn't get an array this is probably a single item collection stored as an object
            if (jsonResources == null) {
                // get the resource as an object
                JSONObject jsonResource = jsonResourcesObject.optJSONObject("resource");
                // if we got something
                if (jsonResource != null) {
                    // make a proper array
                    jsonResources = new JSONArray();
                    // add the item
                    jsonResources.put(jsonResource);
                }
            }

            // check we have something
            if (jsonResources != null) {

                // assume this is a rapid resource
                int dependencyTypeClass = ResourceDependency.RAPID;
                // update if action
                if ("action".equals(jsonObjectType))
                    dependencyTypeClass = ResourceDependency.ACTION;
                // update if control
                if ("control".equals(jsonObjectType))
                    dependencyTypeClass = ResourceDependency.CONTROL;
                // update if theme
                if ("theme".equals(jsonObjectType))
                    dependencyTypeClass = ResourceDependency.THEME;

                // loop them
                for (int j = 0; j < jsonResources.length(); j++) {

                    // get a reference to this resource
                    JSONObject jsonResource = jsonResources.getJSONObject(j);
                    // get the type
                    String resourceType = jsonResource.getString("type");
                    // get the contens which is either a path, or the real stuff
                    String resourceContents = jsonResource.getString("contents").trim();
                    // define the comments name as the name 
                    String commentsName = name;
                    // safety check
                    if (commentsName == null)
                        commentsName = "";
                    // add the json object type and resource type
                    commentsName += " " + jsonObjectType + " " + resourceType;

                    // add as resources if they're files, or append the string builders (the app .js and .css are added as resources at the end)
                    if ("javascript".equals(resourceType)) {
                        js.append("\n/* " + commentsName + " resource JavaScript */\n\n" + resourceContents + "\n");
                    } else if ("css".equals(resourceType)) {
                        css.append("\n/* " + commentsName + " resource styles */\n\n" + resourceContents + "\n");
                    } else if ("javascriptFile".equals(resourceType)) {
                        _resources.add(Resource.JAVASCRIPTFILE, resourceContents, dependencyTypeClass,
                                dependencyType);
                    } else if ("cssFile".equals(resourceType)) {
                        _resources.add(Resource.CSSFILE, resourceContents, dependencyTypeClass, dependencyType);
                    } else if ("javascriptLink".equals(resourceType)) {
                        _resources.add(Resource.JAVASCRIPTLINK, resourceContents, dependencyTypeClass,
                                dependencyType);
                    } else if ("cssLink".equals(resourceType)) {
                        _resources.add(Resource.CSSLINK, resourceContents, dependencyTypeClass, dependencyType);
                    } else if ("file".equals(resourceType)) {
                        _resources.add(Resource.FILE, resourceContents, dependencyTypeClass, dependencyType);
                    }

                } // resource loop

            } // json resource check

        } // json resources check

    }

    // this function initialises the application when its first loaded, initialises the security adapter and builds the rapid.js and rapid.css files
    public void initialise(ServletContext servletContext, boolean createResources) throws JSONException,
            InstantiationException, IllegalAccessException, ClassNotFoundException, IllegalArgumentException,
            InvocationTargetException, SecurityException, NoSuchMethodException, IOException {

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

        // trace log that we're initialising
        logger.trace("Initialising application " + _name + "/" + _version);

        // initialise the security adapter 
        setSecurityAdapter(servletContext, _securityAdapterType);

        // initialise the form adapter 
        setFormAdapter(servletContext, _formAdapterType);

        // initialise the resource includes collection
        _resources = new Resources();

        // if there is any app JavaScript functions - this is for backwards compatibility as _functions have been moved to JavaScript resources
        if (_functions != null) {
            // initialise app resources if need be
            if (_appResources == null)
                _appResources = new Resources();
            // add _functions as JavaScript resource to top of list
            _appResources.add(0, new Resource("Application functions", 1, _functions));
            // remove the _functions
            _functions = null;
        }

        // if the created date is null set to today
        if (_createdDate == null)
            _createdDate = new Date();

        // when importing an application we need to initialise but don't want the resource folders made in the old applications name
        if (createResources) {

            // get the jsonControls
            JSONArray jsonControls = (JSONArray) servletContext.getAttribute("jsonControls");

            // get the jsonActions
            JSONArray jsonActions = (JSONArray) servletContext.getAttribute("jsonActions");

            // string builders for the different sections in our rapid.js file
            StringBuilder resourceJS = new StringBuilder();
            StringBuilder initJS = new StringBuilder();
            StringBuilder dataJS = new StringBuilder();
            StringBuilder actionJS = new StringBuilder();

            // string builder for our rapid.css file
            StringBuilder resourceCSS = new StringBuilder();

            // check controls
            if (jsonControls != null) {

                // check control types
                if (_controlTypes != null) {

                    // remove the page control (if it's there)
                    _controlTypes.remove("page");
                    // add it to the top of the list
                    _controlTypes.add(0, "page");

                    // collection of dependent controls that need adding
                    ArrayList<String> dependentControls = new ArrayList<String>();

                    // loop control types used by this application
                    for (String controlType : _controlTypes) {

                        // loop all available controls
                        for (int i = 0; i < jsonControls.length(); i++) {

                            // get the control
                            JSONObject jsonControl = jsonControls.getJSONObject(i);

                            // check if we're on the type we need
                            if (controlType.equals(jsonControl.optString("type"))) {

                                // look for any dependent control types
                                JSONObject dependantTypes = jsonControl.optJSONObject("dependentTypes");
                                // if we got some
                                if (dependantTypes != null) {
                                    // look for an array
                                    JSONArray dependantTypesArray = dependantTypes.optJSONArray("dependentType");
                                    // if we got one
                                    if (dependantTypesArray != null) {
                                        // loop the array
                                        for (int j = 0; j < dependantTypesArray.length(); j++) {
                                            String dependantType = dependantTypesArray.getString(j);
                                            if (!_controlTypes.contains(dependantType)
                                                    && !dependentControls.contains(dependantType))
                                                dependentControls.add(dependantType);
                                        }
                                    } else {
                                        // just use the object
                                        String dependantType = dependantTypes.getString("dependentType");
                                        if (!_controlTypes.contains(dependantType)
                                                && !dependentControls.contains(dependantType))
                                            dependentControls.add(dependantType);
                                    }
                                }

                                // we're done
                                break;
                            } // available control type check

                        } // available control types loop

                    } // application control types loop

                    // now add all of the dependent controls
                    _controlTypes.addAll(dependentControls);

                    // loop control types used by this application
                    for (String controlType : _controlTypes) {

                        // loop all available controls
                        for (int i = 0; i < jsonControls.length(); i++) {

                            // get the control
                            JSONObject jsonControl = jsonControls.getJSONObject(i);

                            // check if we're on the type we need
                            if (controlType.equals(jsonControl.optString("type"))) {

                                // add any resources (actions can have them too)
                                addResources(jsonControl, "control", resourceJS, resourceCSS);

                                // get any initJavaScript
                                String js = jsonControl.optString("initJavaScript", "");
                                // check we got some
                                if (js.length() > 0) {
                                    initJS.append("\nfunction Init_" + jsonControl.getString("type")
                                            + "(id, details) {\n");
                                    initJS.append("  " + js.trim().replace("\n", "\n  "));
                                    initJS.append("\n}\n");
                                }

                                // check for a getData method
                                String getDataFunction = jsonControl.optString("getDataFunction");
                                // if there was something
                                if (getDataFunction != null) {
                                    // clean and print! (if not an empty string)
                                    if (getDataFunction.trim().length() > 0)
                                        dataJS.append("\nfunction getData_" + controlType
                                                + "(ev, id, field, details) {\n  "
                                                + getDataFunction.trim().replace("\n", "\n  ") + "\n}\n");

                                }

                                // check for a setData method
                                String setDataFunction = jsonControl.optString("setDataJavaScript");
                                // if there was something
                                if (setDataFunction != null) {
                                    // clean and print! (if not an empty string)
                                    if (setDataFunction.trim().length() > 0)
                                        dataJS.append("\nfunction setData_" + controlType
                                                + "(ev, id, field, details, data, changeEvents) {\n  "
                                                + setDataFunction.trim().replace("\n", "\n  ") + "\n}\n");
                                }

                                // retrieve any runtimeProperties
                                JSONObject jsonRuntimePropertyCollection = jsonControl
                                        .optJSONObject("runtimeProperties");
                                // check we got some
                                if (jsonRuntimePropertyCollection != null) {

                                    // get the first one
                                    JSONObject jsonRuntimeProperty = jsonRuntimePropertyCollection
                                            .optJSONObject("runtimeProperty");
                                    // get an array
                                    JSONArray jsonRunTimeProperties = jsonRuntimePropertyCollection
                                            .optJSONArray("runtimeProperty");

                                    // initialise counters
                                    int index = 0;
                                    int count = 0;

                                    // if we got an array
                                    if (jsonRunTimeProperties != null) {
                                        // retain the first entry in the object
                                        jsonRuntimeProperty = jsonRunTimeProperties.getJSONObject(0);
                                        // retain the size
                                        count = jsonRunTimeProperties.length();
                                    }

                                    do {

                                        // get the type
                                        String type = jsonRuntimeProperty.getString("type");

                                        // get the get function
                                        String getFunction = jsonRuntimeProperty.optString("getPropertyFunction",
                                                null);
                                        // print the get function if there was one
                                        if (getFunction != null)
                                            dataJS.append("\nfunction getProperty_" + controlType + "_" + type
                                                    + "(ev, id, field, details) {\n  "
                                                    + getFunction.trim().replace("\n", "\n  ") + "\n}\n");

                                        // get the set function
                                        String setFunction = jsonRuntimeProperty.optString("setPropertyJavaScript",
                                                null);
                                        // print the get function if there was one
                                        if (setFunction != null)
                                            dataJS.append("\nfunction setProperty_" + controlType + "_" + type
                                                    + "(ev, id, field, details, data, changeEvents) {\n  "
                                                    + setFunction.trim().replace("\n", "\n  ") + "\n}\n");

                                        // increment index
                                        index++;

                                        // get the next one
                                        if (index < count)
                                            jsonRuntimeProperty = jsonRunTimeProperties.getJSONObject(index);

                                    } while (index < count);

                                }

                                // we're done with this jsonControl
                                break;
                            }

                        } // jsonControls loop

                    } // control types loop

                } // control types check

            } // jsonControls check

            // check  actions
            if (jsonActions != null) {

                // check action types
                if (_actionTypes != null) {

                    // collection of dependent controls that need adding
                    ArrayList<String> dependentActions = new ArrayList<String>();

                    // loop control types used by this application
                    for (String actionType : _actionTypes) {

                        // loop all available controls
                        for (int i = 0; i < jsonActions.length(); i++) {

                            // get the action
                            JSONObject jsonAction = jsonActions.getJSONObject(i);

                            // check if we're on the type we need
                            if (actionType.equals(jsonAction.optString("type"))) {

                                // look for any dependant control types
                                JSONObject dependantTypes = jsonAction.optJSONObject("dependentTypes");
                                // if we got some
                                if (dependantTypes != null) {
                                    // look for an array
                                    JSONArray dependantTypesArray = dependantTypes.optJSONArray("dependentType");
                                    // if we got one
                                    if (dependantTypesArray != null) {
                                        // loop the array
                                        for (int j = 0; j < dependantTypesArray.length(); j++) {
                                            String dependantType = dependantTypesArray.getString(j);
                                            if (!_actionTypes.contains(dependantType)
                                                    && !dependentActions.contains(dependantType))
                                                dependentActions.add(dependantType);
                                        }
                                    } else {
                                        // just use the object
                                        String dependantType = dependantTypes.getString("dependentType");
                                        if (!_actionTypes.contains(dependantType)
                                                && !dependentActions.contains(dependantType))
                                            dependentActions.add(dependantType);
                                    }
                                }

                                // we're done
                                break;
                            }

                        }

                    }

                    // now add all of the dependent controls
                    _controlTypes.addAll(dependentActions);

                    // loop action types used by this application
                    for (String actionType : _actionTypes) {

                        // loop jsonActions
                        for (int i = 0; i < jsonActions.length(); i++) {

                            // get action
                            JSONObject jsonAction = jsonActions.getJSONObject(i);

                            // check the action is the one we want
                            if (actionType.equals(jsonAction.optString("type"))) {

                                // add any resources (controls can have them too)
                                addResources(jsonAction, "action", resourceJS, resourceCSS);

                                // get action JavaScript
                                String js = jsonAction.optString("actionJavaScript");
                                // only produce rapid action is this is rapid app
                                if (js != null && ("rapid".equals(_id) || !"rapid".equals(actionType))) {
                                    // clean and print! (if not an empty string)
                                    if (js.trim().length() > 0)
                                        actionJS.append("\n" + js.trim() + "\n");
                                }

                                // move onto the next action type
                                break;
                            }

                        } // jsonActions loop

                    } // action types loop

                } // action types check

            } // jsonAction check

            // assume no theme css
            String themeCSS = null;
            // assume no theme name
            String themeName = null;

            // check the theme type
            if (_themeType != null) {
                // get the themes
                List<Theme> themes = (List<Theme>) servletContext.getAttribute("themes");
                // check we got some
                if (themes != null) {
                    // loop them
                    for (Theme theme : themes) {
                        // check type
                        if (_themeType.equals(theme.getType())) {
                            // retain the theme CSS
                            themeCSS = theme.getCSS();
                            // retain the name
                            themeName = theme.getName();
                            // get any resources
                            addResources(theme.getResources(), "theme", themeName, null, resourceJS, resourceCSS);
                            // we're done
                            break;
                        }
                    }
                }
            }

            // put the appResources at the end so they can be overrides
            if (_appResources != null) {
                for (Resource resource : _appResources) {
                    // create new resource based on this one (so that the dependancy doesn't get written back to the application.xml file)
                    Resource appResource = new Resource(resource.getType(), resource.getContent(),
                            ResourceDependency.RAPID);
                    // if the type is a file or link prefix with the application folder
                    switch (resource.getType()) {
                    case Resource.JAVASCRIPTFILE:
                    case Resource.CSSFILE:
                        // files are available on the local file system so we prefix with the webfolder
                        appResource.setContent(getWebFolder(this)
                                + (resource.getContent().startsWith("/") ? "" : "/") + resource.getContent());
                        break;
                    case Resource.JAVASCRIPTLINK:
                    case Resource.CSSLINK:
                        // links are not so go in as-is
                        appResource.setContent(resource.getContent());
                        break;
                    }
                    // add new resource based on this one but with Rapid dependency
                    _resources.add(appResource);
                }
            }

            // create folders to write the rapid.js file
            String applicationPath = getWebFolder(servletContext);
            File applicationFolder = new File(applicationPath);
            if (!applicationFolder.exists())
                applicationFolder.mkdirs();

            // write the rapid.js file
            FileOutputStream fos = new FileOutputStream(applicationPath + "/rapid.js");
            PrintStream ps = new PrintStream(fos);

            // write the rapid.min.js file
            FileOutputStream fosMin = new FileOutputStream(applicationPath + "/rapid.min.js");
            PrintWriter pw = new PrintWriter(fosMin);

            // file header
            ps.print(
                    "\n/* This file is auto-generated on application load and save - it is minified when the application status is live */\n");
            // check functions
            if (_functions != null) {
                if (_functions.length() > 0) {
                    // header (this is removed by minify)
                    ps.print("\n\n/* Application functions JavaScript */\n\n");
                    // insert params
                    String functionsParamsInserted = insertParameters(servletContext, _functions);
                    // print
                    ps.print(functionsParamsInserted);
                    // print minify 
                    Minify.toWriter(functionsParamsInserted, pw, Minify.JAVASCRIPT);
                }
            }
            // check resource js
            if (resourceJS.length() > 0) {
                // header
                ps.print("\n\n/* Control and Action resource JavaScript */\n\n");
                // insert params
                String resourceJSParamsInserted = insertParameters(servletContext, resourceJS.toString());
                // print
                ps.print(resourceJS.toString());
                // print minify 
                Minify.toWriter(resourceJSParamsInserted, pw, Minify.JAVASCRIPT);
            }
            // check init js
            if (initJS.length() > 0) {
                // header
                ps.print("\n\n/* Control initialisation methods */\n\n");
                // insert params
                String initJSParamsInserted = insertParameters(servletContext, initJS.toString());
                // print
                ps.print(initJS.toString());
                // print minify 
                Minify.toWriter(initJSParamsInserted, pw, Minify.JAVASCRIPT);
            }
            // check datajs
            if (dataJS.length() > 0) {
                // header
                ps.print("\n\n/* Control getData and setData methods */\n\n");
                // insert params
                String dataJSParamsInserted = insertParameters(servletContext, dataJS.toString());
                // print
                ps.print(dataJS.toString());
                // print minify 
                Minify.toWriter(dataJSParamsInserted, pw, Minify.JAVASCRIPT);
            }
            // check action js
            if (actionJS.length() > 0) {
                // header
                ps.print("\n\n/* Action methods */\n\n");
                // insert params
                String actionParamsInserted = insertParameters(servletContext, actionJS.toString());
                // print
                ps.print(actionJS.toString());
                // print minify 
                Minify.toWriter(actionParamsInserted, pw, Minify.JAVASCRIPT);
            }

            // close debug writer and stream
            ps.close();
            fos.close();
            // close min writer and stream
            pw.close();
            fosMin.close();

            // get the rapid CSS into a string and insert parameters
            String resourceCSSWithParams = insertParameters(servletContext, resourceCSS.toString());
            String appThemeCSSWithParams = insertParameters(servletContext, themeCSS);
            String appCSSWithParams = insertParameters(servletContext, _styles);

            // write the rapid.css file
            fos = new FileOutputStream(applicationPath + "/rapid.css");
            ps = new PrintStream(fos);
            ps.print(
                    "\n/* This file is auto-generated on application load and save - it is minified when the application status is live */\n\n");
            if (resourceCSSWithParams != null) {
                ps.print(resourceCSSWithParams.trim());
            }
            if (appThemeCSSWithParams != null) {
                ps.print("\n\n/* " + themeName + " theme styles */\n\n");
                ps.print(appThemeCSSWithParams.trim());
            }
            if (appCSSWithParams != null) {
                ps.print("\n\n/* Application styles */\n\n");
                ps.print(appCSSWithParams.trim());
            }
            ps.close();
            fos.close();

            // minify it to a rapid.min.css file
            Minify.toFile(resourceCSSWithParams + appCSSWithParams, applicationPath + "/rapid.min.css", Minify.CSS);

            // check the status
            if (_status == STATUS_LIVE) {
                // add the application js min file as a resource
                _resources.add(new Resource(Resource.JAVASCRIPTFILE, getWebFolder(this) + "/rapid.min.js",
                        ResourceDependency.RAPID));
                // add the application css min file as a resource          
                _resources.add(new Resource(Resource.CSSFILE, getWebFolder(this) + "/rapid.min.css",
                        ResourceDependency.RAPID));
            } else {
                // add the application js file as a resource
                _resources.add(new Resource(Resource.JAVASCRIPTFILE, getWebFolder(this) + "/rapid.js",
                        ResourceDependency.RAPID));
                // add the application css file as a resource          
                _resources.add(new Resource(Resource.CSSFILE, getWebFolder(this) + "/rapid.css",
                        ResourceDependency.RAPID));
            }

            // loop all resources and minify js and css files
            for (Resource resource : _resources) {
                // get the content (which is the filename)
                String fileName = resource.getContent();
                // only interested in js and css files
                switch (resource.getType()) {
                case Resource.JAVASCRIPTFILE:
                    // get a file for this
                    File jsFile = new File(
                            servletContext.getRealPath("/") + (fileName.startsWith("/") ? "" : "/") + fileName);
                    // if the file exists, and it's in the scripts folder and ends with .js
                    if (jsFile.exists() && fileName.startsWith("scripts/") && fileName.endsWith(".js")) {
                        // derive the min file name by modifying the start and end
                        String fileNameMin = "scripts_min/" + fileName.substring(8, fileName.length() - 3)
                                + ".min.js";
                        // get a file for minifying 
                        File jsFileMin = new File(servletContext.getRealPath("/") + "/" + fileNameMin);
                        // if this file does not exist
                        if (!jsFileMin.exists()) {
                            // make any dirs it may need
                            jsFileMin.getParentFile().mkdirs();
                            // minify to it
                            Minify.toFile(jsFile, jsFileMin, Minify.JAVASCRIPT);
                        }
                        // if this application is live, update the resource to the min file
                        if (_status == STATUS_LIVE)
                            resource.setContent(fileNameMin);
                    }
                    break;
                case Resource.CSSFILE:
                    // get a file for this
                    File cssFile = new File(
                            servletContext.getRealPath("/") + (fileName.startsWith("/") ? "" : "/") + fileName);
                    // if the file exists, and it's in the scripts folder and ends with .js
                    if (cssFile.exists() && fileName.startsWith("styles/") && fileName.endsWith(".css")) {
                        // derive the min file name by modifying the start and end
                        String fileNameMin = "styles_min/" + fileName.substring(7, fileName.length() - 4)
                                + ".min.css";
                        // get a file for minifying 
                        File cssFileMin = new File(servletContext.getRealPath("/") + "/" + fileNameMin);
                        // if this file does not exist
                        if (!cssFileMin.exists()) {
                            // make any dirs it may need
                            cssFileMin.getParentFile().mkdirs();
                            // minify to it
                            Minify.toFile(cssFile, cssFileMin, Minify.CSS);
                        }
                        // if this application is live, update the resource to the min file
                        if (_status == STATUS_LIVE)
                            resource.setContent(fileNameMin);
                    }
                    break;
                }

            } // loop resources

            // a list for all of the style classes we're going to send up with
            _styleClasses = new ArrayList<String>();

            // populate the list of style classes by scanning the global styles
            scanStyleClasses(_styles, _styleClasses);
            // and any theme
            scanStyleClasses(appThemeCSSWithParams, _styleClasses);

            // remove any that have the same name as controls
            if (jsonControls != null) {
                // loop them
                for (int i = 0; i < jsonControls.length(); i++) {
                    // remove any classes with this controls type
                    _styleClasses.remove(jsonControls.getJSONObject(i).getString("type"));
                }
            }

        } // create resources

        // empty the list of page variables so it's regenerated
        _pageVariables = null;

        // debug log that we initialised
        logger.debug(
                "Initialised application " + _name + "/" + _version + (createResources ? "" : " (no resources)"));

    }

    // remove any page locks for a given user
    public int removeUserPageLocks(ServletContext servletContext, String userName) throws RapidLoadingException {
        // assume no locks removed
        int locksRemoved = 0;
        // check there are pages
        if (_pages != null) {
            // loop them
            for (String pageId : _pages.getPageIds()) {
                // get the page
                Page page = _pages.getPage(pageId);
                // if the page is still in memory
                if (page != null) {
                    // get the page lock
                    Lock pageLock = page.getLock();
                    // if there was one
                    if (pageLock != null) {
                        // if it matches the user name remove the lock
                        if (userName.equals(pageLock.getUserName())) {
                            page.setLock(null);
                            locksRemoved++;
                        }
                    }
                }
            }
        }
        return locksRemoved;
    }

    public List<Backup> getApplicationBackups(RapidHttpServlet rapidServlet) throws JSONException {

        List<Backup> backups = new ArrayList<Backup>();

        File backupFolder = new File(getBackupFolder(rapidServlet.getServletContext(), false));

        if (backupFolder.exists()) {

            for (File backup : backupFolder.listFiles()) {

                String id = backup.getName();

                String[] nameParts = id.split("_");

                if (id.contains(_id) && nameParts.length >= 3) {

                    String size = Files.getSizeName(backup);

                    SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd HHmmss");

                    Date date = new Date();

                    try {
                        date = df.parse(nameParts[nameParts.length - 3] + " " + nameParts[nameParts.length - 2]);
                    } catch (ParseException ex) {
                        throw new JSONException(ex);
                    }

                    backups.add(new Backup(id, date, nameParts[nameParts.length - 1], size));

                } // name parts > 3

            } // file loop

            // sort the list by date
            Collections.sort(backups, new Comparator<Backup>() {
                @Override
                public int compare(Backup obj1, Backup obj2) {
                    if (obj1.getDate().before(obj2.getDate())) {
                        return -1;
                    } else {
                        return 1;
                    }
                }
            });

            // check if we have too many
            while (backups.size() > _applicationBackupsMaxSize) {
                // get the top backup folder into a file object
                backupFolder = new File(
                        getBackupFolder(rapidServlet.getServletContext(), false) + "/" + backups.get(0).getId());
                // delete it
                Files.deleteRecurring(backupFolder);
                // remove it
                backups.remove(0);
            }

        }

        return backups;

    }

    public List<Backup> getPageBackups(RapidHttpServlet rapidServlet) throws JSONException {

        List<Backup> backups = new ArrayList<Backup>();

        File backupFolder = new File(getBackupFolder(rapidServlet.getServletContext(), false));

        if (backupFolder.exists()) {

            for (File backup : backupFolder.listFiles()) {

                String fileName = backup.getName();

                if (fileName.endsWith(".page.xml")) {

                    String[] nameParts = fileName.split("_");

                    if (nameParts.length >= 3) {

                        String name = nameParts[0];

                        for (int i = 1; i < nameParts.length - 3; i++) {
                            name += "_" + nameParts[i];
                        }

                        String size = Files.getSizeName(backup);

                        SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd HHmmss");

                        Date date = new Date();

                        try {
                            date = df
                                    .parse(nameParts[nameParts.length - 3] + " " + nameParts[nameParts.length - 2]);
                        } catch (ParseException ex) {
                            throw new JSONException(ex);
                        }

                        String[] userParts = nameParts[nameParts.length - 1].split("\\.");

                        backups.add(new Backup(fileName, name, date, userParts[0], size));

                    } // name parts > 3

                } // ends .page.xml

            } // file loop

            // sort the list by date
            Collections.sort(backups, new Comparator<Backup>() {
                @Override
                public int compare(Backup obj1, Backup obj2) {
                    if (obj1.getDate().before(obj2.getDate())) {
                        return -1;
                    } else {
                        return 1;
                    }
                }
            });

            // create a map to count backups of each page
            Map<String, Integer> pageBackupCounts = new HashMap<String, Integer>();
            // loop all of the backups in reverse order
            for (int i = backups.size() - 1; i >= 0; i--) {
                // get the back up
                Backup pageBackup = backups.get(i);
                // assume no backups so far for this page
                int pageBackupCount = 0;
                // set the backup count if we have one
                if (pageBackupCounts.get(pageBackup.getName()) != null)
                    pageBackupCount = pageBackupCounts.get(pageBackup.getName());
                // increment the count
                pageBackupCount++;
                // check the size
                if (pageBackupCount > _pageBackupsMaxSize) {
                    // get the backup into a file object
                    File backupFile = new File(backupFolder.getAbsolutePath() + "/" + backups.get(i).getId());
                    // delete it
                    backupFile.delete();
                    // remove this backup
                    backups.remove(i);
                }
                // store the page backup count
                pageBackupCounts.put(pageBackup.getName(), pageBackupCount);
            }

        }

        return backups;

    }

    public void backup(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, boolean allVersions)
            throws IOException {

        // get the username
        String userName = rapidRequest.getUserName();
        if (userName == null)
            userName = "unknown";

        // get the current date and time in a string
        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String dateString = formatter.format(new Date());

        // create a fileName for the archive
        String fileName = _id + _version + "_" + dateString + "_" + Files.safeName(userName);

        // create folders to backup the app
        String backupPath = getBackupFolder(rapidServlet.getServletContext(), allVersions) + "/" + fileName;
        File backupFolder = new File(backupPath);
        if (!backupFolder.exists())
            backupFolder.mkdirs();

        // create a file object for the application data folder
        File appFolder = new File(getConfigFolder(rapidServlet.getServletContext()));

        // create a list of files to ignore
        List<String> ignoreFiles = new ArrayList<String>();
        ignoreFiles.add(BACKUP_FOLDER);

        // copy the existing files and folders to the backup folder    
        Files.copyFolder(appFolder, backupFolder, ignoreFiles);

        // create a file object and folders for the web folder archive
        backupFolder = new File(backupPath + "/WebContent");
        if (!backupFolder.exists())
            backupFolder.mkdirs();

        // create a file object for the application web folder
        appFolder = new File(getWebFolder(rapidServlet.getServletContext()));

        // copy the existing web content files and folders to the webcontent backup folder    
        Files.copyFolder(appFolder, backupFolder, ignoreFiles);

    }

    public Application copy(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, String newId,
            String newVersion, boolean backups, boolean delete) throws Exception {

        // retain the ServletContext
        ServletContext servletContext = rapidServlet.getServletContext();

        // load the app into a copy without initialising
        Application appCopy = Application.load(servletContext,
                new File(getConfigFolder(servletContext) + "/application.xml"), false);

        // update the copy id 
        appCopy.setId(newId);
        // update the copy version
        appCopy.setVersion(newVersion);
        // update the copy status to in developement
        appCopy.setStatus(Application.STATUS_DEVELOPMENT);
        // update the created date
        appCopy.setCreatedDate(new Date());

        // get the app copy folder
        File appCopyFolder = new File(appCopy.getConfigFolder(servletContext));
        // get a app web copy folder location
        File appWebCopyFolder = new File(appCopy.getWebFolder(servletContext));

        try {

            // save the copy to create the folder and, application.xml and page.xml files
            appCopy.save(rapidServlet, rapidRequest, false);

            // get the copy of the application.xml file
            File appCopyFile = new File(appCopyFolder + "/application.xml");
            // read the copy to a string
            String appCopyXML = Strings.getString(appCopyFile);
            // replace all app/version references
            appCopyXML = appCopyXML.replace("/" + _id + "/" + _version + "/", "/" + newId + "/" + newVersion + "/");
            // save it back
            FileWriter fs = new FileWriter(appCopyFile);
            fs.write(appCopyXML);
            fs.close();

            // look for a security.xml file
            File appSecurityFile = new File(getConfigFolder(servletContext) + "/security.xml");
            // if we have one, copy it
            if (appSecurityFile.exists())
                Files.copyFile(appSecurityFile, new File(appCopyFolder + "/security.xml"));

            // get the pages config folder
            File appPagesFolder = new File(getConfigFolder(servletContext) + "/pages");
            // check it exists
            if (appPagesFolder.exists()) {
                // the folder we are copying to
                File appPagesCopyFolder = new File(appCopyFolder + "/pages");
                // make the dirs
                appPagesCopyFolder.mkdirs();
                // loop the files
                for (File appCopyPageFile : appPagesFolder.listFiles()) {
                    // if this is a page.xml file
                    if (appCopyPageFile.getName().endsWith(".page.xml")) {
                        // read the copy to a string
                        String pageCopyXML = Strings.getString(appCopyPageFile);
                        // replace all app/version references
                        pageCopyXML = pageCopyXML
                                .replace("/" + _id + "/" + _version + "/", "/" + newId + "/" + newVersion + "/")
                                .replace("~?a=" + _id + "&amp;" + _version + "&amp;",
                                        "~?a=" + newId + "&amp;" + newVersion + "&amp;");
                        // get the page file
                        File pageFile = new File(appPagesCopyFolder + "/" + appCopyPageFile.getName());
                        // save it back to it's new location
                        Strings.saveString(pageCopyXML, pageFile);
                    }
                }
            }

            // get the web folder
            File appWebFolder = new File(getWebFolder(servletContext));
            // if it exists
            if (appWebFolder.exists()) {
                // copy everything
                Files.copyFolder(appWebFolder, appWebCopyFolder);
            }

            // if we want to copy the backups too
            if (backups) {
                // get the backups folder
                File appBackupFolder = new File(getBackupFolder(servletContext, false));
                // check it exists
                if (appBackupFolder.exists()) {
                    // create a folder to copy to
                    File appBackupCopyFolder = new File(appCopy.getBackupFolder(servletContext, false));
                    // make the dirs
                    appBackupCopyFolder.mkdirs();
                    // copy the folder
                    Files.copyFolder(appBackupFolder, appBackupCopyFolder);
                }
            }

            // reload the application with the new app and page references
            appCopy = Application.load(servletContext, appCopyFile);

            // add this one to the applications collection
            rapidServlet.getApplications().put(appCopy);

            // delete this one
            if (delete)
                delete(rapidServlet, rapidRequest, false);

            // return the copy
            return appCopy;

        } catch (Exception ex) {

            // remove the failed copy from the applications collection
            rapidServlet.getApplications().remove(appCopy);

            // delete copy folder
            Files.deleteRecurring(appCopyFolder);

            // delete copy web folder
            Files.deleteRecurring(appWebCopyFolder);

            // rethrow
            throw ex;

        }

    }

    public void save(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, boolean backup) throws JAXBException,
            IOException, IllegalArgumentException, SecurityException, JSONException, InstantiationException,
            IllegalAccessException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException {

        // create folders to save the app
        String folderPath = getConfigFolder(rapidServlet.getServletContext());
        File folder = new File(folderPath);
        if (!folder.exists())
            folder.mkdirs();

        // create a file object for the application
        File appFile = new File(folderPath + "/application.xml");
        // backup the app if it already exists
        if (appFile.exists() && backup)
            backup(rapidServlet, rapidRequest, false);

        // create a temp file for saving the application to
        File tempFile = new File(folderPath + "/application-saving.xml");

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

        // marshal the application object to the temp file
        FileOutputStream fos = new FileOutputStream(tempFile.getAbsolutePath());
        RapidHttpServlet.getMarshaller().marshal(this, fos);
        fos.close();

        // copy / overwrite the app file with the temp file                     
        Files.copyFile(tempFile, appFile);

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

        // put this application in the collection
        rapidServlet.getApplications().put(this);

        // initialise the application, rebuilding the resources
        initialise(rapidServlet.getServletContext(), true);

    }

    public void delete(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, boolean allVersions)
            throws JAXBException, IOException {

        // get the servlet context
        ServletContext servletContext = rapidServlet.getServletContext();

        // create a file object for the config folder
        File appFolder = new File(getConfigFolder(servletContext));
        // if this is all versions promote from version to app
        if (allVersions)
            appFolder = appFolder.getParentFile();

        // create a file object for the webcontent folder
        File webFolder = new File(getWebFolder(servletContext));
        // if this is all versions promote from version to app
        if (allVersions)
            webFolder = webFolder.getParentFile();

        // if the app folder exists
        if (appFolder.exists()) {
            // backup the application
            backup(rapidServlet, rapidRequest, allVersions);
            // delete the app folder
            Files.deleteRecurring(appFolder);
            // delete the web folder
            Files.deleteRecurring(webFolder);
        }

        // close the application
        close(servletContext);

        // remove this application from the collection
        rapidServlet.getApplications().remove(this);

    }

    // create a named .zip file for the app in the /temp folder   
    public void zip(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, User user, String fileName,
            boolean offlineUse) throws JAXBException, IOException, JSONException, RapidLoadingException {

        // create folders to save locate app file
        String folderPath = getConfigFolder(rapidServlet.getServletContext());

        // create a file object for the application
        File appFile = new File(folderPath + "/application.xml");

        // if the app file exists
        if (appFile.exists()) {

            // create a list of sources for our zip
            ZipSources zipSources = new ZipSources();

            // deleteable folder
            File deleteFolder = null;

            // create a file object for the webcontent folder
            File webFolder = new File(getWebFolder(rapidServlet.getServletContext()));

            // if for offlineUse
            if (offlineUse) {

                // loop the contents of the webFolder 
                for (File file : webFolder.listFiles()) {
                    // add this file to the zip using the web folder path
                    zipSources.add(file, getWebFolder(this));
                }

                // set the delete folder
                deleteFolder = new File(rapidServlet.getServletContext()
                        .getRealPath("/WEB-INF/temp/" + _id + "_" + rapidRequest.getUserName()));
                // create it
                deleteFolder.mkdirs();

                // make a details.txt file
                File detailsFile = new File(deleteFolder + "/details.txt");
                // get a file writer
                Writer fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(detailsFile), "UTF-8"));
                // write the details
                fw.write(_id + "\r\n" + Rapid.MOBILE_VERSION + " - " + _version + "\r\n" + _title + "\r\n");
                // lines 4 to 7 are for the status bar colours
                if (_statusBarColour != null)
                    fw.write(_statusBarColour + "\r\n");
                if (_statusBarHighlightColour != null)
                    fw.write(_statusBarHighlightColour + "\r\n");
                if (_statusBarTextColour != null)
                    fw.write(_statusBarTextColour + "\r\n");
                if (_statusBarIconColour != null)
                    fw.write(_statusBarIconColour + "\r\n");
                // close the file writer
                fw.close();
                // add the file to the zip with a root path
                zipSources.add(detailsFile, "");

                // check we have pages
                if (_pages != null) {
                    // loop them
                    for (PageHeader pageHeader : _pages.getSortedPages()) {
                        // get the page id
                        String pageId = pageHeader.getId();
                        // get a reference to the page
                        Page page = _pages.getPage(rapidServlet.getServletContext(), pageId);
                        // create a file for it in the delete folder
                        File pageFile = new File(deleteFolder + "/" + pageId + ".htm");
                        // create a file writer for it for now
                        fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pageFile), "UTF-8"));
                        // for now get a printWriter to write the page html
                        page.writeHtml(rapidServlet, null, rapidRequest, this, user, fw, false);
                        // close it
                        fw.close();
                        // add the file to the zip with a root path
                        zipSources.add(pageFile, "");
                    }
                    // get the start page
                    Page page = getStartPage(rapidServlet.getServletContext());
                    // if we got one add it as index.htm
                    if (page != null) {
                        // create a file for it for it in the delete folder
                        File pageFile = new File(deleteFolder + "/" + "index.htm");
                        // create a file writer for it for now
                        fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pageFile), "UTF-8"));
                        // for now get a printWriter to write the page html
                        page.writeHtml(rapidServlet, null, rapidRequest, this, user, fw, false);
                        // close it
                        fw.close();
                        // add the file to the zip with a root path
                        zipSources.add(pageFile, "");
                    }

                }

                // check we have resources
                if (_resources != null) {
                    // loop them
                    for (Resource resource : _resources) {
                        // check they're any of our file types
                        if (resource.getType() == Resource.JAVASCRIPTFILE || resource.getType() == Resource.CSSFILE
                                || resource.getType() == Resource.FILE) {
                            // get a file object for them
                            File resourceFile = new File(
                                    rapidServlet.getServletContext().getRealPath("") + "/" + resource.getContent());
                            // get the path from the file name
                            String path = Files.getPath(resource.getContent());
                            // add as zip source
                            zipSources.add(resourceFile, path);
                        }
                    }
                }

            } else {

                // loop the contents of the webFolder and place in WebContent subfolder
                for (File file : webFolder.listFiles()) {
                    // add this file to the WEB-INF path
                    zipSources.add(new ZipSource(file, "WebContent"));
                }

                // create a file object for the application folder
                File appFolder = new File(folderPath);

                // loop the contents of the appFolder and place in WEB-INF subfolder
                for (File file : appFolder.listFiles()) {
                    // add this file to the WebContent path
                    zipSources.add(new ZipSource(file, "WEB-INF"));
                }

            }

            // get a file for the temp directory
            File tempDir = new File(rapidServlet.getServletContext().getRealPath("/WEB-INF/temp"));
            // create it if not there
            if (!tempDir.exists())
                tempDir.mkdir();

            // create the zip file object with our destination, always in the temp folder
            ZipFile zipFile = new ZipFile(
                    new File(rapidServlet.getServletContext().getRealPath("/WEB-INF/temp/" + fileName)));

            // create a list of files to ignore
            ArrayList<String> ignoreFiles = new ArrayList<String>();
            // don't include any files or folders from the back in the .zip
            ignoreFiles.add(BACKUP_FOLDER);

            // zip the sources into the file
            zipFile.zipFiles(zipSources, ignoreFiles);

            // loop the deleteable files
            if (deleteFolder != null) {
                try {
                    // delete the folder and all contents
                    Files.deleteRecurring(deleteFolder);
                } catch (Exception ex) {
                    // log exception
                    Logger.getLogger(this.getClass()).error("Error deleting temp file " + deleteFolder, ex);
                }

            }

        }

    }

    // an overload for the above which will include the for export rather than offlineUse files
    public void zip(RapidHttpServlet rapidServlet, RapidRequest rapidRequest, User user, String fileName)
            throws JAXBException, IOException, JSONException, RapidLoadingException {
        zip(rapidServlet, rapidRequest, user, fileName, false);
    }

    // close the database connections and form adapters before reload
    public void close(ServletContext servletContext) {
        // get the logger
        Logger logger = (Logger) servletContext.getAttribute("logger");
        // closing 
        logger.debug("Closing " + _id + "/" + _version + "...");
        // if we got some
        if (_databaseConnections != null) {
            // loop them
            for (DatabaseConnection databaseConnection : _databaseConnections) {
                // close database connection
                try {
                    // call the close method
                    databaseConnection.close();
                    // log
                    logger.debug("Closed " + databaseConnection.getName());
                } catch (SQLException ex) {
                    logger.error("Error closing database connection " + databaseConnection.getName() + " for " + _id
                            + "/" + _version, ex);
                }
            }
        }
        // close form adapter
        if (_formAdapter != null) {
            try {
                // call the close method
                _formAdapter.close();
                // log
                logger.debug("Closed form adapter");
            } catch (Exception ex) {
                logger.error("Error closing form adapter for " + _id + "/" + _version, ex);
            }
        }
    }

    // static methods

    // this is where the application configuration will be stored
    public static String getConfigFolder(ServletContext servletContext, String id, String version) {
        return servletContext.getRealPath("/WEB-INF/applications/" + id + "/" + version);
    }

    // this is the web folder with the full system path
    public static String getWebFolder(ServletContext servletContext, String id, String version) {
        return servletContext.getRealPath("/applications/" + id + "/" + version);
    }

    // this is the web folder as seen externally
    public static String getWebFolder(Application application) {
        return "applications/" + application.getId() + "/" + application.getVersion();
    }

    // this is the backup folder
    public static String getBackupFolder(ServletContext servletContext, String id, String version,
            boolean allVersions) {
        if (allVersions) {
            return servletContext.getRealPath("/WEB-INF/applications/" + BACKUP_FOLDER + "/" + id + "/" + version);
        } else {
            return servletContext.getRealPath("/WEB-INF/applications/" + id + "/" + version + "/" + BACKUP_FOLDER);
        }
    }

    // this is a simple overload for default loading of applications where the resources are all regenerated
    public static Application load(ServletContext servletContext, File file) throws JAXBException, JSONException,
            InstantiationException, IllegalAccessException, ClassNotFoundException, IllegalArgumentException,
            SecurityException, InvocationTargetException, NoSuchMethodException, IOException,
            ParserConfigurationException, SAXException, TransformerFactoryConfigurationError, TransformerException,
            RapidLoadingException, XPathExpressionException {
        return load(servletContext, file, true);
    }

    // this method loads the application by ummarshelling the xml, and then doing the same for all page .xmls, before calling the initialise method
    public static Application load(ServletContext servletContext, File file, boolean initialise)
            throws JAXBException, JSONException, InstantiationException, IllegalAccessException,
            ClassNotFoundException, IllegalArgumentException, SecurityException, InvocationTargetException,
            NoSuchMethodException, IOException, ParserConfigurationException, SAXException,
            TransformerFactoryConfigurationError, TransformerException, RapidLoadingException,
            XPathExpressionException {

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

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

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

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

        // look for a version node
        Node xmlVersionNode = XML.getChildElement(appDocument.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(appDocument.getFirstChild(), "name");

            // log the difference
            logger.debug("Application " + name + " with xml version " + xmlVersion + ", current xml 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 = appDocument.createElement("XMLVersion");
                // add it to the root of the document
                appDocument.getFirstChild().appendChild(xmlVersionNode);
            }

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

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

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

        }

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

        try {

            // unmarshall the application
            Application application = (Application) unmarshaller.unmarshal(file);

            // if we don't want pages loaded or resource generation skip this
            if (initialise) {

                // load the pages (actually clears down the pages collection and reloads the headers)
                application.getPages().loadpages(servletContext);

                // initialise the application and create the resources
                application.initialise(servletContext, true);

            }

            // log that the application was loaded
            logger.info("Loaded application " + application.getName() + "/" + application.getVersion()
                    + (initialise ? "" : " (no initialisation)"));

            return application;

        } catch (JAXBException ex) {

            throw new RapidLoadingException("Error loading application file at " + file, ex);

        }

    }

}