de.innovationgate.webgate.api.WGDatabase.java Source code

Java tutorial

Introduction

Here is the source code for de.innovationgate.webgate.api.WGDatabase.java

Source

/*******************************************************************************
 * Copyright 2009, 2010 Innovation Gate GmbH. All Rights Reserved.
 * 
 * This file is part of the OpenWGA server platform.
 * 
 * OpenWGA is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * In addition, a special exception is granted by the copyright holders
 * of OpenWGA called "OpenWGA plugin exception". You should have received
 * a copy of this exception along with OpenWGA in file COPYING.
 * If not, see <http://www.openwga.com/gpl-plugin-exception>.
 * 
 * OpenWGA is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with OpenWGA in file COPYING.
 * If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package de.innovationgate.webgate.api;

import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.text.Collator;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Semaphore;

import javax.activation.DataSource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.SortedBag;
import org.apache.commons.collections.bag.TreeBag;
import org.apache.commons.collections.map.ListOrderedMap;

import com.google.common.collect.MapMaker;

import de.innovationgate.utils.CertificateValidationUtils;
import de.innovationgate.utils.ConcurrentWeakHashMap;
import de.innovationgate.utils.NullPlaceHolder;
import de.innovationgate.utils.SkippingIterator;
import de.innovationgate.utils.SkippingIteratorWrapper;
import de.innovationgate.utils.UserHashMap;
import de.innovationgate.utils.UserHashMapGroup;
import de.innovationgate.utils.WGUtils;
import de.innovationgate.utils.cache.Cache;
import de.innovationgate.utils.cache.CacheDisposalListener;
import de.innovationgate.utils.cache.CacheException;
import de.innovationgate.utils.cache.CacheFactory;
import de.innovationgate.webgate.api.PageRightsFilter.Right;
import de.innovationgate.webgate.api.auth.AnonymousAuthSession;
import de.innovationgate.webgate.api.auth.AnonymousAwareAuthenticationModule;
import de.innovationgate.webgate.api.auth.AuthSessionWithUserCacheQualifier;
import de.innovationgate.webgate.api.auth.AuthenticationModule;
import de.innovationgate.webgate.api.auth.AuthenticationSession;
import de.innovationgate.webgate.api.auth.AuthenticationSourceListener;
import de.innovationgate.webgate.api.auth.BackendAuthSession;
import de.innovationgate.webgate.api.auth.CertAuthCapableAuthModule;
import de.innovationgate.webgate.api.auth.LabeledNamesProvider;
import de.innovationgate.webgate.api.auth.MasterLoginAuthSession;
import de.innovationgate.webgate.api.auth.RedirectionAuthModule;
import de.innovationgate.webgate.api.fake.WGFakeDatabase;
import de.innovationgate.webgate.api.fake.WGFakeDocument;
import de.innovationgate.webgate.api.fake.WGFakeLanguage;
import de.innovationgate.webgate.api.locking.Lock;
import de.innovationgate.webgate.api.locking.LockException;
import de.innovationgate.webgate.api.locking.LockManager;
import de.innovationgate.webgate.api.locking.LockOwner;
import de.innovationgate.webgate.api.locking.Lockable;
import de.innovationgate.webgate.api.locking.ResourceIsLockedException;
import de.innovationgate.webgate.api.schemadef.WGAreaDefinition;
import de.innovationgate.webgate.api.schemadef.WGContentItemDefinition;
import de.innovationgate.webgate.api.schemadef.WGContentTypeDefinition;
import de.innovationgate.webgate.api.schemadef.WGLanguageDefinition;
import de.innovationgate.webgate.api.schemadef.WGMetaFieldDefinition;
import de.innovationgate.webgate.api.schemadef.WGSchemaDefinition;
import de.innovationgate.webgate.api.schemadef.WGSchemaDocumentDefinition;
import de.innovationgate.webgate.api.servers.WGDatabaseServer;
import de.innovationgate.webgate.api.utils.MasterSessionTask;
import de.innovationgate.webgate.api.workflow.WGDefaultWorkflowEngine;
import de.innovationgate.webgate.api.workflow.WGWorkflow;
import de.innovationgate.webgate.api.workflow.WGWorkflowEngine;
import de.innovationgate.webgate.api.workflow.WGWorkflowEvent;
import de.innovationgate.webgate.api.workflow.WGWorkflowEventListener;
import de.innovationgate.wga.common.Constants;
import de.innovationgate.wga.common.beans.csconfig.v1.CSConfig;
import de.innovationgate.wga.common.beans.csconfig.v1.Version;
import de.innovationgate.wga.model.VersionCompliance;
import de.innovationgate.wga.modules.options.JSONListOptionType;
import de.innovationgate.wga.modules.options.OptionConversionException;

/**
 * <p>
 * Represents a WGA Database. Can be a content database, design database,
 * repository database, user profile database or a mixture of it. The Roles of a
 * database give info about it's contents. Can be retrieved via getRoles().
 * </p>
 * 
 * <h4>Usage patterns:</h4>
 * 
 * <p>
 * <b>Simple use: </b>
 * <p/>
 * <p>
 * This is recommended, when there are is only one stream of operation to be
 * done and the used databases will not be opened again for a time.
 * </p>
 * 
 * <code>
 * WGDatabase db = WGFactory.getInstance().openDatabase(dbtype, dbpath, username, password, options);<br/>
 * ... db actions ...<br/>
 * db.close();<br/>
 * </code>
 * <p>
 * After db.close() the database object must not be used further.
 * </p>
 * 
 * 
 * 
 * <p>
 * <b>Recurring and multithreaded use: </b>
 * </p>
 * <p>
 * This is recommended when the opened databases should be reused at later time
 * and/or will be used by multiple threads. This mode introduces the "database
 * session", that will open a WGDatabase object for usage and can be closed
 * without completely closing the database object and opened again later.
 * </p>
 * <code>
 * // Initial opening - Opens the database and implicitly a session too<br/>
 *  WGDatabase db = WGFactory.getInstance().openDatabase(dbtype, dbpath, username, password, options);<br/>
 *  ... db actions ...<br/>
 *  db.closeSession();<br/>
 * <br/>
 *  ...<br/>
 * <br/>
 *  // Recurring opening - maybe in another thread using the same db object<br/>
 *  db.openSession(username, password)<br/>
 *  ... db actions ...<br/>
 *  db.closeSession();<br/>
 * <br/>
 *  ...<br/>
 * <br/>
 *  // Final closing at the end of the program - No further WGA processing after that<br/>
 *  db.close();<br/>
 * </code>
 * <p>
 * This mode not only allows the WGAPI to keep database connections but also
 * allows for caching of field values.
 * </p>
 * 
 */
public class WGDatabase implements Lockable, WGDesignChangeListener, PageHierarchyNode,
        AuthenticationSourceListener, WGExtensionDataContainer {

    public static final String EXTDATA_DATABASE_UUID = "database_uuid";
    public static final String EXTDATA_PATCH_LEVEL = "ddlpatchlevel";
    public static final String EXTDATA_DERIVATE_CREATORS = "filederivates_creators";
    public static final String EXTDATA_DERIVATE_REVISION = "filederivates_lastupdaterevision";
    public static final String EXTDATA_CS_SYNC_RUNNING = "contentstore_syncing";

    /**
     * Extension data fields on the database which are no regular data and do not represent any database state and should be protected from data deletion operations 
     */
    public static final List<String> PROTECTED_DATABASE_EXTDATA = Arrays
            .asList(new String[] { EXTDATA_DATABASE_UUID, EXTDATA_PATCH_LEVEL });

    public static final int DOCUMENTCACHE_SIZE_DEFAULT = 5000;

    public static final int NAMECACHE_SIZE_DEFAULT = 5000;

    public static final int QUERYCACHE_SIZE_DEFAULT = 1000;

    /** Content store version identifying a database that is no content store at all */
    public static final double CSVERSION_NO_CONTENTSTORE = 0;

    /** Content store version identifying a content store used as of WGA version 3.0 */
    public static final double CSVERSION_WGA3 = 3;

    /** Content store version identifying a content store used as of WGA version 4.1, including optimized file handling */
    public static final double CSVERSION_WGA4_1 = 4.1;

    /** Content store version identifying a content store used as of WGA version 5 */
    public static final double CSVERSION_WGA5 = 5;

    /**
     * Name of the implicit "authenticated" group holding all users that were able to authenticate themselves
     */
    public static final String AUTHENTICATED_GROUP = "authenticated";

    public class ReconfigurationAccessor {

        public void reconfigureCore(boolean prepareOnly, WGUserAccess userAccess) throws WGAPIException {
            WGDatabase.this.reconfigureCore(prepareOnly, userAccess);
        }

    }

    public class WGRealTransaction implements WGTransaction {

        private boolean _open = true;
        private List<Callable<Object>> _afterCommitTasks = new ArrayList<Callable<Object>>();

        @Override
        public boolean commit() throws WGAPIException {

            getSessionContext().removeTransactionFromStack(this);
            _open = false;
            if (!doCommitTransaction()) {
                throw new WGBackendException("Transaction commit failed");
            }
            for (Callable<Object> r : _afterCommitTasks) {
                try {
                    r.call();
                } catch (Exception e) {
                    WGFactory.getLogger().error("Exception executing after-transaction operation", e);
                }
            }
            return true;

        }

        @Override
        public void rollback() {
            _open = false;
            try {
                if (!doRollbackTransaction()) {
                    throw new WGAPIException("Transaction rollback failed because of unknown cause");
                }
            } catch (Throwable t) {
                WGFactory.getLogger().error("Transaction rollback failed", t);
            } finally {
                try {
                    getSessionContext().removeTransactionFromStack(this);
                } catch (Throwable t) {
                    WGFactory.getLogger().error("Exception removing transaction from stack", t);
                }
            }
        }

        @Override
        public boolean isOpen() {
            return _open;
        }

        public void addAfterCommitTask(Callable r) {
            _afterCommitTasks.add(r);
        }

    }

    public class WGCascadedTransaction implements WGTransaction {

        private boolean _open = true;

        @Override
        public boolean commit() throws WGAPIException {
            _open = false;
            verboseCacheManagement("Committing cascaded transaction, no backup changes catchup. Stack: "
                    + WGUtils.serializeCollection(getSessionContext().getTransactionsStack(), " <- "));
            getSessionContext().removeTransactionFromStack(this);
            return false;
        }

        @Override
        public void rollback() {
            _open = false;
            try {
                if (!doRollbackTransaction()) {
                    throw new WGAPIException("Transaction rollback failed because of unknown cause");
                }
            } catch (Throwable t) {
                WGFactory.getLogger().error("Transaction rollback failed", t);
            } finally {
                try {
                    getSessionContext().removeTransactionFromStack(this);
                } catch (Throwable t) {
                    WGFactory.getLogger().error("Exception removing transaction from stack", t);
                }
            }
        }

        @Override
        public boolean isOpen() {
            return _open;
        }

    }

    public class WGFakeTransaction implements WGTransaction {

        private boolean _open = true;

        @Override
        public boolean commit() throws WGAPIException {
            _open = false;
            getSessionContext().removeTransactionFromStack(this);
            verboseCacheManagement("Committing fake transaction. No cache maintenance");
            return false;
        }

        @Override
        public void rollback() {
            _open = false;
            try {
                getSessionContext().removeTransactionFromStack(this);
            } catch (Throwable t) {
                WGFactory.getLogger().error("Exception removing transaction from stack", t);
            }

        }

        public boolean isOpen() {
            return _open;
        };

    }

    /**
     * Represents an ACL privilege for an ACL level
     */
    public static class AccessLevelPrivilege {

        public AccessLevelPrivilege(boolean defaultValue, boolean available) {
            super();
            this._defaultValue = defaultValue;
            this._available = available;
        }

        private boolean _defaultValue;
        private boolean _available;

        /**
         * Returns if the privilege is default
         */
        public boolean isDefaultValue() {
            return _defaultValue;
        }

        /**
         * Returns if the privilege is available
         */
        public boolean isAvailable() {
            return _available;
        }

    }

    /**
     * Represents an ACL Access Level
     */
    public static class AccessLevel {

        public static final String ACCESSLEVEL_GROUP_PREFIX = "accesslevel.";

        private int _code;
        private String _name;
        private AccessLevelPrivilege _allowMovingStructEntries;
        private AccessLevelPrivilege _allowDeletingDocuments;
        private AccessLevelPrivilege _allowDirectAccess;
        private boolean _usageInACL;

        private AccessLevel(int code, String name, boolean usageInACL,
                AccessLevelPrivilege allowMovingStructEntries, AccessLevelPrivilege allowDeletingDocuments,
                AccessLevelPrivilege allowUrlAccess) {
            _code = code;
            _name = name;

            _usageInACL = usageInACL;

            _allowMovingStructEntries = allowMovingStructEntries;
            _allowDeletingDocuments = allowDeletingDocuments;
            _allowDirectAccess = allowUrlAccess;
        }

        /**
         * Returns if users of this level have the privilege to move pages by default
         */
        public boolean isAllowMovingStructEntriesDefault() {
            return _allowMovingStructEntries.isDefaultValue();
        }

        /**
         * Returns if users of this level have the privilege to delete documents by default
         */
        public boolean isAllowDeletingDocumentsDefault() {
            return _allowDeletingDocuments.isDefaultValue();
        }

        /**
         * Returns if users of this level have the privilege for direct access by default
         */
        public boolean isAllowDirectAccessDefault() {
            return _allowDirectAccess.isDefaultValue();
        }

        /**
         * Returns if users of this level may have the privilege to move pages
         */
        public boolean isAllowMovingStructEntriesAvailable() {
            return _allowMovingStructEntries.isAvailable();
        }

        /**
         * Returns if users of this level may have the privilege to delete documents
         */
        public boolean isAllowDeletingDocumentsAvailable() {
            return _allowDeletingDocuments.isAvailable();
        }

        /**
         * Returns if users of this level may have the privilege for direct access
         */
        public boolean isAllowDirectAccessAvailable() {
            return _allowDirectAccess.isAvailable();
        }

        /**
         * Returns the numeric code
         */
        public int getCode() {
            return _code;
        }

        /**
         * Returns the name
         */
        public String getName() {
            return _name;
        }

        /**
         * Returns the name of the group containing all users having at least this access level
         */
        public String getGroupName() {

            return ACCESSLEVEL_GROUP_PREFIX + getName().toLowerCase();

        }

        /**
         * Returns if this level may be used for ACL entries
         */
        public boolean isUsageInACL() {
            return _usageInACL;
        }

    }

    private class DocumentCacheDisposalListener implements CacheDisposalListener {

        public void disposeAll() {

            // Just remove all document cores from session. Other cache cleanups will be done in WGDatabase.refresh(), which triggers this
            if (isSessionOpen()) {
                getSessionContext().dropAllDocumentCores(true);
            }
        }

        public void dispose(String key, Object value) {
            if (value instanceof WGDocument) {
                try {
                    ((WGDocument) value).dispose();
                } catch (WGAPIException e) {
                    WGFactory.getLogger().error("Exception disposing document object", e);
                }
            }

        }
    }

    public class DocumentCollectionHierarchy implements PageHierarchyNode {

        private int _type;

        public DocumentCollectionHierarchy(int type) {
            _type = type;
        }

        public Class<? extends WGDocument> getChildNodeType() {
            switch (_type) {
            case WGDocument.TYPE_AREA:
                return WGArea.class;

            case WGDocument.TYPE_LANGUAGE:
                return WGLanguage.class;

            case WGDocument.TYPE_CONTENTTYPE:
                return WGContentType.class;

            case WGDocument.TYPE_CSSJS:
                return WGScriptModule.class;

            case WGDocument.TYPE_TML:
                return WGTMLModule.class;

            case WGDocument.TYPE_FILECONTAINER:
                return WGFileContainer.class;

            default:
                throw new IllegalStateException("Unknown document type " + _type);

            }
        }

        public List<? extends PageHierarchyNode> getChildNodes() throws WGAPIException {
            return getDesignObjects(_type);
        }

        @Override
        public SkippingIterator<? extends PageHierarchyNode> getChildNodeIterator(int pageSize)
                throws WGAPIException {
            return new SkippingIteratorWrapper<PageHierarchyNode>(
                    (Iterator<PageHierarchyNode>) getChildNodes().iterator());
        }

        public String getNodeKey() throws WGAPIException {
            return WGDocument.doctypeNumberToName(_type);
        }

        public String getNodeTitle(String language) throws WGAPIException {

            switch (_type) {

            case WGDocument.TYPE_AREA:
                return "Areas";

            case WGDocument.TYPE_CONTENTTYPE:
                return "Content types";

            case WGDocument.TYPE_LANGUAGE:
                return "Languages";

            case WGDocument.TYPE_FILECONTAINER:
                return "File containers";

            case WGDocument.TYPE_TML:
                return "WebTML modules";

            case WGDocument.TYPE_CSSJS:
                return "Scripts";

            }

            return WGDocument.doctypeNumberToName(_type);
        }

        public PageHierarchyNode getParentNode() throws WGAPIException {
            return WGDatabase.this;
        }

    }

    public class AllDocumentsHierarchy {

        private ListOrderedMap _docCollections = new ListOrderedMap();

        public AllDocumentsHierarchy() {
            _docCollections.put(WGDocument.TYPE_AREA, new DocumentCollectionHierarchy(WGDocument.TYPE_AREA));
            _docCollections.put(WGDocument.TYPE_CONTENTTYPE,
                    new DocumentCollectionHierarchy(WGDocument.TYPE_CONTENTTYPE));
            _docCollections.put(WGDocument.TYPE_LANGUAGE,
                    new DocumentCollectionHierarchy(WGDocument.TYPE_LANGUAGE));
            _docCollections.put(WGDocument.TYPE_CSSJS, new DocumentCollectionHierarchy(WGDocument.TYPE_CSSJS));
            _docCollections.put(WGDocument.TYPE_TML, new DocumentCollectionHierarchy(WGDocument.TYPE_TML));
            _docCollections.put(WGDocument.TYPE_FILECONTAINER,
                    new DocumentCollectionHierarchy(WGDocument.TYPE_FILECONTAINER));
        }

        public DocumentCollectionHierarchy getCollectionForType(int type) {
            return (DocumentCollectionHierarchy) _docCollections.get(new Integer(type));
        }

        @SuppressWarnings("unchecked")
        public List<PageHierarchyNode> getChildNodes() throws WGAPIException {
            return _docCollections.valueList();
        }

    }

    private AllDocumentsHierarchy _allDocumentsHierarchy = new AllDocumentsHierarchy();

    /**
     * An interface defining an action that can be execute on certain database events.
     */
    public interface DatabaseAction {

        /**
         * The action to be ran
         * @param db The connected database.
         * @throws Exception
         */
        public void run(WGDatabase db) throws Exception;
    }

    /**
     * @Deprecated Use {@link DatabaseAction}. Just there for downward compatibility.
     */
    public interface ConnectAction extends DatabaseAction {

    }

    /**
     * Defines a behaviour for WGDatabases how they should respond to requests
     * for nonexistent items
     */
    public class NoItemBehaviour extends Object {

        private String behaviour;

        private Object forGetItemValue;

        private String forGetItemText;

        private List<Object> forGetItemValueList;

        private Object forTMLItem;

        private List<Object> forTMLItemList;

        private Object forTMLFormField;

        private List<Object> forTMLFormFieldList;

        private Object forTMLFormEmptyField;

        private List<Object> forTMLFormEmptyFieldList;

        /**
         * Default contructor defining the "straight" behaviour which is default
         * since WGA 4
         */
        public NoItemBehaviour() {
            straight();
        }

        /**
         * Sets the behaviour complicance to the given behaviour string. Uses
         * Constants {@link CSConfig#VERSIONCOMPLIANCE_WGA3} or
         * {@link CSConfig#VERSIONCOMPLIANCE_WGA4}
         */
        public void compliantTo(String compliance) {

            if (!compliance.equals(behaviour)) {

                if (compliance.equals(CSConfig.VERSIONCOMPLIANCE_WGA3)) {
                    compatibleForDBImplementation(getCore());
                } else {
                    straight();
                }
            }

        }

        public void straight() {

            behaviour = CSConfig.VERSIONCOMPLIANCE_WGA51;
            forGetItemValue = null;
            forGetItemText = null;
            forGetItemValueList = new ArrayList<Object>();
            forTMLItem = null;
            forTMLItemList = new ArrayList<Object>();
            forTMLFormField = null;
            forTMLFormFieldList = new ArrayList<Object>();
            forTMLFormEmptyField = null;
            forTMLFormEmptyFieldList = new ArrayList<Object>();
        }

        /**
         * Assures WGA 3.3 compatibility for the given database type
         * 
         * @param core
         */
        public void compatibleForDBImplementation(WGDatabaseCore core) {

            behaviour = CSConfig.VERSIONCOMPLIANCE_WGA3;

            List<Object> listWithEmptyString = new ArrayList<Object>();
            listWithEmptyString.add("");

            // Implementation independent in WGA 3.3
            forTMLItem = "";
            forTMLFormField = null;
            forTMLFormFieldList = null;
            forTMLFormEmptyField = "";
            // B00004716
            forTMLFormEmptyFieldList = new ArrayList<Object>();

            // Implementation dependent in WGA 3.3
            if (core.getTypeName().startsWith("domino/")) {
                forGetItemValue = "";
                forGetItemValueList = listWithEmptyString;
                forGetItemText = "";
                forTMLItemList = listWithEmptyString;
            } else if (core.getTypeName().startsWith("jdbc/")) {
                forGetItemValue = new ArrayList<Object>();
                forGetItemValueList = new ArrayList<Object>();
                forGetItemText = null;
                forTMLItemList = new ArrayList<Object>();
            }

        }

        /**
         * Behaviour for method
         * {@link de.innovationgate.webgate.api.WGDocument#getItemValue}
         */
        public Object getForGetItemValue() {
            return clone(forGetItemValue);
        }

        public void setForGetItemValue(Object forGetItemValue) {
            this.forGetItemValue = forGetItemValue;
        }

        /**
         * Behaviour for method {@link WGDocument#getItemValueList}
         */
        @SuppressWarnings("unchecked")
        public List<Object> getForGetItemValueList() {
            return (List<Object>) clone(forGetItemValueList);
        }

        private Object clone(Object obj) {

            if (obj != null && obj instanceof List) {
                return new ArrayList<Object>((List<?>) obj);
            } else {
                return obj;
            }

        }

        public void setForGetItemValueList(List<Object> forGetItemValueList) {
            this.forGetItemValueList = forGetItemValueList;
        }

        /**
         * Behaviour for method TMLForm.field(), when field exists but is empty
         */
        public Object getForTMLFormEmptyField() {
            return clone(forTMLFormEmptyField);
        }

        public void setForTMLFormEmptyField(Object forTMLFormEmptyField) {
            this.forTMLFormEmptyField = forTMLFormEmptyField;
        }

        /**
         * Behaviour for method TMLForm.fieldList, when field exists but is
         * empty
         */
        @SuppressWarnings("unchecked")
        public List<Object> getForTMLFormEmptyFieldList() {
            return (List<Object>) clone(forTMLFormEmptyFieldList);
        }

        public void setForTMLFormEmptyFieldList(List<Object> forTMLFormEmptyFieldList) {
            this.forTMLFormEmptyFieldList = forTMLFormEmptyFieldList;
        }

        /**
         * Behaviour for method TMLForm.field()
         */
        public Object getForTMLFormField() {
            return clone(forTMLFormField);
        }

        public void setForTMLFormField(Object forTMLFormField) {
            this.forTMLFormField = forTMLFormField;
        }

        /**
         * Behaviour for method TMLForm.fieldList()
         */
        @SuppressWarnings("unchecked")
        public List<Object> getForTMLFormFieldList() {
            return (List<Object>) clone(forTMLFormFieldList);
        }

        public void setForTMLFormFieldList(List<Object> forTMLFormFieldList) {
            this.forTMLFormFieldList = forTMLFormFieldList;
        }

        /**
         * Behaviour for method TMLContext.item()
         */
        public Object getForTMLItem() {
            return clone(forTMLItem);
        }

        public void setForTMLItem(Object forTMLItem) {
            this.forTMLItem = forTMLItem;
        }

        /**
         * Behaviour for method TMLContext.itemList()
         */
        @SuppressWarnings("unchecked")
        public List<Object> getForTMLItemList() {
            return (List<Object>) clone(forTMLItemList);
        }

        public void setForTMLItemList(List<Object> forTMLItemList) {
            this.forTMLItemList = forTMLItemList;
        }

        /**
         * Behaviour for method {@link WGDocument#getItemText}
         */
        public String getForGetItemText() {
            return (String) clone(forGetItemText);
        }

        public void setForGetItemText(String forGetItemText) {
            this.forGetItemText = forGetItemText;
        }

        public String getCurrentBehaviour() {
            return behaviour;
        }

    }

    /**
     * Cache of user authorisation details
     */
    private static String USERCACHE_USERDETAILS = "userDetails";

    /**
     * Cache of decisions whether the user is member of a user list or not
     */
    private static String USERCACHE_MEMBERDECISIONS = "memberDecisions";

    /**
     * Predefined creation option, determining how many cache entries the WGAPI
     * cache is allowed to hold in memory. Defaults to 2000.
     * @deprecated
     */
    public static final String COPTION_MEMORYCACHESIZE = "MemoryCacheSize";

    /**
     * Predefined creation option, controlling of the WGAPI Cache is allowed to
     * use disk overflow functionality, as far as the implementation supports
     * this. Defaults to false.
     * @deprecated
     */
    public static final String COPTION_CACHEOVERFLOW = "CacheOverflowOnDisk";

    /**
     * Predefined creation option, controlling if the WGA workflow should
     * automatically approve a document in workflow if it's publisher is also
     * the approver
     */
    public static final String COPTION_AUTOAPPROVE = "AutoApprove";

    /**
     * Predefined creation option, controlling if hierarchical reader fields on areas and
     * pages are in effect, defaulting to true. Setting this to false may improve
     * performance but will let those feld be ignored.
     */
    public static final String COPTION_PAGEREADERS_ENABLED = "PageReadersEnabled";

    /**
     * Controls usage of user caches, which will cache user authorisation results for some time.
     * Disable this if your user may have differing aliases, groups and roles with each request.
     */
    public static final String COPTION_USERCACHES_ENABLED = "UserCachesEnabled";

    /**
     * Class name of a type that implements {@link PageRightsFilter} and is to be used as this
     */
    public static final String COPTION_PAGERIGHTSFILTER = "PageRightsFilter";

    /**
     * Java system property for turning on verbose cachemanagement in applog
     */
    public static final String SYSPROPERTY_VERBOSE_CACHEMANAGEMENT = "de.innovationgate.wga.cachemanagement.verbose";

    /**
     * Java system property for turning on verbose output when a mutable object
     * needs to be cloned in WGAPI
     */
    public static final String SYSPROPERTY_VERBOSE_MUTABLECLONING = "de.innovationgate.wga.mutablecloning.verbose";

    private static final boolean VERBOSE_CACHE_MANAGEMENT = Boolean
            .valueOf(System.getProperty(SYSPROPERTY_VERBOSE_CACHEMANAGEMENT)).booleanValue();

    private static final boolean VERBOSE_MUTABLE_CLONING = Boolean
            .valueOf(System.getProperty(SYSPROPERTY_VERBOSE_MUTABLECLONING)).booleanValue();

    private static final String SYSPROPERTY_VERBOSE_DOCUMENTINSTANTIATION = "de.innovationgate.wga.documentinstantiation.verbose";

    protected static final boolean VERBOSE_DOCUMENT_INSTANTIATION = Boolean
            .valueOf(System.getProperty(SYSPROPERTY_VERBOSE_DOCUMENTINSTANTIATION)).booleanValue();

    /**
     * Java system property to enable/disable automatic operation key maintaining
     */
    public static final String SYSPROPERTY_MAINTAIN_OPERATIONKEYS = "de.innovationgate.wga.maintain-operationkeys";

    /**
     * Sysproperty to enable verbose backend access
     */
    public static final String SYSPROPERTY_VERBOSE_BACKENDACCESS = "de.innovationgate.wga.backend.verbose";

    /**
     * Sysproperty to configure the doctypes for verbose backend access, as commaseparated typename list
     */
    public static final String SYSPROPERTY_VERBOSE_BACKENDACCESS_DOCTYPES = "de.innovationgate.wga.backend.verbose.doctypes";

    private static final boolean VERBOSE_BACKEND_ACCESS = Boolean
            .valueOf(System.getProperty(SYSPROPERTY_VERBOSE_BACKENDACCESS)).booleanValue();

    private static final Set<Integer> VERBOSE_BACKEND_ACCESS_DOCTYPES = new HashSet<Integer>();
    static {
        if (System.getProperty(SYSPROPERTY_VERBOSE_BACKENDACCESS_DOCTYPES) != null) {
            for (String typeName : WGUtils.deserializeCollection(
                    System.getProperty(SYSPROPERTY_VERBOSE_BACKENDACCESS_DOCTYPES), ",", true)) {
                int docType = WGDocument.doctypeNameToNumber(typeName);
                if (docType > 0) {
                    VERBOSE_BACKEND_ACCESS_DOCTYPES.add(docType);
                }
            }
        }
    }

    /**
     * Predefined creation option, determining how many seconds the update
     * monitoring should pause after an update has been processed. Defaults to 5
     * seconds.
     */
    public static final String COPTION_UPDATETIMEOUT = "UpdateTimeout";

    /**
     * Sets a db reference directly at creation time, so the WGAPI might use it
     * from the start
     */
    public static final String COPTION_DBREFERENCE = "DBReference";

    private int updateTimeoutSeconds = 5;

    /**
     * Represents an Entry in Query cache with all neccessary info
     */
    public class QueryCacheEntry {

        private WGCachedResultSet _resultSet;

        private String _fullQuery;

        public QueryCacheEntry(WGCachedResultSet resultSet, String fullQuery) {
            _resultSet = resultSet;
            _fullQuery = fullQuery;
        }

        /**
         * @return Returns the fullQuery.
         */
        protected String getFullQuery() {
            return _fullQuery;
        }

        /**
         * @return Returns the resultSet.
         */
        protected WGCachedResultSet getResultSet() {
            return _resultSet;
        }

    }

    /**
     * Represents the key of a query cache entry, containing query type, query
     * string and parameter
     */
    public class QueryCacheKey implements Serializable {

        private String _type;

        private String _query;

        private Map _parameters;

        public QueryCacheKey(String type, String query, Map parameters) {
            _type = type;
            _query = query;
            _parameters = new HashMap();

            // Convert parameter entries to comparable objects
            Iterator entries = parameters.entrySet().iterator();
            while (entries.hasNext()) {
                Map.Entry entry = (Map.Entry) entries.next();
                Object entryValue = entry.getValue();
                if (entryValue instanceof Comparable) {
                    _parameters.put(entry.getKey(), entryValue);
                } else if (entryValue instanceof WGDocument) {
                    _parameters.put(entry.getKey(), ((WGDocument) entryValue).getDocumentKey());
                } else {
                    _parameters.put(entry.getKey(), String.valueOf(entryValue));
                }
            }

        }

        public int hashCode() {
            final int PRIME = 31;
            int result = 1;
            result = PRIME * result + ((_parameters == null) ? 0 : _parameters.hashCode());
            result = PRIME * result + ((_query == null) ? 0 : _query.hashCode());
            result = PRIME * result + ((_type == null) ? 0 : _type.hashCode());
            return result;
        }

        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            final QueryCacheKey other = (QueryCacheKey) obj;
            if (_parameters == null) {
                if (other._parameters != null)
                    return false;
            } else if (!_parameters.equals(other._parameters))
                return false;
            if (_query == null) {
                if (other._query != null)
                    return false;
            } else if (!_query.equals(other._query))
                return false;
            if (_type == null) {
                if (other._type != null)
                    return false;
            } else if (!_type.equals(other._type))
                return false;
            return true;
        }

    }

    /**
     * Contains statistics about a WGA session. Session statistics are initially
     * collected in WGSessionContext objects but thrown away with these objects
     * when the session exists. The SessionStatistic object can be used to
     * persist certain intersting statistics beyond the session lifetime.
     */
    public class SessionStatistic implements Comparable {

        private boolean _maxDocsExceeded;

        private String _task;

        private Date _created;

        private int _totalDocsRetrieved;

        /**
         * Constructor that will extract statistics from a session context.
         * 
         * @param context
         *            The session context that is used for information retrieval
         */
        public SessionStatistic(WGSessionContext context) {
            _totalDocsRetrieved = context.getTotalFetchedCores();
            _created = context.getCreated();
            _task = context.getTask();
            _maxDocsExceeded = context.isMaxCoreMessageShowed();
        }

        /**
         * Returns the creation date of the session
         */
        public Date getCreated() {
            return _created;
        }

        /**
         * Returns if the "maxdocs"-Property of maximum documents per session
         * was exceeded by this session. If so, the session did drop it's first
         * retrieved backend documents while processing.
         */
        public boolean isMaxDocsExceeded() {
            return _maxDocsExceeded;
        }

        /**
         * Returns the task that the session did accomplish
         */
        public String getTask() {
            return _task;
        }

        /**
         * Returns, how many docs the session did retrieve from backend while it
         * was running
         */
        public int getTotalDocsRetrieved() {
            return _totalDocsRetrieved;
        }

        /*
         * (Kein Javadoc)
         * 
         * @see java.lang.Comparable#compareTo(java.lang.Object)
         */
        public int compareTo(Object o) {
            SessionStatistic otherStatistic = (SessionStatistic) o;
            return (getTotalDocsRetrieved() - otherStatistic.getTotalDocsRetrieved());
        }

    }

    /**
     * Shows, if the database monitors the last change date of the backend
     * database to maintain caches.
     */
    public boolean monitorLastChange() {
        return this.monitorLastChange;
    }

    private String title = null;

    /**
     * Accesslevel-Name for ACCESSLEVEL_NOTLOGGEDIN
     */
    public static final String ALNAME_NOTLOGGEDIN = "(not logged in)";

    /**
     * Accesslevel-Name for ACCESSLEVEL_READER
     */
    public static final String ALNAME_READER = "READER";

    /**
     * Accesslevel-Name for ACCESSLEVEL_NOACCESS
     */
    public static final String ALNAME_NOACCESS = "NOACCESS";

    /**
     * Accesslevel-Name for ACCESSLEVEL_MANAGER
     */
    public static final String ALNAME_MANAGER = "MANAGER";

    /**
     * Accesslevel-Name for ACCESSLEVEL_EDITOR
     */
    public static final String ALNAME_EDITOR = "EDITOR";

    /**
     * Accesslevel-Name for ACCESSLEVEL_CHIEF_EDITOR
     */
    public static final String ALNAME_CHIEF_EDITOR = "CHIEFEDITOR";

    /**
     * Accesslevel-Name for ACCESSLEVEL_AUTHOR
     */
    public static final String ALNAME_AUTHOR = "AUTHOR";

    /**
     * Map of all access levels. This also includes access levels not allowed for usage in ACL entries, like NOTLOGGEDIN, and duplicate mappings because of obsolete access levels
     */
    public static final Map<Integer, AccessLevel> ACCESSLEVELS = new HashMap<Integer, WGDatabase.AccessLevel>();

    /**
     * Map of "real" access levels, which are available for new ACLs entries
     */
    public static final Map<Integer, AccessLevel> REAL_ACCESSLEVELS = new LinkedHashMap<Integer, WGDatabase.AccessLevel>();
    static {
        AccessLevel al;

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_NOTLOGGEDIN, ALNAME_NOTLOGGEDIN, false,
                new AccessLevelPrivilege(false, false), new AccessLevelPrivilege(false, false),
                new AccessLevelPrivilege(false, false));
        ACCESSLEVELS.put(al.getCode(), al);

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_NOACCESS, ALNAME_NOACCESS, true,
                new AccessLevelPrivilege(false, false), new AccessLevelPrivilege(false, false),
                new AccessLevelPrivilege(false, false));
        REAL_ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(al.getCode(), al);

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_READER, ALNAME_READER, true,
                new AccessLevelPrivilege(false, false), new AccessLevelPrivilege(false, false),
                new AccessLevelPrivilege(true, true));
        REAL_ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(15, al); // Transitionary mapping for old "/designer" level

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_AUTHOR, ALNAME_AUTHOR, true,
                new AccessLevelPrivilege(false, false), new AccessLevelPrivilege(false, false),
                new AccessLevelPrivilege(true, true));
        REAL_ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(25, al); // Transitionary mapping for old "/designer" level

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_EDITOR, ALNAME_EDITOR, true,
                new AccessLevelPrivilege(true, true), new AccessLevelPrivilege(true, true),
                new AccessLevelPrivilege(true, true));
        REAL_ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(35, al); // Transitionary mapping for old "/designer" level

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_CHIEF_EDITOR, ALNAME_CHIEF_EDITOR, true,
                new AccessLevelPrivilege(true, true), new AccessLevelPrivilege(true, true),
                new AccessLevelPrivilege(true, true));
        REAL_ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(al.getCode(), al);

        al = new AccessLevel(WGDatabase.ACCESSLEVEL_MANAGER, ALNAME_MANAGER, true,
                new AccessLevelPrivilege(true, true), new AccessLevelPrivilege(true, true),
                new AccessLevelPrivilege(true, true));
        REAL_ACCESSLEVELS.put(al.getCode(), al);
        ACCESSLEVELS.put(al.getCode(), al);

    }

    private Map<String, WGOperationKey> operations = (new MapMaker()).weakValues().makeMap();

    /**
     * Converts access level access level names WGDatabase.ALNAME_... to codes
     * WGDatabase.ACCESSLEVEL_...
     * 
     * @param levelCode
     *            The access level code to convert
     * @return The access level name. If code is unknown returns "(unknown)"
     */
    public static String accessLevelText(int levelCode) {

        AccessLevel level = ACCESSLEVELS.get(levelCode);
        if (level != null) {
            return level.getName();
        } else {
            return "(unknown)";
        }

    }

    /**
     * Converts access level names WGDatabase.ALNAME_... to codes
     * WGDatabase.ACCESSLEVEL_...
     * 
     * @param levelName
     *            The access level name to convert
     * @return The code. If the name is unknown returns
     *         WGDatabase.ACCESSLEVEL_NOTLOGGEDIN
     */
    public static int accessLevelCode(String levelName) {

        for (AccessLevel level : ACCESSLEVELS.values()) {
            if (level.getName().equals(levelName)) {
                return level.getCode();
            }
        }

        return WGDatabase.ACCESSLEVEL_NOTLOGGEDIN;
    }

    /**
     * Returns access level information for the given access level code
     * @param levelCode
     */
    public static AccessLevel accessLevel(int levelCode) {
        return ACCESSLEVELS.get(levelCode);
    }

    /**
     * Collects various statistics about this database.
     */
    public class WGDatabaseStatistics {

        /**
         * Returns the number of documents that currently reside in the cache
         * cache
         */
        public long getDocumentCount() {
            try {
                return masterDocumentsByKey.getSize();
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception retrieving master document cache size", e);
                return 0;
            }
        }

        public long getDocumentCacheMaxSize() {
            try {
                return masterDocumentsByKey.getMaxSize();
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception retrieving master document cache max size", e);
                return 0;
            }
        }

        public int getDocumentCacheUtilisation() {
            try {
                return masterDocumentsByKey.getUtilisation();
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception retrieving master document cache utilisation", e);
                return 0;
            }
        }

    }

    private int CONCURRENCY_WGOPERATION_KEY = 65536;
    private final Semaphore _wgOperationKeySemaphore = new Semaphore(CONCURRENCY_WGOPERATION_KEY, true);

    /**
     * Util class for finding the next free content version
     */
    public class FreeContentVersionFinder {

        private WGStructEntry _entry;

        private WGLanguage _language;

        public FreeContentVersionFinder(WGStructEntry entry, WGLanguage language) {
            _entry = entry;
            _language = language;
        }

        public int findNewVersion() throws WGAPIException {

            int newVersion;

            // Optimized backend method
            if (isBackendServiceSupported(BACKENDSERVICE_NEW_CONTENT_VERSION)) {
                newVersion = (Integer) callBackendService(BACKENDSERVICE_NEW_CONTENT_VERSION,
                        new Object[] { _entry, _language });
            }

            // WGAPI method
            else {
                newVersion = 0;
                Iterator<WGDocumentCore> contentList = getCore().getAllContent(_entry, true).iterator();
                WGDocumentCore content;
                while (contentList.hasNext()) {
                    content = contentList.next();
                    String language = (String) content.getMetaData(WGContent.META_LANGUAGE);
                    int version = ((Number) content.getMetaData(WGContent.META_VERSION)).intValue();
                    if (language.equals(_language.getName()) && version > newVersion) {
                        newVersion = version;
                    }
                }
                newVersion++;
            }

            return newVersion;
        }

    }

    private UserHashMapGroup userHashMapGroup;

    // Special user names
    /**
     * Anonymous user name
     */
    public static final String ANONYMOUS_USER = Constants.ANONYMOUS_USER;

    /**
     * Dummy user name that is used in openSession-Method to insert a session
     * token. The token then should be entered as password.
     */
    public static final String SESSIONTOKEN_USER = "###SESSIONTOKEN###";

    /**
     * Dummy user name that is used on request based authmodules if
     * request.getRemoteUser() returns 'null'
     */
    public static final String UNKNOWN_REMOTE_USER = "###UNKNOWN_REMOTE_USER###";

    /**
     * The default media key
     */
    public static final String DEFAULT_MEDIAKEY = "html";

    /**
     * User hasn't logged in yet or login failed due to incorrect login
     * information
     */
    // Access levels
    /**
     * User nas no valid login
     */
    public static final int ACCESSLEVEL_NOTLOGGEDIN = -1;

    /**
     * User has not access to the database.
     */
    public static final int ACCESSLEVEL_NOACCESS = 0;

    /**
     * User can read documents but cannot create or edit them.
     */
    public static final int ACCESSLEVEL_READER = 10;

    /**
     * User can create content documents but has no access to content documents,
     * not created by him
     */
    public static final int ACCESSLEVEL_AUTHOR = 20;

    /**
     * User can create new content documents, and edit existing ones that he
     * didn't create.
     */
    public static final int ACCESSLEVEL_EDITOR = 30;

    /**
     * Full functional rights, restricted technical rights
     */
    public static final int ACCESSLEVEL_CHIEF_EDITOR = 40;

    /**
     * Full access, creation and administration rights.
     */
    public static final int ACCESSLEVEL_MANAGER = 90;

    /**
     * Enhance the query with default settings (e.g. not to retrieve documents
     * that are set invisible). Set to Boolean(true/false).
     */
    public static final String QUERYOPTION_ENHANCE = "enhance";

    /**
     * Set to Boolean(true), to reduce query results to only released content
     */
    public static final String QUERYOPTION_ONLYRELEASED = "onlyReleased";

    /**
     * Set to a specific language code to reduce query results to content of
     * this language
     */
    public static final String QUERYOPTION_ONLYLANGUAGE = "onlyLanguage";

    /**
     * When set, excludes the WGContent object given as option value from the
     * query result
     */
    public static final String QUERYOPTION_EXCLUDEDOCUMENT = "excludeDocument";

    /**
     * This is an output option set by the implementation, that carries the
     * complete query that was executed, as it was modified by the
     * implementation.
     */
    public static final String QUERYOPTION_RETURNQUERY = "returnQuery";

    /**
     * Controls the maximum results of the query. Specify as new Integer(value). A value of 0 means unlimited results.
     */
    public static final String QUERYOPTION_MAXRESULTS = "maxResults";

    /**
     * Cache the results of queries.
     */
    public static final String QUERYOPTION_CACHERESULT = "cacheResult";

    /**
     * Takes native options for the database implementation as commaseparated
     * key=value-Pairs. E.g. option1=value1,option2=value2...
     */
    public static final String QUERYOPTION_NATIVEOPTIONS = "nativeOptions";

    /**
     * Set to the role, that this query should act as, filtering contents that
     * should be invisible to that role. Default is
     * WGContent.DISPLAYTYPE_SEARCH. Compare WGContent.isHiddenFrom(). Use
     * Constants WGContent.DISPLAYTYPE_... as values.
     */
    public static final String QUERYOPTION_ROLE = "role";

    /**
     * The occurence of this queryoption in the query parameters after the
     * execution of the query shows, that the result set was retrieved from
     * query cache
     */
    public static final String QUERYOPTION_USEDCACHE = "usedCache";

    /**
     * Injects a priority list of languages to return in the query. Should override {@link #QUERYOPTION_ONLYLANGUAGE} if available.
     */
    public static final String QUERYOPTION_LANGUAGES = "languages";

    /**
     * This query option contains a map with query parameters, whose values
     * should be used by the underlying query mechanism to fill parameter parts
     * in the query text.
     */
    public static final String QUERYOPTION_QUERY_PARAMETERS = "queryParameters";;

    /**
     * Query type for {@link #queryUserProfileNames(String, String, Map)} that retrieves all existent user profiles
     */
    public static final String PROFILEQUERY_TYPE_ALL = "all";

    // Roles
    /**
     * Database serves content and supplementary docs like content type,
     * language if it is a full content store
     */
    public static final String ROLE_CONTENT = "content";

    /**
     * Database servers design documents (WebTML, CSS/JS)
     */
    public static final String ROLE_DESIGN = "design";

    /**
     * Database serves file containers
     */
    public static final String ROLE_REPOSITORY = "repository";

    /**
     * Database servers user profiles
     */
    public static final String ROLE_USERPROFILES = "userprofiles";

    /**
     * Content supports query languages.
     */
    public static final String FEATURE_QUERYABLE = "queryable";

    /**
     * The content store is able to also store user profiles and be its own "personalisation database"
     */
    public static final String FEATURE_SELF_PERSONALIZABLE = "selfPersonalizable";

    /**
     * DB has a native expression language
     */
    public static final String FEATURE_NATIVEEXPRESSIONS = "nativeExpressions";

    /**
     * Database is editable i.e. can modify data of it's backend
     */
    public static final String FEATURE_EDITABLE = "editable";

    /**
     * Content can be navigated hierarchical / is hierarchically ordered
     */
    public static final String FEATURE_HIERARCHICAL = "hierarchical";

    /**
     * Content database maintains a last changed property, that should be
     * monitored to track database changes.
     */
    public static final String FEATURE_LASTCHANGED = "lastChanged";

    /**
     * Content database can store complex values beyond string, number an dates
     * and performs type checking itself
     */
    public static final String FEATURE_COMPLEXVALUES = "complexValues";

    /**
     * Content database needs temporary storage of webtml variables in content
     * documents (e.g. to allow native expression languages to access them)
     */
    public static final String FEATURE_STORESVARS = "storesVars";

    /**
     * Content database has content types, struct entries, is multilingual and
     * maintains content versioning, so content document creation must receive
     * and generate all this info.
     */
    public static final String FEATURE_FULLCONTENTFEATURES = "fullContentFeatures";

    /**
     * Controls the behaviour when setting object relations for newly created
     * objects. If feature present, the objects themselves will be passed as
     * metadata. If not, only the name/key will be passed
     */
    public static final String FEATURE_USE_OBJECTS_AS_REFERENCES = "useObjectsAsReferences";

    /**
     * Controls, if this implementations's newly created struct entries can
     * generate their own keys
     */
    public static final String FEATURE_GENERATES_STRUCTKEYS = "generatesStructkeys";

    /**
     * Controls if this implementation's newly created struct entries can accept
     * keys from the creator
     */
    public static final String FEATURE_ACCEPTS_STRUCTKEYS = "acceptsStructkeys";

    /**
     * Feature that will prevent the WGAPI from limiting the fetched cores for a session
     * Should be set for DBs where limiting on WGAPI level makes no sense
     */
    public static final String FEATURE_UNLIMITED_CORES = "unlimitedCores";

    /**
     * Controls if this implementation is able to perform a backend login, i.e. the core itself
     * logs in with the user's login data. If this feature is activated and the database has
     * no authentication module configured, the login information is given to {@link WGDatabaseCore#openSession(AuthenticationSession, Object, boolean)}
     * as {@link BackendAuthSession}.
     */
    public static final String FEATURE_PERFORMS_BACKEND_LOGIN = "performsBackendLogin";

    /**
     * Shows if this database relies on authentication modules for external
     * authentication.
     */
    public static final String FEATURE_EXTERNAL_AUTHENTICATION = "externalAuthentication";

    /**
     * Controls if this WGAPI implementation implements the getAllContentKeys()
     * method
     */
    public static final String FEATURE_RETRIEVE_ALL_CONTENTKEYS = "retrieveAllContentKeys";

    /**
     * Shows, if this implementation supports querying user profiles by method
     * queryUserProfileNames
     */
    public static final String FEATURE_QUERY_PROFILES = "queryProfiles";

    /**
     * Automatically cascades deletions (i.e. deletes all dependent documents),
     * so that the WGAPI doesn't need to do that
     */
    public static final String FEATURE_AUTOCASCADES_DELETIONS = "autocascadesDeletions";

    /**
     * Controls if this implementation can take session tokens for a login
     */
    public static final String FEATURE_SESSIONTOKEN = "sessionToken";

    /**
     * Controls, if this implementation holds content in multiple languages
     */
    public static final String FEATURE_MULTILANGUAGE = "multilanguage";

    /**
     * Content database supports transactions
     */
    public static final String FEATURE_TRANSACTIONS = "transactions";

    /**
     * This feature instructs the WGAPI to reduce callbacks to the database on
     * some occassions where it is unlikely necessary to connect (Should
     * increase performance on implementations with slow database connections)
     */
    public static final String FEATURE_REDUCE_CALLBACKS = "reduceCallbacks";

    /**
     * ACL of this database is manageable via WGAPI
     */
    public static final String FEATURE_ACL_MANAGEABLE = "aclManageable";

    /**
     * Find updated documents since a cutoff date by core-method
     * getUpdateDocumentsSince
     */
    public static final String FEATURE_FIND_UPDATED_DOCS = "findUpdatedDocs";

    /**
     * Use the WGAPI to create a new database of this type (Method
     * WGFactory.createDatabase())
     */
    public static final String FEATURE_CREATEABLE = "createable";

    /**
     * determines that the underlying core has the feature to loadbalance
     * connections to multiple db servers, in this case connections are by
     * default readonly and each db updates have to be initialized by calling
     * beginUpdate()
     */
    public static final String FEATURE_LOADBALANCE = "loadbalance";

    /**
     * determines if documents in this database has the ability to validate file
     * attachments by calling doc.validateAttachments();
     */
    public static final String FEATURE_VALIDATE_ATTACHMENTS = "validateAttachments";

    /**
     * determines if the database supports real content relations
     */
    public static final String FEATURE_CONTENT_RELATIONS = "contentRelations";

    /**
     * determines if the database implementation has the ability to return document changes and modified dates on millisecond precision
     */
    public static final String FEATURE_MILLISECOND_PRECISION = "millisecondPrecision";

    /**
     * Enabling this feature will disable the lowercasing of metadata field values for fields that normally enforce this.
     */
    public static final String FEATURE_DISABLE_META_LOWERCASING = "disableMetaLowercasing";

    /**
     * Determines if the database supports removing and re-adding a subentity (item, relation, file attachments) without intermittent save 
     */
    public static final String FEATURE_DIRECT_ENTITY_READDING = "directEntityReadding";

    /**
     * Determines if a database returns ordered results on hierarchy navigation methods {@link WGDatabaseCore#getRootEntries(WGArea, WGPageOrderSet)} and {@link WGDatabaseCore#getChildEntries(WGStructEntry, WGPageOrderSet)}, allowing the fetching of partial results  
     */
    public static final String FEATURE_ORDERED_NAVIGATION_RESULTS = "orderedNavigationResults";

    /**
     * Determines if a database is able to return meta {@link WGUserProfile#META_PORTLETITEMSTORAGE}, a storage service for items of transient portlets
     */
    public static final String FEATURE_PROVIDE_PORTLETITEM_STORAGE = "providePortletItemStorage";

    /**
     * Indicates if the database knows any kind of user-specific content read protection, so that content caches must be individual for every user
     */
    public static final String FEATURE_CONTENT_READ_PROTECTION = "contentReadProtection";

    /**
     * Determines if the database is able to store derivates to content file attachments
     */
    public static final String FEATURE_CONTENT_FILE_DERIVATES = "contentFileDerivates";

    /**
     * Marks databases that should not be patched, f.e. because they are read-only
     */
    public static final String FEATURE_UNPATCHEABLE = "unpatcheable";

    /**
     * Predefined creation option to specify configuration XML.
     * @deprecated
     */
    public static final String COPTION_XML = "XML";

    /**
     * Defines if caching is globally enabled for this database, defaults to
     * true. If false, the database will cache no document data. In this state
     * the WGAPI locking functions are not effective.
     */
    public static final String COPTION_CACHING_ENABLED = "CachingEnabled";

    /**
     * Predefined creation option, that defines how many "cores" (i.e. backend
     * documents) a session may retrieve at once. If more are retrieved, the
     * first cores will be dropped to stay under this threshold.
     */
    public static final String COPTION_MAXCORES = "MaxDocs";

    /**
     * Predefined creation option, that determines if the LastChange-Date of
     * this database should be monitored to drop cache when it changes.
     */
    public static final String COPTION_MONITORLASTCHANGE = "MonitorLastChange";

    /**
     * Specifies the document types that a design provider for the current db
     * should provide, taking a commaseparated list of typenames of design
     * documents. This option must be enforced by the provider itself.
     */
    public static final String COPTION_DESIGNPROVIDERTYPES = "DesignProviderDoctypes";

    /**
     * Predefined creation option, that determines the class name of a workflow
     * engine to use. If this is not set, the default workflow engine of the
     * implementation is used
     */
    public static final String COPTION_WORKFLOWENGINE = "WorkflowEngine";

    /**
     * Determines the behaviour of this database and functionalitites that use
     * this database regarding the retrieval of nonexistent items. Allowed
     * values: - "straight" - The default. Always return null. - "compatible" -
     * Compatibility with WGA 3.3. Use the values used there which differ by DB
     * implementation and functionality
     */
    public static final String COPTION_NOITEMBEHAVIOUR = "NoItemBehaviour";

    /**
     * Determines the latency of user specific caches that expire after the
     * given time (in minutes). If 0 the caches never expire.
     */
    public static final String COPTION_USERCACHELATENCY = "UserCacheLatency";

    /**
     * Determines the latency of regular caches that expire after the given time (in minutes).
     * If 0 the caches will never expire. 
     */
    public static final String COPTION_CACHELATENCY = "CacheLatency";

    /**
     * Determines if this database may use a shared connection pool on servers
     * which have one available. Databases implicitly use a shared pool when
     * one is available, unless this setting is disabled. 
     */
    public static final String COPTION_SHAREDPOOL = "SharedPool";

    /**
     * Determines if users at access level "READER" are allowed to create user
     * profiles in this database
     */
    public static final String COPTION_READERPROFILECREATION = "ReaderProfileCreation";

    /**
     * Option to directly determine the content store version of this database 
     */
    public static final String COPTION_CONTENT_STORE_VERSION = "ContentStoreVersion";

    /**
     * Option to define users/groups/roles that should be able to read all documents no matter the contents of reader fields
     */
    public static final String COPTION_MANDATORY_READERS = "MandatoryReaders";

    /**
     * Special DB attribute that is to be set while a batch process (e.g.
     * synchronization) is currently updating the database.
     */
    public static final String ATTRIB_UPDATED = "$updated";

    /**
     * CA loaded via publisher option POPTION_CA
     */
    private X509Certificate _currentCA;

    private long _currentCALastModified;

    /**
     * CRL loaded via publisher option POPTION_CRL
     */
    private X509CRL _currentCRL;

    private long _currentCRLLastModified;

    // Core object
    private WGDatabaseCore core = null;

    // Storing of information
    private volatile boolean ready = false;

    private boolean monitorLastChange = false;

    protected boolean cachingEnabled = true;

    /**
     * The revision indicator of data in this database. Actually the state
     * of the database that is represented by the current cache state.
     */
    private WGDatabaseRevision _revision = null;

    /**
     * The date that the current revision corresponds to
     */
    private Date _revisionDate = null;

    /**
     * Time of the last cache maintenance. This is currently only used for {@link #getLastCacheMaintenance()}.
     * The readonly cache functionality is now driven by #lastCacheMaintenanceNanos.
     */
    private Date lastCacheMaintenance = null;

    private String masterLoginInputName = null;

    private String masterLoginName = null;

    private String masterLoginPassword = null;

    private String dbReference = "(none)";

    private String path = null;

    private boolean contentRole = false;

    private boolean designRole = false;

    private boolean repositoryRole = false;

    private boolean userProfilesRole = false;

    private int maxCores = 1000;

    private Map<String, Object> creationOptions = new ConcurrentHashMap<String, Object>();

    private Map<String, Object> customAttributes = new ConcurrentHashMap<String, Object>();

    private Map features = new ConcurrentHashMap();

    private SortedBag sessionStatistics = new TreeBag();

    // Session objects
    protected ThreadLocal sessionContext = new ThreadLocal();

    protected Cache masterDocumentsByKey;
    protected Cache masterDocumentByName;

    protected UserHashMap _userCache = null;

    protected Cache masterQueryCache;

    protected ConcurrentWeakHashMap<WGDocumentKey, WGDocument.Cache> masterDocumentCaches = new ConcurrentWeakHashMap<>();

    protected Map<Integer, WGDocumentListCache> designDocumentLists = new ConcurrentHashMap<Integer, WGDocumentListCache>();

    // Event listener registration
    private List<WGDatabaseEventListener> databaseEventListeners = new CopyOnWriteArrayList<WGDatabaseEventListener>();

    private List<WGDatabaseConnectListener> databaseConnectListeners = new CopyOnWriteArrayList<WGDatabaseConnectListener>();

    private List<WGDesignChangeListener> databaseDesignChangeListeners = new CopyOnWriteArrayList<WGDesignChangeListener>();

    private List<WGBackendChangeListener> backendChangeListeners = new CopyOnWriteArrayList<WGBackendChangeListener>();

    private List<WGContentEventListener> contentEventListeners = new CopyOnWriteArrayList<WGContentEventListener>();

    private List<WGWorkflowEventListener> workflowEventListeners = new CopyOnWriteArrayList<WGWorkflowEventListener>();

    private List<WGFileAnnotator> fileAnnotators = new CopyOnWriteArrayList<WGFileAnnotator>();

    private WGFileConverter fileConverter = null;

    private WGDesignProvider designProvider = null;

    private WGSchemaDefinition schemaDefinition = null;

    protected WGWorkflowEngine workflowEngine = new WGDefaultWorkflowEngine();

    // Utility objects
    protected ThreadLocal<Boolean> recursiveEventSemaphore = new ThreadLocal<Boolean>();

    private PageRightsFilter _pageRightsFilter = new DefaultPageRightsFilter();

    private long updateTimeout = Long.MIN_VALUE;

    private boolean allowCacheMaintenance = true;

    private Set unmodifiableDoctypes = new HashSet();

    private boolean readerProfileCreation = true;

    private boolean projectMode = false;

    /**
     * Defines, if incremental cache maintenance is allowed for this database.
     * Defaults to true (if the database type supports it). If set to false, the
     * database will completely clear all caches once the data is manipulated in
     * the background
     */
    public static final String COPTION_ALLOWCACHEMAINTENANCE = "AllowCacheMaintenance";

    /**
     * Defines if an online deletion check for documents should be enabled which
     * costs performance. This is disabled by default.
     */
    public static final String COPTION_DELETIONCHECK = "DeletionCheck";

    /**
     * Enables/Disables project mode. In project mode no versioning and
     * workflows are used.
     */
    public static final String COPTION_PROJECTMODE = "ProjectMode";

    /**
     * Portlet registry mode "transient", where registrations are only kept for each individual request.
     */
    public static final String PORTLETREGISTRYMODE_TRANSIENT = "transient";

    /**
     * Portlet registry mode "persistent" where registrations are stored on the user profile and persisted.
     */
    public static final String PORTLETREGISTRYMODE_PERSISTENT = "persistent";

    /**
     * The user name that is returned for the user of a WGA master session
     */
    public static final String MASTER_USERNAME = "Master Session";

    /**
     * Number of documents in a cache list that may need to be retrieved to rebuild the cache. If more docs need to be retrieved the cache will not be used.
     */
    public static final String COPTION_LIST_CACHE_REBUILD_THRESHOLD = "ListCacheRebuildThreshold";

    // lockMangager for locks of objects in this db
    private LockManager _lockManager = new LockManager();

    private AuthenticationModule _authenticationModule = null;

    /**
     * Injected default language by outer code
     */
    private volatile String _defaultLanguage = null;

    private volatile boolean connected = false;

    private String type;

    private String typeName;

    private int userCacheLatency = 30;

    private Collator _defaultLanguageCollator = null;

    private NoItemBehaviour noItemBehaviour;

    private boolean deletionCheck = false;

    private List<DatabaseAction> _connectActions = new ArrayList<DatabaseAction>();
    private List<DatabaseAction> _openActions = new ArrayList<DatabaseAction>();

    private boolean autoApprove = true;
    private boolean pageReadersEnabled = true;

    private boolean userCachesEnabled = true;

    private int listCacheRebuildThreshold = 10;

    private WGDatabaseServer server;

    private boolean _defaultLanguageAutoDetermined = false;

    private List<String> _mandatoryReaders = Collections.emptyList();

    private boolean _maintainOperationKeys;

    private Version _complianceVersion = CSConfig.getComplianceVersion(VersionCompliance.VERSIONCOMPLIANCE_DEFAULT);

    private String _uuid = null;
    private double _csVersion;
    private int _csPatchLevel;
    private int _cacheLatency;
    private Object _cacheMaintenanceMonitor = new Object();

    /**
     * Option to disable certificate authentication even when the auth module to use has certauth enabled
     */
    public static final String COPTION_DISABLE_CERTAUTH = "DisableCertAuth";

    /**
     * Option to fake certificate authentication. Database will enable certauth, no matter the auth module, and accept all X509 certificates verification
     */
    public static final String COPTION_FAKE_CERTAUTH = "FakeCertAuth";

    /**
     * A query option used on non-iterating query types, determining the size of a results fetch page to retrieve from the backend
     */
    public static final String QUERYOPTION_FETCHSIZE = "fetchsize";

    /**
     * Option to configure the maximum number of document objects to be kept in document cache
     */
    public static final String COPTION_DOCUMENTCACHESIZE = "DocumentCacheSize";

    /**
     * Option to configure the maximum number of document names to be kept in name cache
     */
    public static final String COPTION_NAMECACHESIZE = "NameCacheSize";

    /**
     * Option to configure the maximum number of query results to be kept in query cache
     */
    public static final String COPTION_QUERYCACHESIZE = "QueryCacheSize";

    /**
     * Option to configure if this database is clustered if used in an OpenWGA server cluster
     */
    public static final String COPTION_CLUSTERED = "Clustered";

    /**
     * Option to enable legacy DBCP 1 JMX monitoring, provided by WGA-own beans (actually only for databases using DBCP, ergo JDBC databases)
     */
    public static final String COPTION_LEGACY_DBCP_MONITORING = "JDBC.LegacyDBCPMonitoring";

    /**
     * Service to clear all user profiles of the personalisation database in one fast batch operation.
     * As this operation invalidates all existing profiles attached to sessions it should not be executed on productive environments.
     * This operation will leave no trace in historylog.
     */
    public static final String BACKENDSERVICE_CLEAR_USERPROFILES = "clearUserProfiles";

    /**
     * Service to effectively determine the version number for a new content document
     */
    public static final String BACKENDSERVICE_NEW_CONTENT_VERSION = "newContentVersion";

    /**
     * Service to clear all areas, struct entries and content documents from the content store in one fast batch operation.
     * This operation will leave no trace in historylog.
     * Schema from design may not be enforced after this service. Enforce manually or reopen the database to enforce again.
     */
    public static final String BACKENDSERVICE_CLEAR_CONTENT = "clearContent";

    /**
     * Service to clear all content (areas, structs and contents) and schema (contenttypes and language definitions) from the content store in one fast batch operation.
     * This operation will leave no trace in historylog.
     * Schema from design may not be enforced after this service. Enforce manually or reopen the database to enforce again.
     */
    public static final String BACKENDSERVICE_CLEAR_CONTENT_AND_SCHEMA = "clearContentAndSchema";

    /**
     * Completely clears all data from the database, including all documents, the ACL, database metadata, historylog. Also resets system and custom sequences.
     * This operation will leave no trace in historylog (well, besides clearing it)
     * Schema from design may not be enforced after this service. Enforce manually or reopen the database to enforce again.
     */
    public static final String BACKENDSERVICE_CLEAR_DATABASE = "clearDatabase";

    /**
     * A backend service defining some daily maintenance operation, to execute on the first event thread run each day
     */
    public static final String BACKENDSERVICE_DAILY_MAINTENANCE = "dailyMaintenance";

    /**
     * Selects all documents that are pending release. Returns a {@link WGAbstractResultSet}.
     */
    public static final String BACKENDSERVICE_SELECT_PENDINGRELEASE_DOCS = "selectPendingReleaseDocuments";

    /**
     * Checks if a content document for a certain page exists in a given language and status
     */
    public static final String BACKENDSERVICE_PROBE_CONTENT = "probeContent";

    /**
     * Fetches multiple contents in one backend call by their content key
     */
    public static final String BACKENDSERVICE_FETCH_MULTI_CONTENT = "fetchMultiContent";

    /**
     * Backend services that are allowed to be called under user session
     */
    public static final Set<String> USER_BACKENDSERVICES = new HashSet<String>(
            Arrays.asList(new String[] { BACKENDSERVICE_PROBE_CONTENT, BACKENDSERVICE_NEW_CONTENT_VERSION }));

    private static final String USERCACHE_FILTEREDACCESS_PREFIX = "FilteredUserAccess:";
    private static final int CACHELATENCY_DEFAULT_NOT_UPDATEABLE_DBS = 1;

    /**
     * Protected constructor. Construction from outside only via WGFactory
     * object.
     * 
     * @param server
     *            The database server that this database resides on
     * @param strType
     *            Type of database implementation (i.e. full qualified name of
     *            the database implementation class)
     * @param strPath
     *            Path to database. Interpreted by the implementation and
     *            therefor varying by implementation
     * @param strUserName
     *            Login user name
     * @param strUserPwd
     *            Password of user
     * @param options
     *            Map of implementation specific options.
     * @throws WGAPIException
     */
    protected WGDatabase(WGDatabaseServer server, String strType, String strPath, String strUserName,
            String strUserPwd, Map<String, String> options, boolean prepareOnly) throws WGAPIException {

        // Initialize basic members
        this.server = server;
        this.type = strType;
        this.path = strPath;
        this.masterLoginInputName = strUserName;
        this.masterLoginName = strUserName;
        this.masterLoginPassword = strUserPwd;

        this.userHashMapGroup = new UserHashMapGroup();
        this._userCache = userHashMapGroup.newUserHashMap("dbCacheUserCache");

        // Setting creation options
        if (options != null) {
            WGUtils.putAllNonNullValues(options, this.creationOptions);
        }

        if (this.creationOptions.containsKey(COPTION_DBREFERENCE)) {
            setDbReference(String.valueOf(this.creationOptions.get(COPTION_DBREFERENCE)));
        } else {
            setDbReference(getPath());
        }

        // Try to get implementation class and open database
        connectDBCore(prepareOnly);
        Map<String, Object> genericCacheParams = new HashMap<String, Object>();
        genericCacheParams.put(Cache.PARAM_TIME_TO_LIVE_SECONDS, _cacheLatency * 60);

        // Create master document cache
        Map<String, Object> params = new HashMap<String, Object>(genericCacheParams);
        params.put(Cache.PARAM_DISPOSAL_LISTENER, new DocumentCacheDisposalListener());
        int docCacheCapacity = DOCUMENTCACHE_SIZE_DEFAULT;
        if (options.containsKey(WGDatabase.COPTION_DOCUMENTCACHESIZE)) {
            docCacheCapacity = Integer.parseInt((String) options.get(WGDatabase.COPTION_DOCUMENTCACHESIZE));
        }
        try {
            this.masterDocumentsByKey = CacheFactory.createCache("WGAPIDocumentByKeyCache_" + getDbReference(),
                    docCacheCapacity, params);
        } catch (CacheException e) {
            throw new WGSystemException("Exception creating master document cache", e);
        }

        // Create master name cache
        params = new HashMap<String, Object>(genericCacheParams);
        int nameCacheCapacity = NAMECACHE_SIZE_DEFAULT;
        if (options.containsKey(WGDatabase.COPTION_NAMECACHESIZE)) {
            nameCacheCapacity = Integer.parseInt((String) options.get(WGDatabase.COPTION_NAMECACHESIZE));
        }
        try {
            this.masterDocumentByName = CacheFactory.createCache("WGAPIContentByNameCache_" + getDbReference(),
                    nameCacheCapacity, params);
        } catch (CacheException e) {
            throw new WGSystemException("Exception creating master name cache", e);
        }

        // Create master query cache
        params = new HashMap<String, Object>(genericCacheParams);
        int queryCacheCapacity = QUERYCACHE_SIZE_DEFAULT;
        if (options.containsKey(WGDatabase.COPTION_QUERYCACHESIZE)) {
            queryCacheCapacity = Integer.parseInt((String) options.get(WGDatabase.COPTION_QUERYCACHESIZE));
        }
        try {
            this.masterQueryCache = CacheFactory.createCache("WGAPIQueryCache_" + getDbReference(),
                    queryCacheCapacity, params);
        } catch (CacheException e) {
            throw new WGSystemException("Exception creating master query cache", e);
        }

        this.ready = true;

        // Execute connect actions that may have been registered while the db got connected 
        if (isConnected()) {
            notifyDatabaseActions(_connectActions);
        }

        _maintainOperationKeys = Boolean
                .parseBoolean(System.getProperty(SYSPROPERTY_MAINTAIN_OPERATIONKEYS, "false"));

    }

    private void createDBCoreObject(String strType) throws WGInvalidDatabaseException {
        Class dbClass;
        try {
            dbClass = Class.forName(strType, true, WGFactory.getImplementationLoader());

            if (!WGDatabaseCore.class.isAssignableFrom(dbClass)) {
                throw new WGInvalidDatabaseException("Cannot allocate db type " + strType
                        + ": is no implementation of " + WGDatabaseCore.class.getName());
            }
            this.core = (WGDatabaseCore) dbClass.newInstance();
        } catch (ClassNotFoundException exc) {
            throw new WGInvalidDatabaseException(
                    "Database implementation class or dependent class not found. Check classpath: "
                            + exc.getMessage());
        } catch (NoClassDefFoundError err) {
            throw new WGInvalidDatabaseException(
                    "Database implementation class or dependent class not found. Check classpath: "
                            + err.getMessage());
        } catch (IllegalAccessException exc) {
            throw new WGInvalidDatabaseException(
                    "Database implementation class or dependent class not accessible: " + exc.getMessage());
        } catch (InstantiationException exc) {
            throw new WGInvalidDatabaseException("Could not instantiate implementation class: " + exc.getMessage());
        }
    }

    /**
     * Creates the connection to the database backend, instantiates and opens
     * the db core
     * 
     * @param prepareOnly
     *            Specify false to only prepare the database for connection, but
     *            leave it unconnected after this method, standing by for the
     *            first session to be opened.
     * @return true, if the method connected the core, false if not (when the
     *         core already was connected)
     * @throws WGAPIException
     */
    /**
     * @param prepareOnly
     * @return true if the db has actually been connected, false if it has just been prepared
     * @throws WGAPIException
     */
    private synchronized boolean connectDBCore(boolean prepareOnly) throws WGAPIException {
        WGUserAccess userAccess;

        // Prevent running when already connected
        if (connected) {
            return false;
        }

        _connectActions.clear();
        _openActions.clear();

        // / Create core object
        createDBCoreObject(this.type);

        // Inject reconfiguration accessor
        if (this.core instanceof WGReconfigurableDatabaseCore) {
            ((WGReconfigurableDatabaseCore) this.core).injectReconfigurationAccessor(new ReconfigurationAccessor());
        }

        // Open database
        userAccess = this.core.open(this, path, masterLoginName, masterLoginPassword, prepareOnly);
        if (userAccess.getAccessLevel() <= WGDatabase.ACCESSLEVEL_NOACCESS) {
            throw new WGInvalidDatabaseException(
                    "Master login " + masterLoginName + " either has wrong password or no access to database");
        }

        reconfigureCore(prepareOnly, userAccess);

        return true;
    }

    private void reconfigureCore(boolean prepareOnly, WGUserAccess userAccess) throws WGAPIException {
        // Initialize data from core
        if (this.title == null) {
            this.title = this.core.getTitle();
        }

        this.typeName = this.core.getTypeName();

        // Initialize CS roles
        List<?> roles = this.core.getRoles();
        if (roles.contains(WGDatabase.ROLE_CONTENT)) {
            this.contentRole = true;
        }
        if (roles.contains(WGDatabase.ROLE_DESIGN)) {
            this.designRole = true;
        }
        if (roles.contains(WGDatabase.ROLE_REPOSITORY)) {
            this.repositoryRole = true;
        }
        if (roles.contains(WGDatabase.ROLE_USERPROFILES)) {
            this.userProfilesRole = true;
        }

        // Initialize max cores setting
        if (creationOptions.containsKey(COPTION_MAXCORES)) {
            try {
                maxCores = Integer.parseInt((String) creationOptions.get(COPTION_MAXCORES));
            } catch (NumberFormatException e) {
                WGFactory.getLogger().error("Cannot interpret creation option " + COPTION_MAXCORES
                        + " as an integer. Defaulting to " + maxCores);
            }
            if (maxCores == 0) {
                maxCores = Integer.MAX_VALUE;
            }
        }

        // Initialize background monitoring for database changes
        if (!prepareOnly) {
            this.monitorLastChange = this.core.hasFeature(FEATURE_LASTCHANGED);

            if (creationOptions.containsKey(COPTION_MONITORLASTCHANGE)) {
                monitorLastChange = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_MONITORLASTCHANGE)))
                        .booleanValue();
            }
        }

        // Initialize caching enablement
        if (creationOptions.containsKey(COPTION_CACHING_ENABLED)) {
            cachingEnabled = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_CACHING_ENABLED)))
                    .booleanValue();
        }

        // Initialize cache maintenance
        if (creationOptions.containsKey(COPTION_ALLOWCACHEMAINTENANCE)) {
            allowCacheMaintenance = Boolean
                    .valueOf(String.valueOf(creationOptions.get(COPTION_ALLOWCACHEMAINTENANCE))).booleanValue();
        }

        // Online deletion check for documents
        if (creationOptions.containsKey(COPTION_DELETIONCHECK)) {
            deletionCheck = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_DELETIONCHECK)))
                    .booleanValue();
        }

        // Project mode
        if (creationOptions.containsKey(COPTION_PROJECTMODE)) {
            projectMode = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_PROJECTMODE))).booleanValue();
            WGFactory.getLogger().info("Enabled project mode for database " + getDbReference());
        }

        // Initialize reader profile creation
        if (creationOptions.containsKey(COPTION_READERPROFILECREATION)) {
            readerProfileCreation = Boolean
                    .valueOf(String.valueOf(creationOptions.get(COPTION_READERPROFILECREATION))).booleanValue();
        }

        // Initialize page rights filter
        if (creationOptions.containsKey(COPTION_PAGERIGHTSFILTER)) {
            String filterName = (String) creationOptions.get(COPTION_PAGERIGHTSFILTER);
            try {
                @SuppressWarnings("unchecked")
                Class<? extends PageRightsFilter> filterClass = (Class<? extends PageRightsFilter>) WGFactory
                        .getImplementationLoader().loadClass(filterName);
                PageRightsFilter filter = filterClass.newInstance();
                _pageRightsFilter = filter;
            } catch (Exception e) {
                throw new WGIllegalArgumentException("Exception instantiating page rights filter: " + filterName,
                        e);
            }
        }
        onConnect(new DatabaseAction() {
            @Override
            public void run(WGDatabase db) throws Exception {
                getPageRightsFilter().init(db);
            }

        });

        if (creationOptions.containsKey(COPTION_AUTOAPPROVE)) {
            autoApprove = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_AUTOAPPROVE))).booleanValue();
        }

        pageReadersEnabled = this.core.hasFeature(FEATURE_FULLCONTENTFEATURES)
                && this.core.getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5;
        if (creationOptions.containsKey(COPTION_PAGEREADERS_ENABLED)) {
            pageReadersEnabled = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_PAGEREADERS_ENABLED)))
                    .booleanValue();
        }
        if (creationOptions.containsKey(COPTION_USERCACHES_ENABLED)) {
            userCachesEnabled = Boolean.valueOf(String.valueOf(creationOptions.get(COPTION_USERCACHES_ENABLED)))
                    .booleanValue();
        }

        // Initialize update timeout for background changes
        if (creationOptions.containsKey(COPTION_UPDATETIMEOUT)) {
            try {
                updateTimeoutSeconds = Integer.parseInt((String) creationOptions.get(COPTION_UPDATETIMEOUT));
                WGFactory.getLogger().info("Update timeout is set to " + updateTimeoutSeconds + " seconds.");
            } catch (NumberFormatException e) {
                WGFactory.getLogger().error("Cannot parse db option " + COPTION_UPDATETIMEOUT + " value "
                        + creationOptions.get(COPTION_UPDATETIMEOUT));
            }
        }

        // Initialize user cache latency
        if (creationOptions.containsKey(COPTION_USERCACHELATENCY)) {
            String latencyStr = (String) creationOptions.get(COPTION_USERCACHELATENCY);
            try {
                userCacheLatency = Integer.parseInt(latencyStr);
            } catch (NumberFormatException e) {
                WGFactory.getLogger().error("Cannot parse db option " + COPTION_USERCACHELATENCY + " value "
                        + creationOptions.get(COPTION_USERCACHELATENCY));
            }
        }

        // Initialize other caches latency
        _cacheLatency = 0;
        if (!hasFeature(FEATURE_FIND_UPDATED_DOCS)) {
            _cacheLatency = CACHELATENCY_DEFAULT_NOT_UPDATEABLE_DBS;
        }
        if (creationOptions.containsKey(COPTION_CACHELATENCY)) {
            String latencyStr = (String) creationOptions.get(COPTION_CACHELATENCY);
            try {
                _cacheLatency = Integer.parseInt(latencyStr);
            } catch (NumberFormatException e) {
                WGFactory.getLogger().error("Cannot parse db option " + COPTION_CACHELATENCY + " value "
                        + creationOptions.get(COPTION_CACHELATENCY));
            }
        }

        // Setup user hashmaps
        if (!hasFeature(FEATURE_CONTENT_READ_PROTECTION)) {
            this.userHashMapGroup.setSingleUserMode(true);
        } else if (userCacheLatency != 0) {
            _userCache.setMapLatency(userCacheLatency * 1000 * 60);
        }

        // Initialize list cache rebuild threshold
        if (creationOptions.containsKey(COPTION_LIST_CACHE_REBUILD_THRESHOLD)) {
            String thresholdStr = (String) creationOptions.get(COPTION_LIST_CACHE_REBUILD_THRESHOLD);
            try {
                listCacheRebuildThreshold = Integer.parseInt(thresholdStr);
                WGFactory.getLogger()
                        .info("User list cache rebuild threshold is set to " + listCacheRebuildThreshold);
            } catch (NumberFormatException e) {
                WGFactory.getLogger().error("Cannot parse db option " + COPTION_LIST_CACHE_REBUILD_THRESHOLD
                        + " value " + thresholdStr);
            }
        }

        // Init mandatory readers
        if (creationOptions.containsKey(COPTION_MANDATORY_READERS)) {
            try {
                _mandatoryReaders = (List<String>) JSONListOptionType.INSTANCE
                        .unconvert((String) creationOptions.get(COPTION_MANDATORY_READERS));
            } catch (OptionConversionException e) {
                WGFactory.getLogger().error("Cannot parse db option " + COPTION_MANDATORY_READERS + " value "
                        + creationOptions.get(COPTION_MANDATORY_READERS));
            }
        }

        // Init content store version
        _csVersion = this.core.getContentStoreVersion();
        _csPatchLevel = this.core.getContentStorePatchLevel();

        // Init last modified date
        updateRevision(WGDatabaseRevision.forValue(this.core.getRevision()));

        // Init value of nonexistent items
        noItemBehaviour = new NoItemBehaviour();
        if (creationOptions.containsKey(COPTION_NOITEMBEHAVIOUR)) {
            String noItemValueProp = (String) creationOptions.get(COPTION_NOITEMBEHAVIOUR);
            if (noItemValueProp.equalsIgnoreCase("compatible")) {
                noItemBehaviour.compatibleForDBImplementation(core);
            }
        }

        // If the db should only get prepared, close the core again
        if (prepareOnly) {
            core.close();
            // core = null; Not safe. Many processes rely on an existing core
            connected = false;
        }

        // Else open a master session
        else {
            // Open a session context
            WGSessionContext sessionContext = new WGSessionContext(this, MasterLoginAuthSession.getInstance(), null,
                    userAccess, null);
            this.setSessionContext(sessionContext);

            // Initialize default language collator
            try {
                _defaultLanguageCollator = Collator
                        .getInstance(WGLanguage.languageNameToLocale(getDefaultLanguage()));
            } catch (Exception e) {
                WGFactory.getLogger()
                        .error("Error determining default language collator. Using default platform collator", e);
                _defaultLanguageCollator = Collator.getInstance();
            }

            // Initialize custom workflow engine
            initWorkflowEngine();

            // Set database UUID if not yet present
            if (getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5
                    && !getTypeName().equals(WGFakeDatabase.DBTYPE)) {
                _uuid = (String) getExtensionData(EXTDATA_DATABASE_UUID);
                if (_uuid == null) {
                    _uuid = UUID.randomUUID().toString();
                    writeExtensionData(EXTDATA_DATABASE_UUID, _uuid);
                }
            }
            connected = true;
        }

    }

    private void initWorkflowEngine() throws WGAPIException {

        Class engineClass = null;

        // Custom workflow engine configured by creation option
        if (getCreationOptions().containsKey(COPTION_WORKFLOWENGINE)) {
            String engineClassName = (String) this.getCreationOptions().get(COPTION_WORKFLOWENGINE);
            try {
                engineClass = WGFactory.getImplementationLoader().loadClass(engineClassName);
            } catch (ClassNotFoundException e) {
                throw new WGConfigurationException("Cannot load Workflow engine class " + engineClassName, e);
            }
        }

        // Dedicated workflow engine for the current db implementation 
        else if (getCore().getDedicatedWorkflowEngine() != null) {
            engineClass = getCore().getDedicatedWorkflowEngine();
        }

        // Default workflow engine
        else {
            engineClass = WGFactory.getDefaultWorkflowEngine();
        }

        if (!WGWorkflowEngine.class.isAssignableFrom(engineClass)) {
            throw new WGConfigurationException("Workflow engine class " + engineClass.getName()
                    + " does not implement " + WGWorkflowEngine.class.getName());
        }

        try {
            WGWorkflowEngine engine;
            if (WGFactory.getModuleRegistry() != null) {
                engine = (WGWorkflowEngine) WGFactory.getModuleRegistry().instantiate(engineClass);
            } else {
                engine = (WGWorkflowEngine) engineClass.newInstance();
            }
            engine.init(this);
            workflowEngine = engine;
        } catch (WGAPIException e) {
            throw e;
        } catch (Throwable e) {
            throw new WGBackendException("Exception initializing workflow engine", e);
        }

    }

    /**
     * Triggers checking for database updates.
     * @param force Whether to force the check. Specifying false will respect the update timeout configured.
     * @return true if updates were detected and processed
     * @throws WGAPIException
     */
    protected boolean checkDatabaseUpdates(boolean force) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (this.monitorLastChange == false) {
            return false;
        }

        // No updates while in transaction
        if (getSessionContext().isTransactionActive()) {
            return false;
        }

        // Update timeout?
        if (!force && updateTimeout != 0 && updateTimeout >= System.currentTimeMillis()) {
            return false;
        }
        updateTimeout = 0;

        // This method is already called higher in the call stack - so do
        // nothing
        if (this.recursiveEventSemaphore.get() != null) {
            return false;
        }
        this.recursiveEventSemaphore.set(new Boolean(true));
        try {
            boolean processed = false;

            // Outer test, if updating is needed. Faster, bc. there is no
            // synchronisation
            if (isDatabaseUpdatedInBackground()) {

                synchronized (_cacheMaintenanceMonitor) {

                    // When in synchronized block, test again, bc. another
                    // thread may already have performed update while
                    // this task was waiting for the sync lock to be released
                    WGDatabaseRevision newLastChanged = WGDatabaseRevision.forValue(getCore().getRevision());
                    if (isDatabaseUpdatedInBackground()) {

                        verboseCacheManagement("Cache management starting. Database date changed from " + _revision
                                + " to " + newLastChanged);

                        // No other task performing update at the moment - so
                        // refresh right here right now
                        if (hasFeature(FEATURE_FIND_UPDATED_DOCS) && allowCacheMaintenance == true) {
                            List<WGUpdateLog> changeLogs = getCore().getUpdateLogs(_revision.getRevisionValue());
                            processChanges(changeLogs);
                        } else {
                            refresh();
                        }

                        // Update some flags, clear some (not maintainable)
                        // caches and fire event

                        this.getSessionContext().setDatabaseUpdated(true);

                        // No more neccessary? Is done in processChanges() or refresh()
                        // this.fireDatabaseEvent(new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_UPDATE, null));

                        this.getCore().refresh();
                        updateTimeout = System.currentTimeMillis() + (1000 * updateTimeoutSeconds);
                        verboseCacheManagement("Cache management ended");
                        processed = true;

                    }
                    updateRevision(newLastChanged);

                }

            }

            return processed;

        } finally {
            this.recursiveEventSemaphore.remove();
        }
    }

    /**
     * Returns if the database has been updated in background and caches may not
     * reflect the most up-to-date state, because cache management has not yet
     * processed these updates
     */
    public boolean isDatabaseUpdatedInBackground() throws WGAPIException {
        Comparable revisionValue = getCore().getRevision();
        if (revisionValue == null) {
            return false;
        }

        WGDatabaseRevision newRevision = WGDatabaseRevision.forValue(revisionValue);
        WGDatabaseRevision cacheRevision = _revision;
        if (newRevision.isProbablyNewerThan(cacheRevision)) {
            if (newRevision.isUniqueValue()) {
                return true;
            } else if (newRevision.equals(cacheRevision)) {
                if (cacheRevision.isLaterEqualRevisionValuePossible()) {
                    return true;
                } else {
                    return false;
                }
            } else {
                return true;
            }
        } else {
            return false;
        }
    }

    /**
     * Processes a list of updates, that occured in the background. Caches will
     * be maintained and events will be fired.
     * 
     * @param updatedDocuments
     *            List of WGUpdateLog objects, representing database updates
     * @throws WGAPIException
     */
    private void processChanges(List<WGUpdateLog> updatedDocuments) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        verboseCacheManagement("Performing incremental cache update");

        if (getSessionContext().isTransactionActive() || !hasFeature(FEATURE_FIND_UPDATED_DOCS)) {
            return;
        }

        if (updatedDocuments == null) {
            WGFactory.getLogger().error("No updated documents provided for change processing");
            return;
        }

        // Iterate through update events
        Iterator<WGUpdateLog> changes = updatedDocuments.iterator();
        while (changes.hasNext()) {
            Thread.yield();
            WGUpdateLog log = changes.next();
            verboseCacheManagement("Processing change log " + log.getRevision().getRevisionValue().toString()
                    + ", Type " + log.getType() + ", Target " + log.getDocumentKey() + ", Transactional: "
                    + getSessionContext().isTransactionActive());

            // Call backend change listeners
            for (WGBackendChangeListener listener : this.backendChangeListeners) {
                listener.processChange(this, log);
            }

            // Filter out rows that do not refer to document updates (like ACL
            // entries)
            if (log.getDocumentKey().startsWith("$")) {
                verboseCacheManagement("Non-document change log. Skipped");
                continue;
            }

            WGDocumentKey docKey = new WGDocumentKey(log.getDocumentKey());

            // Perform all neccessary operation for the given type of update
            if (log.getType() == WGUpdateLog.TYPE_UPDATE) {
                try {
                    clearNonexistentDocIndicatorForDocument(log.getDocumentKey());
                    WGDocument doc = getDocumentByKey(log.getDocumentKey());
                    if (doc != null) {
                        WGDatabaseRevision cacheRevision = doc.getCacheRevision();
                        if (cacheRevision != null && log.getRevision() != null && cacheRevision.isUniqueValue()
                                && cacheRevision.compareTo(log.getRevision()) >= 0) {
                            verboseCacheManagement("Document already on cache revision. Skipped");
                            continue;
                        }
                    }

                    verboseCacheManagement("Performing creation/update of doc " + log.getDocumentKey());
                    documentSaved(doc, docKey, true, false, log.getRevision());

                    // If metadata module was updated in background we drop the complete cache
                    // as it might symbolize the end of a batch update process (F00004402)
                    if (log.getDocumentKey().startsWith(WGCSSJSModule.METADATA_MODULE_QUALIFIER)) {
                        verboseCacheManagement(
                                "Update of metadata module " + log.getDocumentKey() + ". Complete cache refresh");
                        refresh();
                    }
                } catch (WGDeletedException e) {
                    // Silently ignore here. Most likely this update log refers a backend entity that has already been deleted in the meantime.
                    // Continue processing to pickup deletion log (#00002163)
                }

            } else if (log.getType() == WGUpdateLog.TYPE_DELETE) {
                try {
                    WGDocument doc = getDocumentByDocumentKeyFromCache(docKey, false);
                    if (doc != null) {
                        WGDatabaseRevision cacheRevision = doc.getCacheRevision();
                        if (cacheRevision != null && log.getRevision() != null && cacheRevision.isUniqueValue()
                                && cacheRevision.compareTo(log.getRevision()) >= 0) {
                            verboseCacheManagement("Document already on cache revision. Skipped");
                            continue;
                        }
                    }

                    verboseCacheManagement("Performing deletion of doc " + log.getDocumentKey());
                    documentRemoved(doc, docKey, false, (docKey.getDocType() == WGDocument.TYPE_CONTENT),
                            log.getRevision());
                } catch (WGDocumentDoesNotExistException e) {
                    // NOOP: Was deleted and already marked as nonexistent
                }
            } else if (log.getType() == WGUpdateLog.TYPE_STRUCTMOVE) {
                try {
                    WGStructEntry struct = (WGStructEntry) getDocumentByKey(log.getDocumentKey());
                    if (struct != null) {
                        verboseCacheManagement("Performing struct movage of doc " + log.getDocumentKey());
                        processMovedDocuments(struct, false);
                    }
                } catch (WGDeletedException e) {
                    // Silently ignore here. Most likely this update log refers a backend entity that has already been deleted in the meantime.
                    // Continue processing to pickup deletion log (#00002163)
                }
            } else {
                WGFactory.getLogger().warn("Unknown log type: " + log.getType());
            }

            getSessionContext().clearCache();
        }

        // Throw global update event so event listeners may update
        this.fireDatabaseEvent(new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_UPDATE));

    }

    /**
     * Removes a nodoc placeholder for the given document to be able to load
     * this document when it just has been created
     * 
     * @param documentKey
     *            The document key of the document
     */
    private void clearNonexistentDocIndicatorForDocument(String documentKey) {
        WGDocumentKey key = new WGDocumentKey(documentKey);
        clearNonexistentDocIndicator(masterDocumentsByKey, String.valueOf(key));
    }

    private void clearNonexistentDocIndicator(Cache cache, String cacheKey) {
        try {
            Object entry = cache.read(cacheKey);
            if (entry instanceof NullPlaceHolder) {
                cache.flush(cacheKey);
            }
        } catch (CacheException e) {
            WGFactory.getLogger().error("Exception clearing NDI master document cache", e);
        }
    }

    private void verboseCacheManagement(String msg) {
        if (VERBOSE_CACHE_MANAGEMENT) {
            WGFactory.getLogger().info("Cache Management DB:" + getDbReference() + " - " + msg);
        }
    }

    protected void verboseMutableCloning(Object obj) {
        if (VERBOSE_MUTABLE_CLONING) {
            WGFactory.getLogger().info(
                    "Mutable cloning DB:" + getDbReference() + " - Object of type " + obj.getClass().getName());
        }
    }

    protected void verboseBackendAccess(int operation, Object key) {
        if (VERBOSE_BACKEND_ACCESS) {

            int docType = determineBackendAccessDocType(operation, key);
            if (docType == 0) { // Means: Is no backend access
                return;
            }

            if (docType == -1 || VERBOSE_BACKEND_ACCESS_DOCTYPES.size() == 0
                    || VERBOSE_BACKEND_ACCESS_DOCTYPES.contains(docType)) {
                WGFactory.getLogger()
                        .info("Backend Access DB:" + getDbReference() + " - Operation: "
                                + WGOperationKey.getOperationName(operation) + " - Key: "
                                + WGUtils.strReplace(String.valueOf(key), "\n", "", true) + " - Cache: "
                                + (!getSessionContext().isCachingEnabled() ? "disabled"
                                        : !getSessionContext().isCacheWritingEnabled() ? "readonly" : "enabled")
                                + " - Session: " + getSessionContext().hashCode() + " - Task: "
                                + getSessionContext().getTask());
            }
        }
    }

    private int determineBackendAccessDocType(int operation, Object key) {

        switch (operation) {

        case WGOperationKey.OP_CONTENT_BY_NAME:
        case WGOperationKey.OP_QUERY:
        case WGOperationKey.OP_QUERY_RESULT_COUNT:
        case WGOperationKey.OP_QUERY_HAS_RESULTS:
        case WGOperationKey.OP_STRUCT_CONTENTS:
        case WGOperationKey.OP_STRUCT_CONTENTS_INCLUDING_ARCHIVED:
            return WGDocument.TYPE_CONTENT;

        case WGOperationKey.OP_DESIGN_LIST:
            if (key instanceof Number) {
                return ((Number) key).intValue();
            } else {
                return -1;
            }

        case WGOperationKey.OP_DOCUMENT_BY_KEY:
        case WGOperationKey.OP_DOCUMENT_CORE:
        case WGOperationKey.OP_DOCUMENT_CORE_FASTACCESS:
        case WGOperationKey.OP_SAVE:
        case WGOperationKey.OP_DELETE:
            if (key instanceof WGDocumentKey) {
                return ((WGDocumentKey) key).getDocType();
            } else if (key instanceof String) {

            } else {
                return -1;
            }

        case WGOperationKey.OP_HDB_CREATE_STORAGE:
        case WGOperationKey.OP_HDB_GET_OR_CREATE_STORAGE:
            return 0;

        case WGOperationKey.OP_STRUCT_PARENT:
        case WGOperationKey.OP_STRUCT_ROOTS:
        case WGOperationKey.OP_STRUCT_CHILDREN:
            return WGDocument.TYPE_STRUCTENTRY;

        default:
            return -1;
        }

    }

    /**
     * Refreshes the data in database, testing for changes in the backend and
     * clearing all caches.
     * 
     * @throws WGAPIException
     */
    public void refresh() throws WGAPIException {

        synchronized (_cacheMaintenanceMonitor) {
            if (!isSessionOpen()) {
                throw new WGClosedSessionException();
            }

            verboseCacheManagement("Performing complete cache refresh");

            if (getSessionContext().isTransactionActive()) {
                //clear session cache            
                getSessionContext().clearCache();
                return;
            }

            // obtain a dblock - to ensure no documents are locked
            this.lock();

            this.getSessionContext().setDatabaseUpdated(true);
            this.clearDocumentMappings();
            this.getCore().refresh();
            clearUserCaches();
            getSessionContext().clearCache();
            this.fireDatabaseEvent(new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_UPDATE));

            // unlock db
            this.unlock();
        }
    }

    /**
     * Closes the database session. The database object and all child objects.
     * 
     * @throws WGAPIException
     */
    public void closeSession() throws WGAPIException {
        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        WGSessionContext oldContext = getSessionContext();
        oldContext.preClose();

        // Notify design provider
        if (designProvider != null) {
            designProvider.closeSession();
        }

        try {
            getCore().closeSession();
        } finally {

            oldContext.close();
            if (oldContext != null && oldContext.isMaxCoreMessageShowed()) {
                addToStatistics(oldContext);
            }

            // release obtained locks of sessioncontext
            getLockManager().releaseAllLocks(oldContext);

            setSessionContext(null);
        }
    }

    /**
     * Called to drop all retrieved document cores for this session at once.
     * 
     * @param untimelyDispose
     */
    public void dropAllDocumentCores(boolean untimelyDispose) {

        getSessionContext().dropAllDocumentCores(untimelyDispose);

    }

    /**
     * Adds a session statistic about the current session to collected session
     * statistics.
     * 
     * @param oldContext
     */
    private void addToStatistics(WGSessionContext oldContext) {

        sessionStatistics.add(new SessionStatistic(oldContext));
        if (sessionStatistics.size() > 100) {
            sessionStatistics.remove(sessionStatistics.first());
        }

    }

    /**
     * Closes and reopens the session with the given user information
     * 
     * @param username
     * @param credentials
     * @return the access level of the new session
     * @throws WGAPIException
     */
    public int reopenSession(String username, Object credentials) throws WGAPIException {
        return reopenSession(username, credentials, null);
    }

    /**
     * Closes and reopens the session with the given user information
     * 
     * @param username
     * @param credentials
     * @param filter ID of the user access filter to apply. Specify null for none.
     * @return the access level of the new session
     * @throws WGAPIException
     */
    public int reopenSession(String username, Object credentials, String filter) throws WGAPIException {

        if (isSessionOpen()) {
            closeSession();
        }

        int accessLevel = openSession(username, credentials, filter);
        if (accessLevel > WGDatabase.ACCESSLEVEL_NOTLOGGEDIN) {
            // Do an "out of the order" cache maintenance now, to ensure the new session is on the most current state
            // as reopenSession() is often used to catch-up with changes done on another thread
            catchupBackendChanges();
        }

        return accessLevel;

    }

    /**
     * Closes and reopens the session with the current logged in user. If no
     * session is open, opens a master session.
     * 
     * @return The access level in the new session
     * @throws WGAPIException
     */
    public int reopenSession() throws WGAPIException {

        if (!isSessionOpen() || getSessionContext().isMasterSession()) {
            return reopenSession(null, null);
        } else {
            return reopenSession(getSessionContext().getUser(), getSessionContext().getCredentials());
        }

    }

    /**
     * Removed a document and does all neccessary operations after that. -
     * Maintain caches - Throw events - Set states Either parameter document or
     * log must be present
     * 
     * @param document
     *            The document that is/was removed. Can be null if it was
     *            deleted in background and not in cache
     * @param docKey
     *            The document key of the document removed
     * @param removedByWGAPI
     *            true if the removing is triggered by WGAPI and should be
     *            executed in this method.
     * @return True if deletion succeeded
     * @throws WGAPIException
     */
    protected boolean remove(WGDocument document) throws WGAPIException {

        // Prepare deletion
        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        // Only neccessary if document was in cache = document is present
        boolean dropQueryCache = false;

        // check document lock
        if (document.getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException("Unable to remove document. Document is locked.");
        }

        // check child locks of document for current session context
        if (getLockManager().foreignChildLocksExists(document, getSessionContext())) {
            throw new ResourceIsLockedException(
                    "Unable to remove document. Child objects of the object you want to remove are locked.");
        }

        // remove all relations pointing to me
        // + look if we need to clear the query cache
        if (document instanceof WGContent) {
            WGContent content = (WGContent) document;
            content.removeAllIncomingRelations();
            if (content.getRetrievalStatus().equals(WGContent.STATUS_RELEASE)) {
                dropQueryCache = true;
            }

        }

        // Ensure the document knows its cache parents
        if (document instanceof WGStructEntry) {
            WGStructEntry struct = (WGStructEntry) document;
            struct.getParentEntry();
            struct.getArea();
        }

        // Delete
        verboseBackendAccess(WGOperationKey.OP_DELETE, document.getDocumentKeyObj());
        WGDatabaseRevision revision = document.getCore().remove();

        documentRemoved(document, document.getDocumentKeyObj(), true, dropQueryCache, revision);
        return true;

    }

    private void documentRemoved(WGDocument document, WGDocumentKey docKey, boolean removedByWGAPI,
            boolean dropQueryCache, WGDatabaseRevision revision) throws WGAPIException {

        if (getSessionContext().isTransactionActive()) {
            return;
        }

        // Clear the document cache
        masterDocumentCaches.remove(docKey);

        // Clear the caches on the cached document object(s) and its cache parents, mark them deleted
        if (document != null) {
            document.setEdited(false);
            document.setDeleted(true);
            for (WGDocumentKey cacheParentKey : document.getCacheParents()) {
                WGDocument cacheParent = getDocumentByDocumentKeyFromCache(cacheParentKey);
                if (cacheParent != null) {
                    cacheParent.dropCache();
                }
            }

        }

        WGDocument cachedDocument = unmapDocumentObject(docKey);
        if (cachedDocument != null && cachedDocument != document) {
            cachedDocument.setEdited(false);
            cachedDocument.setDeleted(true);
            for (WGDocumentKey cacheParentKey : cachedDocument.getCacheParents()) {
                WGDocument cacheParent = getDocumentByDocumentKeyFromCache(cacheParentKey);
                if (cacheParent != null) {
                    cacheParent.dropCache();
                }
            }
        }

        // Clear indirect caches
        if (document == null && isDesignDocumentType(docKey.getDocType())) {
            designDocumentLists.clear();
        }

        _userCache.clear();
        if (dropQueryCache) {
            try {
                verboseCacheManagement("Flushing query cache on behalf of removed document " + docKey.toString());
                masterQueryCache.flushAll();
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception flushing query cache on database '" + getDbReference() + "'",
                        e);
            }
        }

        // Fire content has been deleted event
        if (docKey.getTypename().equals(WGDocument.TYPENAME_CONTENT)) {
            WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_HASBEENDELETED, docKey.toString(), null,
                    this);
            if (document != null) {
                event.setContent((WGContent) document);
            }
            fireContentEvent(event);
        }

        // Fire design change event
        if (docKey.getTypename().equals(WGDocument.TYPENAME_FILECONTAINER)
                || docKey.getTypename().equals(WGDocument.TYPENAME_TML)
                || docKey.getTypename().equals(WGDocument.TYPENAME_CSSJS)) {
            // Only fire when no design provider present. Otherwise the
            // listeners are registered at the provider and it is his
            // responsibility to throw the event
            if (getDesignProvider() == null) {
                List<WGUpdateLog> logs = new ArrayList<WGUpdateLog>();
                logs.add(new WGUpdateLog(WGUpdateLog.TYPE_DELETE, new Date(), getSessionContext().getUser(),
                        docKey.toString(), null, revision));
                fireDatabaseDesignChangedEvent(new WGDesignChangeEvent(null, this, logs));
            }
        }

        // Remove the session context, so we won't refetch it by the
        // cached document there
        getSessionContext().removeDocumentContext(docKey);

        // Fire database event if the remove has been done by WGAPI
        // When this is done in background change processing, the event
        // will get fired after all changes by checkDatabaseUpdates
        updateCacheMaintenanceData();
        if (removedByWGAPI) {
            this.getSessionContext().setDatabaseUpdated(true);
            fireDatabaseEvent(new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_UPDATE, docKey));
        }

        if (document != null && revision != null && !getSessionContext().isTransactionActive()) {
            document.setCacheRevision(revision);
        }
    }

    /**
     * Returns the area with the given name
     * 
     * @param strName
     *            Name of the area
     * @return WGArea
     * @throws WGAPIException
     */
    public WGArea getArea(String strName) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return (WGArea) getDesignObject(WGDocument.TYPE_AREA, strName, null);

    }

    /**
     * Returns a map of all areas of this database, mapped by their area name.
     * 
     * @return WGAreaMap
     * @throws WGAPIException
     */
    public WGAreaMap getAreas() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        List areasList = getDesignObjects(WGDocument.TYPE_AREA);
        if (areasList == null) {
            return null;
        }

        Iterator areas = areasList.iterator();
        TreeMap areasMap = new TreeMap();

        WGArea area;
        while (areas.hasNext()) {
            area = (WGArea) areas.next();
            areasMap.put(area.getName(), area);
        }

        return new WGAreaMap(areasMap);
    }

    /**
     * Returns a struct entry by it's page sequence
     * 
     * @param seq            The page sequence number
     * @return WGStructEntry    The found struct entry, null if there is none with that key.
     * @throws WGAPIException
     */
    public WGStructEntry getStructEntryBySequence(long seq) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }
        if (!(getCore() instanceof WGDatabaseCoreFeaturePageSequences))
            throw new WGNotSupportedException("Page sequences are not supported for this database");

        WGDocumentCore struct = ((WGDatabaseCoreFeaturePageSequences) getCore()).getStructEntryBySequence(seq);
        if (struct != null && !struct.isDeleted()) {
            return this.getOrCreateStructEntryObject(struct, new WGDocumentObjectFlags());
        }

        return null;

    }

    /**
     * Returns a struct entry by it's struct key
     * 
     * @param structKey         The struct key to find a struct entry for.
     * @return WGStructEntry    The found struct entry, null if there is none with that key.
     * @throws WGAPIException
     */
    public WGStructEntry getStructEntryByKey(Object structKey) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return (WGStructEntry) getDocumentByKey(
                new WGDocumentKey(WGDocument.TYPE_STRUCTENTRY, String.valueOf(structKey), null));
    }

    private WGStructEntry retrieveStructEntry(Object structKey) throws WGAPIException {

        WGStructEntry entry = null;

        WGDocumentCore entryCore = this.getCore().getStructEntryByKey(structKey);
        if (entryCore != null && !entryCore.isDeleted()) {
            entry = this.getOrCreateStructEntryObject(entryCore, new WGDocumentObjectFlags());
        } else {
            mapNonExistentDocIndicator(masterDocumentsByKey,
                    String.valueOf(new WGDocumentKey(WGDocument.TYPE_STRUCTENTRY, String.valueOf(structKey), null)),
                    WGDocument.TYPE_STRUCTENTRY);
        }

        return entry;
    }

    /**
     * Wraps a struct entry core into a WGStructEntry object
     * 
     * @param doc
     *            core to wrap
     * @return WGStructEntry
     * @throws WGAPIException
     */
    protected WGStructEntry getOrCreateStructEntryObject(WGDocumentCore doc, WGDocumentObjectFlags flags)
            throws WGAPIException {

        WGDocumentKey key = WGDocument.buildDocumentKey(doc, this);
        WGStructEntry entry;
        try {
            entry = (WGStructEntry) getDocumentByDocumentKeyFromCache(key, false);
        } catch (WGDocumentDoesNotExistException e) {
            entry = null;
        }

        if (entry == null) {
            entry = new WGStructEntry(this, doc, flags);
            if (entry.getStructKey() != null) {

                // Directly fill some very relevant caches once we have the core
                if (!entry.isTemporary() && !entry.isDummy() && !"true".equals(
                        getSessionContext().getAttribute(WGSessionContext.SATTRIB_BYPASS_PREEMTIVE_CACHING))) {
                    entry.getMetaData(WGStructEntry.META_KEY);
                    entry.getMetaData(WGStructEntry.META_AREA);
                    entry.getMetaData(WGStructEntry.META_POSITION);
                    entry.getMetaData(WGStructEntry.META_CONTENTTYPE);
                }

                mapDocumentObject(entry);
            }
        } else {
            if (!entry.isEdited()) {
                entry.setCore(doc);
            }
        }
        return entry;

    }

    /**
     * Returns the child entries of a given struct entry
     * 
     * @param structEntry
     *            struct entry to retrieve child entries for
     * @return WGAPIException
     */
    protected WGStructEntryRetrievalIterator getChildEntries(WGStructEntry structEntry, WGPageOrderSet pageOrder)
            throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (structEntry == null || structEntry.isDeleted() || structEntry.isDummy()
                || (structEntry.getCore() instanceof WGFakeDocument && structEntry.getCore().isDeleted())) {
            return WGStructEntryRetrievalIterator.emptyIterator();
        }

        return new WGStructEntryRetrievalIterator(this, structEntry,
                this.getCore().getChildEntries(structEntry, pageOrder));

    }

    /**
     * Returns all content for a struct entry
     * 
     * @param structEntry
     *            Struct entry, whose content is to be retrieved
     * @return WGContentList
     * @throws WGAPIException
     */
    protected WGContentList getAllContent(WGStructEntry structEntry, boolean includeArchived)
            throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        try {
            if (structEntry == null || structEntry.isDeleted() || structEntry.isDummy()
                    || (structEntry.getCore() instanceof WGFakeDocument && structEntry.getCore().isDeleted())) {
                return WGContentList.create();
            }
        } catch (WGDeletedException e) {
            return WGContentList.create();
        }

        ArrayList contents = new ArrayList();
        List contentCoresList = this.getCore().getAllContent(structEntry, includeArchived);
        if (contentCoresList != null) {
            Iterator contentCores = contentCoresList.iterator();

            WGDocumentCore contentCore;
            WGContent content;

            while (contentCores.hasNext()) {
                contentCore = (WGDocumentCore) contentCores.next();
                content = getOrCreateContentObject(contentCore);

                if (content != null) {
                    contents.add(content);
                }
            }
        }

        return WGContentList.create(contents);

    }

    /**
     * Returns the released content of the given struct entry and language without retrieving anything other
     * 
     * @param entry The struct entry
     * @param strLanguage The language to retrieve.
     * @throws WGAPIException 
     */
    protected WGContent getReleasedContent(WGStructEntry entry, String strLanguage) throws WGAPIException {

        if (strLanguage == null) {
            strLanguage = getDefaultLanguage();
        }

        WGContentKey relContentKey = new WGContentKey(entry.getStructKey(), strLanguage, 0);
        return retrieveContentByKey(relContentKey);

    }

    /**
     * Returns on operation key for syncing of backend access. Should not be
     * used outside the WGAPI.
     * 
     * @param operation
     * @param key
     * @return The key for the given operation. Can be a new created one or an
     *         existing key for this operation, if there was one.
     */
    public WGOperationKey obtainOperationKey(int operation, Object key) {

        if (_maintainOperationKeys) {
            try {
                _wgOperationKeySemaphore.acquireUninterruptibly();
                return performObtainOperationKey(operation, String.valueOf(key));
            } finally {
                _wgOperationKeySemaphore.release();
            }
        } else {
            return performObtainOperationKey(operation, String.valueOf(key));
        }
    }

    private WGOperationKey performObtainOperationKey(int operation, String key) {
        String operationKeyStr = WGOperationKey.createString(operation, key);
        WGOperationKey opKey = (WGOperationKey) this.operations.get(operationKeyStr);
        if (opKey != null) {
            return opKey;
        } else {
            opKey = new WGOperationKey(this, operation, key);
            this.operations.put(operationKeyStr, opKey);
            return opKey;
        }
    }

    /**
     * Returns all content for a struct entry
     * 
     * @param structEntry
     *            Struct entry, whose content is to be retrieved
     * @return WGContentList
     * @throws WGAPIException
     */
    protected WGContentList getAllContent(WGStructEntry structEntry) throws WGAPIException {
        return this.getAllContent(structEntry, false);
    }

    /**
     * Wraps a design document core into a matching WGDesignDocument object
     * 
     * @param doc
     *            core to wrap
     * @return WGDesignDocument
     * @throws WGAPIException
     */
    private WGDesignDocument createDesignDocumentObject(WGDocumentCore doc, WGDocumentObjectFlags flags)
            throws WGAPIException {

        int type = doc.getType();
        WGDesignDocument design = null;

        if (type == WGDocument.TYPE_AREA) {
            design = new WGArea(this, doc, flags);
        } else if (type == WGDocument.TYPE_CONTENTTYPE) {
            design = new WGContentType(this, doc, flags);
        } else if (type == WGDocument.TYPE_TML) {
            design = new WGTMLModule(this, doc, flags);
        } else if (type == WGDocument.TYPE_FILECONTAINER) {
            design = new WGFileContainer(this, doc, flags);
        } else if (type == WGDocument.TYPE_LANGUAGE) {
            design = new WGLanguage(this, doc, flags);
        } else if (type == WGDocument.TYPE_CSSJS) {
            design = new WGScriptModule(this, doc, flags);
        }

        mapDocumentObject(design);
        return design;
    }

    /**
     * Returns a design object with the given information
     * 
     * @param type
     *            document type choosing, which kind of design object to
     *            retrieve. Use constants WGDocument.TYPE_...
     * @param strName
     *            Name of the design object
     * @param mediaKey
     *            media key of the design object. If design object has no media
     *            key, provide null.
     * @return WGDesignDocument
     * @throws WGAPIException
     */
    public WGDesignDocument getDesignObject(int type, String strName, String mediaKey) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (strName == null) {
            return null;
        }

        return (WGDesignDocument) getDocumentByKey(new WGDocumentKey(type, strName, mediaKey));
    }

    private WGDesignDocument retrieveDesignDocument(int type, String strName, String mediaKey)
            throws WGAPIException {

        WGDesignDocument design = null;
        WGDocumentCore designCore = getDesignObjectCore(type, strName, mediaKey);
        if (designCore != null && !designCore.isDeleted()) {
            design = this.getOrCreateDesignDocumentObject(designCore, new WGDocumentObjectFlags());
        } else {
            mapNonExistentDocIndicator(masterDocumentsByKey,
                    (new WGDocumentKey(type, strName, mediaKey)).toString(), type);
        }

        return design;

    }

    protected WGDocumentCore getDesignObjectCore(int type, String strName, String mediaKey) throws WGAPIException {
        String mediaKeyParam = (mediaKey != null ? mediaKey.toLowerCase() : null);
        String designNameParam = toLowerCaseMeta(strName);

        if (designProvider != null && designProvider.providesType(type) && !isMetadataModule(type, strName)) {
            return designProvider.getDesignObject(type, designNameParam, mediaKeyParam);
        } else {
            return this.getCore().getDesignObject(type, designNameParam, mediaKeyParam);
        }
    }

    private boolean isMetadataModule(int type, String strName) {

        return (type == WGDocument.TYPE_CSSJS && strName.startsWith(WGCSSJSModule.METADATA_MODULE_QUALIFIER));

    }

    /**
     * Returns a design object with the given information. In this method the
     * media key is omitted. Therefor it is not suitable to retrieve WebTML
     * Modules
     * 
     * @param type
     *            document type choosing, which kind of design object to
     *            retrieve. Use constants WGDocument.TYPE_...
     * @param strName
     *            Name of the design object
     * @return WGDesignDocument
     * @throws WGAPIException
     */
    public WGDesignDocument getDesignObject(int type, String strName) throws WGAPIException {
        return this.getDesignObject(type, strName, null);
    }

    /**
     * Returns the root entries to an area.
     * 
     * @param area
     *            The area to fetch root entries for
     * @return WGStructEntryList
     * @throws WGAPIException
     * @throws WGAPIException
     */
    protected WGStructEntryRetrievalIterator getRootEntries(WGArea area, WGPageOrderSet order)
            throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (area == null || area.isDeleted() || area.isDummy()
                || (area.getCore() instanceof WGFakeDocument && area.getCore().isDeleted())) {
            return WGStructEntryRetrievalIterator.emptyIterator();
        }

        return new WGStructEntryRetrievalIterator(this, area, this.getCore().getRootEntries(area, order));

    }

    /**
     * Returns the title of this database (if there is one).
     * 
     * @return String
     */
    public String getTitle() {

        return title;

    }

    /**
     * Registers a listerer for database events.
     * 
     * @param listener
     *            The listener to register.
     */
    public void addDatabaseEventListener(WGDatabaseEventListener listener) {

        if (listener.isTemporary()) {
            this.getSessionContext().addTemporaryEventListener(listener);
        } else {
            if (!this.databaseEventListeners.contains(listener)) {
                this.databaseEventListeners.add(listener);
            }
        }

    }

    /**
     * Adds a listener for the connection of this database to its backend.
     * 
     * @param listener
     */
    public void addDatabaseConnectListener(WGDatabaseConnectListener listener) {

        if (!this.databaseConnectListeners.contains(listener)) {
            this.databaseConnectListeners.add(listener);
        }

    }

    /**
     * Removes a listener for the connection of this database to its backend
     * 
     * @param listener
     */
    public void removeDatabaseConnectListener(WGDatabaseConnectListener listener) {
        this.databaseConnectListeners.remove(listener);
    }

    /**
     * Adds a listener for content events to the database.
     * 
     * @param listener
     */
    public void addContentEventListener(WGContentEventListener listener) {

        if (!contentEventListeners.contains(listener)) {
            this.contentEventListeners.add(listener);
        }

    }

    /**
     * Adds a listener for workflow events to the database
     * 
     * @param listener
     */
    public void addWorkflowEventListener(WGWorkflowEventListener listener) {

        if (!this.workflowEventListeners.contains(listener)) {
            this.workflowEventListeners.add(listener);
        }

    }

    /**
     * Removes a database event listener.
     */
    public void removeDatabaseEventListener(WGDatabaseEventListener listener) {

        if (listener.isTemporary()) {
            return;
        }

        this.databaseEventListeners.remove(listener);

    }

    /**
     * Removes a content event listener.
     */
    public void removeContentEventListener(WGContentEventListener listener) {

        this.contentEventListeners.remove(listener);

    }

    /**
     * Removes a workflow event listener.
     */
    public void removeWorkflowEventListener(WGWorkflowEventListener listener) {

        this.workflowEventListeners.remove(listener);

    }

    /**
     * Fires a database event and notifies all listeners
     * 
     * @param event
     *            The event to throw
     */
    private void fireDatabaseEvent(WGDatabaseEvent event) {

        if (!isSessionOpen()) {
            return;
        }

        if (!getSessionContext().isEventsEnabled()) {
            return;
        }

        Iterator listeners = this.databaseEventListeners.iterator();
        while (listeners.hasNext()) {
            WGDatabaseEventListener listener = (WGDatabaseEventListener) listeners.next();
            listener.databaseUpdate(event);
        }

    }

    /**
     * Fires a event, that signals that this database has been connected to its
     * backend or that this process resulted in an error.
     * 
     * @param event
     *            The event to fire
     */
    private void fireDatabaseConnectEvent(WGDatabaseEvent event) {

        if (!isSessionOpen()) {
            if (event.getType() != WGDatabaseEvent.TYPE_CONNECTION_ERROR) {
                return;
            }
        } else if (!getSessionContext().isEventsEnabled()) {
            return;
        }

        Iterator listeners = this.databaseConnectListeners.iterator();
        while (listeners.hasNext()) {
            WGDatabaseConnectListener listener = (WGDatabaseConnectListener) listeners.next();
            if (event.getType() == WGDatabaseEvent.TYPE_CONNECTED) {
                listener.databaseConnected(event);
            } else if (event.getType() == WGDatabaseEvent.TYPE_CONNECTION_ERROR) {
                listener.databaseConnectionError(event);
            }
        }

    }

    protected boolean fireContentEvent(WGContentEvent event) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!getSessionContext().isEventsEnabled()) {
            return true;
        }

        Iterator listeners = this.contentEventListeners.iterator();
        while (listeners.hasNext()) {
            WGContentEventListener listener = (WGContentEventListener) listeners.next();
            if (event.getType() == WGContentEvent.TYPE_CREATED) {
                listener.contentCreated(event);
            } else if (event.getType() == WGContentEvent.TYPE_SAVED) {
                if (listener.contentSaved(event) == false) {
                    return false;
                }
            } else if (event.getType() == WGContentEvent.TYPE_HASBEENSAVED) {
                listener.contentHasBeenSaved(event);
            } else if (event.getType() == WGContentEvent.TYPE_HASBEENDELETED) {
                listener.contentHasBeenDeleted(event);
            } else if (event.getType() == WGContentEvent.TYPE_HASBEENMOVED) {
                listener.contentHasBeenMoved(event);
            } else if (event.getType() == WGContentEvent.TYPE_STATUSCHANGED) {
                listener.contentStatusChanged(event);
            }
        }

        return true;
    }

    /**
     * Fires a workflow event. Not to be called outside the WGAPI.
     * 
     * @param event
     * @return A list of results that were returned by the event recipients
     */
    public List fireWorkflowEvent(WGWorkflowEvent event) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!getSessionContext().isEventsEnabled()) {
            return new ArrayList();
        }

        List resultList = new ArrayList();

        Iterator listeners = this.workflowEventListeners.iterator();
        while (listeners.hasNext()) {
            WGWorkflowEventListener listener = (WGWorkflowEventListener) listeners.next();
            listener.workflowMail(event);
        }

        return resultList;
    }

    /**
     * Opens a new session.
     * 
     * @param user
     *            The username for the new session. Specify null for master
     *            login.
     * @param credentials
     *            The password to the username. Specify null for master login.
     * @return The access level to the database, as constant
     *         WGDatabase.ACCESSLEVEL_...
     * @throws WGAPIException
     */
    public int openSession(String user, Object credentials) throws WGAPIException {
        return innerOpenSession(user, credentials, null, null);
    }

    /**
     * Opens a session as anonymous user
     * @throws WGAPIException
     */
    public int openAnonymousSession() throws WGAPIException {
        return openSession(WGDatabase.ANONYMOUS_USER, null);
    }

    /**
     * Opens a session as anonymous user, including an access filter
     * @param accessFilter The filter id
     * @throws WGAPIException
     */
    public int openAnonymousSession(String accessFilter) throws WGAPIException {
        return openSession(WGDatabase.ANONYMOUS_USER, null, accessFilter);
    }

    /**
     * Opens a new session.
     * 
     * @param user
     *            The username for the new session. Specify null for master
     *            login.
     * @param credentials
     *            The password to the username. Specify null for master login.
     * @param filter
     *            A user access filter to reduce the user rights
     * @return The access level to the database, as constant
     *         WGDatabase.ACCESSLEVEL_...
     * @throws WGAPIException
     */
    public int openSession(String user, Object credentials, String filter, HttpServletRequest request)
            throws WGAPIException {
        return innerOpenSession(user, credentials, filter, request);
    }

    public int openSession(String user, Object credentials, String filter) throws WGAPIException {
        return innerOpenSession(user, credentials, filter, null);
    }

    private int innerOpenSession(String user, Object credentials, String accessFilterUid,
            HttpServletRequest request) throws WGAPIException {

        if (!isReady()) {
            throw new WGUnavailableException(this, "The database is currently not ready for operation");
        }

        if (this.isSessionOpen()) {
            if (user == null && getSessionContext().isMasterSession()) {
                return this.getSessionContext().getAccessLevel();
            } else if (isMemberOfUserList(Collections.singletonList(user))) {
                return this.getSessionContext().getAccessLevel();
            } else {
                closeSession();
            }
        }

        // Determine if this database yet has to be connected to the backend
        if (!isConnected()) {
            synchronized (this) {
                if (!isConnected()) {
                    boolean wasConnected = false;
                    try {
                        wasConnected = connectDBCore(false);
                    } catch (WGInvalidDatabaseException e) {
                        WGDatabaseEvent event = new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_CONNECTION_ERROR);
                        fireDatabaseConnectEvent(event);
                        throw new WGUnavailableException(this, "Unable to connect to database core", e);
                    }

                    // Fire the connect event in case connectDBCore has connected the
                    // core
                    // If this is false, the current thread most likely was blocked on
                    // connectDBCore() by some other
                    // thread that built the core connection.So this thread entered the
                    // method when the core was
                    // already connected.
                    if (wasConnected == true) {
                        WGDatabaseEvent event = new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_CONNECTED);
                        fireDatabaseConnectEvent(event);
                        notifyDatabaseActions(_connectActions);
                        // We close the (master) session that resulted from connecting
                        // the core
                        // We will open the real user session later
                        closeSession();
                    }
                }
            }
        }

        WGFactory.getLogger().debug("WGDatabase open session " + path);

        // If both params are null, the master login (from initial open command)
        // will be used
        boolean masterLogin = false;

        // Create authentication session
        AuthenticationSession authSession = null;
        AuthenticationModule authModule = getAuthenticationModule();

        // Master login
        if (user == null && credentials == null) {
            user = this.masterLoginInputName;
            credentials = this.masterLoginPassword;
            masterLogin = true;
            authSession = MasterLoginAuthSession.getInstance();
        }

        // Anonymous login
        else if (user.equals(WGDatabase.ANONYMOUS_USER)) {
            if (authModule != null && authModule instanceof AnonymousAwareAuthenticationModule)
                authSession = ((AnonymousAwareAuthenticationModule) authModule).anonymousLogin(request);
            else
                authSession = AnonymousAuthSession.getInstance();
        }

        // Regular login against authentication module
        else if (authModule != null) {
            if (certAuthEnabled() && (credentials instanceof X509Certificate)) {
                authSession = ((CertAuthCapableAuthModule) authModule).login((X509Certificate) credentials);
            } else {
                authSession = authModule.login(user, credentials);
            }

            if (authSession == null) {
                return WGDatabase.ACCESSLEVEL_NOTLOGGEDIN;
            }
        }

        // Backend login
        else if (hasFeature(FEATURE_PERFORMS_BACKEND_LOGIN)) {
            authSession = new BackendAuthSession(user, credentials);
        }

        // If no auth module and database does not accept backend logins we use an anonymous session
        else {
            authSession = AnonymousAuthSession.getInstance();
        }

        // Open core session
        WGUserAccess userAccess = this.core.openSession(authSession, credentials, masterLogin);
        if (userAccess.getAccessLevel() <= ACCESSLEVEL_NOACCESS) {
            return userAccess.getAccessLevel();
        }

        WGUserAccess originalUserAccess = null;
        String userCacheKey = getUserCacheKey(authSession);
        if (accessFilterUid != null) {
            UserAccessFilter filter = WGFactory.getInstance().getUserAccessFilter(accessFilterUid);
            if (filter != null) {
                originalUserAccess = userAccess;
                Map userCache = getUserCache().getMapForUser(userCacheKey);
                userAccess = (WGUserAccess) userCache.get(USERCACHE_FILTEREDACCESS_PREFIX + accessFilterUid);
                if (userAccess == null || userAccess.isOutdated()) {
                    userAccess = originalUserAccess.applyUserAccessFilter(filter);
                    userCache.put(USERCACHE_FILTEREDACCESS_PREFIX + accessFilterUid, userAccess);
                }
                userCacheKey = accessFilterUid + "////" + userCacheKey;
            }
        }

        userHashMapGroup.fetchAllMapsForUser(userCacheKey);
        WGSessionContext sessionContext = new WGSessionContext(this, authSession, credentials, userAccess,
                originalUserAccess);
        sessionContext.setCachingEnabled(cachingEnabled);
        this.setSessionContext(sessionContext);
        core.setCurrentSession(sessionContext);

        // Notify design provider
        if (designProvider != null) {
            designProvider.openSession(sessionContext);
        }

        return userAccess.getAccessLevel();

    }

    /**
     * Opens a new session using a certificate for login.
     * To use certificate authentication you must use a {@link CertAuthCapableAuthModule} with enabled certificate authentication
     * @param cert
     *            The certificate used to login.
     * @param filter   
     *            A user access filter to reduce user rights
     * @return The access level to the database, as constant
     *         WGDatabase.ACCESSLEVEL_...
     * @throws WGAPIException
     */
    public int openSession(X509Certificate cert, String filter) throws WGAPIException {

        if (!"true".equals(getCreationOptions().get(COPTION_FAKE_CERTAUTH))) {

            if (!certAuthEnabled()) {
                WGFactory.getLogger().warn("WGAPI: Tried to access database " + getDbReference()
                        + " via certificate auth although it is not configured for it");
                return WGDatabase.ACCESSLEVEL_NOTLOGGEDIN;
            }

            if (cert == null) {
                return WGDatabase.ACCESSLEVEL_NOTLOGGEDIN;
            }

            CertAuthCapableAuthModule auth = (CertAuthCapableAuthModule) getAuthenticationModule();

            X509Certificate caCert = auth.getCA();
            X509CRL crl = auth.getCRL();

            // verify if clientCert is issued by given dbCA
            if (!CertificateValidationUtils.verify(cert, caCert)) {
                String message = "Failed login for '" + cert.getSubjectDN().getName()
                        + "': Certificate was signed by another CA (Certificate authentication)";
                WGFactory.getLogger().warn(message);
                return WGDatabase.ACCESSLEVEL_NOTLOGGEDIN;
            }

            // check if client certificate is revoked
            if (CertificateValidationUtils.isCertRevoked(crl, cert)) {
                String message = "Failed login for '" + cert.getSubjectDN().getName()
                        + "': Certificate was revoked by CRL (Certificate authentication)";
                WGFactory.getLogger().info(message);
                return WGDatabase.ACCESSLEVEL_NOTLOGGEDIN;
            }
        }

        String user = cert.getSubjectDN().toString();
        return innerOpenSession(user, cert, filter, null);

    }

    private synchronized void notifyDatabaseActions(List<DatabaseAction> actions) {

        Iterator<DatabaseAction> actionsIt = actions.iterator();
        while (actionsIt.hasNext()) {
            DatabaseAction action = actionsIt.next();
            try {
                action.run(this);
            } catch (Exception e) {
                WGFactory.getLogger().error("Error executing connect action", e);
            }
        }
        actions.clear();

    }

    /**
     * Opens a new session with master login.
     * 
     * @throws WGAPIException
     */
    public int openSession() throws WGAPIException {
        return this.openSession(null, null, null);
    }

    private List wrapDesignCores(List designCores) throws WGAPIException {

        if (designCores == null) {
            return null;
        }

        if (designCores.size() == 0) {
            designCores = new ArrayList();
        }

        List designs = new ArrayList();
        Iterator objectCores = designCores.iterator();

        WGDocumentCore designCore;
        WGDesignDocument design;
        String name;
        String mediaKey;

        while (objectCores.hasNext()) {
            designCore = (WGDocumentCore) objectCores.next();
            if (designCore == null || designCore.isDeleted()) {
                continue;
            }

            name = (String) designCore.getMetaData(WGDesignDocument.META_NAME);

            if (designCore.getType() == WGDocument.TYPE_TML) {
                mediaKey = (String) designCore.getMetaData(WGTMLModule.META_MEDIAKEY);
            } else {
                mediaKey = null;
            }

            design = getOrCreateDesignDocumentObject(designCore, new WGDocumentObjectFlags());
            if (design != null) {
                designs.add(design);
            } else {
                if (WGFactory.getLogger().isDebugEnabled()) {

                    WGFactory.getLogger()
                            .error("Could not retrieve design object "
                                    + WGDesignDocument.doctypeNumberToName(designCore.getType()) + "/" + name + "/"
                                    + mediaKey + ". Check name validity");
                }
            }
        }
        return designs;
    }

    /**
     * Returns the imlementation type (i.e. the class name of the database core
     * implementation)
     */
    public String getType() {

        return this.type;

    }

    /**
     * Returns a descriptive type name of the database implementation used.
     */
    public String getTypeName() {
        return typeName;
    }

    /**
     * Returns all design objects of a specific type.
     * 
     * @param type
     *            The type of design objects to retrieve. Use constants
     *            WGDocument.TYPE_...
     * @return ArrayList List of WGDesignObject objects
     * @throws WGAPIException
     */
    public List<? extends WGDesignDocument> getDesignObjects(int type) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        // Double checked cache (un-synchronized and synchronized)
        Integer typeObj = new Integer(type);
        List designs = fetchDocumentListCache((WGDocumentListCache) this.designDocumentLists.get(typeObj), type);
        if (!getSessionContext().isCachingEnabled() || designs == null) {
            WGOperationKey op = obtainOperationKey(WGOperationKey.OP_DESIGN_LIST, type);
            synchronized (op) {
                try {
                    op.setUsed(true);
                    designs = fetchDocumentListCache((WGDocumentListCache) this.designDocumentLists.get(typeObj),
                            type);
                    if (!getSessionContext().isCachingEnabled() || designs == null) {
                        designs = wrapDesignCores(getDesignObjectCores(type));
                        if (getSessionContext().isCacheWritingEnabled()) {
                            this.designDocumentLists.put(typeObj, WGDocumentListCache.buildFromDocuments(designs));
                        }
                    }
                } finally {
                    op.setUsed(false);
                }
            }
        }

        if (designs != null) {
            return designs;
        } else {
            return null;
        }

    }

    protected List<? extends WGDocument> fetchDocumentListCache(WGDocumentListCache listCache, int docType) {

        if (!isDoctypeCacheable(docType)) {
            return null;
        }

        if (listCache != null && isCachingEnabled()) {
            return listCache.buildDocumentList(this);
        } else {
            return null;
        }
    }

    protected List fetchDocumentListCache(WGDocumentListCache listCache, int docType, int offset, int size) {

        if (!isDoctypeCacheable(docType)) {
            return null;
        }

        if (listCache != null && isCachingEnabled()) {
            return listCache.buildDocumentSubList(this, offset, size);
        } else {
            return null;
        }
    }

    protected boolean isDoctypeCacheable(int docType) {
        boolean docTypeCacheable = true;
        if (getDesignProvider() != null) {
            if (!getDesignProvider().isNotifying() && getDesignProvider().providesType(docType)) {
                docTypeCacheable = false;
            }
        }
        return docTypeCacheable;
    }

    private List getDesignObjectCores(int type) throws WGAPIException {

        if (designProvider != null && designProvider.providesType(type)) {
            List designCores = designProvider.getDesignObjects(type);
            if (designCores != null && type == WGDocument.TYPE_CSSJS) {
                designCores = removeMetadataModules(designCores);
            }
            return designCores;
        } else {
            return this.getCore().getDesignObjects(type);
        }

    }

    private List removeMetadataModules(List designCores) throws WGAPIException {

        List results = new ArrayList();
        Iterator cores = designCores.iterator();
        WGDocumentCore core;
        while (cores.hasNext()) {
            core = (WGDocumentCore) cores.next();
            if (!isMetadataModule(core.getType(), (String) core.getMetaData(WGDesignDocument.META_NAME))) {
                results.add(core);
            }
        }
        return results;

    }

    /**
     * Retrieves a content by it's unique name.
     * 
     * @param strName
     *            Unique name of the content
     * @param strLanguage
     *            Language of the content
     * @return WGContent Content matching unique name and language, null if none
     *         exists.
     * @throws WGAPIException
     */
    public WGContent getContentByName(String strName, String strLanguage) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (strName == null || strName.trim().equals("")) {
            return null;
        }
        strName = strName.toLowerCase();

        String strCacheLanguage = strLanguage;
        if (strLanguage == null) {
            strCacheLanguage = getDefaultLanguage();
            if (strCacheLanguage == null) {
                strCacheLanguage = "de";
            }
        }

        strCacheLanguage = strCacheLanguage.toLowerCase();

        // Double checked cache (un-synchronized and synchronized)
        try {
            String byNameKey = buildUniqueNameCacheKey(strName, strCacheLanguage);
            WGContent content = (WGContent) fetchDocumentFromCache(this.masterDocumentByName, byNameKey);
            if (content == null || !isReleasedDocumentOfName(content, strName)) {
                WGOperationKey op = obtainOperationKey(WGOperationKey.OP_CONTENT_BY_NAME, strName);
                synchronized (op) {
                    try {
                        op.setUsed(true);
                        content = (WGContent) fetchDocumentFromCache(this.masterDocumentByName, byNameKey);
                        if (content == null || !isReleasedDocumentOfName(content, strName)) {
                            if (content != null) {
                                unmapUniqueName(strName);
                            }
                            content = retrieveContentByName(strName, strLanguage, byNameKey);
                        }
                    } finally {
                        op.setUsed(false);
                    }
                }
            }

            return content;

        } catch (WGDocumentDoesNotExistException e) {
            return null;
        }

    }

    private String buildUniqueNameCacheKey(String strName, String strCacheLanguage) {
        return strName.toLowerCase().concat("|").concat(strCacheLanguage.toLowerCase());
    }

    private boolean isReleasedDocumentOfName(WGDocument doc, String strName) throws WGAPIException {

        if (doc instanceof WGContent) {
            WGContent content = (WGContent) doc;

            if (!WGContent.STATUS_RELEASE.equals(content.getStatus())) {
                return false;
            }

            if (strName.equals(content.getUniqueName())) {
                return true;
            }

            if (strName.equals(content.getStructEntry().getUniqueName())) {
                return true;
            }
        } else if (doc instanceof WGStructEntry) {
            WGStructEntry struct = (WGStructEntry) doc;

            if (strName.equals(struct.getUniqueName())) {
                return true;
            }

        }

        return false;

    }

    private WGContent retrieveContentByName(String strName, String strLanguage, String byNameKey)
            throws WGAPIException {
        WGContent content = null;

        // First try to find a struct entry of that name. If a struct of that name exists, look if it has released contents.
        WGStructEntry struct = getStructEntryByName(strName);
        if (struct != null) {
            content = struct.getReleasedContent(strLanguage);
            if (content != null) {
                return content;
            } else {
                return null;
            }
        }

        // Then try to find a content with the uname/lang combination
        WGDocumentCore contentCore = getCore().getContentByName(strName, strLanguage);
        if (contentCore != null && !contentCore.isDeleted()) {
            content = getOrCreateContentObject(contentCore);
            if (content != null) {
                if (content.isReadableForUser()) {
                    return content;
                } else {
                    return null;
                }
            }
        }

        // No content to find, cache the "non-existence" then return null
        mapNonExistentDocIndicator(this.masterDocumentByName, byNameKey, WGDocument.TYPE_CONTENT);
        return null;

    }

    /**
     * Retrieves a struct entry which has the given unique name
     * This method will only return struct entries if they themselves own the unique name, not if any content of them owns it.
     * @param strName Unique name
     * @return A struct entry of this name or null if the name is not given to any struct entry.
     * @throws WGAPIException
     */
    public WGStructEntry getStructEntryByName(String strName) throws WGAPIException {

        if (getContentStoreVersion() < CSVERSION_WGA5) {
            return null;
        }

        // Double checked cache (un-synchronized and synchronized)
        try {
            WGStructEntry struct = (WGStructEntry) fetchDocumentFromCache(this.masterDocumentByName, strName);
            if (struct == null || !isReleasedDocumentOfName(struct, strName)) {
                WGOperationKey op = obtainOperationKey(WGOperationKey.OP_STRUCT_BY_NAME, strName);
                synchronized (op) {
                    try {
                        op.setUsed(true);
                        struct = (WGStructEntry) fetchDocumentFromCache(this.masterDocumentByName, strName);
                        if (struct == null || !isReleasedDocumentOfName(struct, strName)) {
                            if (struct != null) {
                                unmapUniqueName(strName);
                            }
                            struct = retrieveStructEntryByName(strName);
                        }
                    } finally {
                        op.setUsed(false);
                    }
                }
            }

            return struct;

        } catch (WGDocumentDoesNotExistException e) {
            return null;
        }

    }

    protected WGStructEntry retrieveStructEntryByName(String strName) throws WGAPIException {

        WGStructEntry struct = null;
        WGDocumentCore structCore = getCore().getStructEntryByName(strName);
        if (structCore != null) {
            struct = getOrCreateStructEntryObject(structCore, new WGDocumentObjectFlags());
        } else {
            mapNonExistentDocIndicator(this.masterDocumentByName, strName, WGDocument.TYPE_STRUCTENTRY);
        }
        return struct;

    }

    /**
     * Retrieves a content by it's unique name in the database's default
     * language.
     * 
     * @param name
     *            Unique name of the content
     * @return WGContent Content matching unique name, null if none exists.
     * @throws WGAPIException
     */
    public WGContent getContentByName(String name) throws WGAPIException {
        return getContentByName(name, getDefaultLanguage());
    }

    /**
     * Sets the sessionContext
     * 
     * @param sessionContext
     *            The sessionContext to set
     */
    private void setSessionContext(WGSessionContext sessionContext) {

        if (sessionContext == null) {
            this.sessionContext.remove();
        } else {
            this.sessionContext.set(sessionContext);
        }
    }

    /**
     * Sets a custom attribute to the database. These attributes remain at the
     * database object forever and can even be retrieved when no session is
     * open. The attributes can later be retrieved via getAttribute().
     * 
     * @param name
     *            The custom attribute name
     * @param obj
     *            The attribute value.
     */
    public void setAttribute(String name, Object obj) {
        if (obj != null) {
            this.customAttributes.put(name, obj);
        } else {
            this.customAttributes.remove(name);
        }
    }

    /**
     * Removes a database attribute.
     * 
     * @param name
     *            Name of the attribute to remove
     * @return The removed attribute value, if there was one, null otherwise
     */
    public Object removeAttribute(String name) {
        return this.customAttributes.remove(name);
    }

    /**
     * Returns a custom attribute set to this database.
     * 
     * @param name
     *            Name of the attribute
     * @return Object Value of the attribute
     */
    public Object getAttribute(String name) {
        return this.customAttributes.get(name);
    }

    /**
     * Tests if the database contains an attribute of the given name
     * 
     * @param name
     *            Attribute name
     * @return true, if attribute exists, false (suprise) if not
     */
    public boolean hasAttribute(String name) {
        return this.customAttributes.containsKey(name);
    }

    /**
     * Returns the boolean value of an attribute, accepting boolean objects and
     * those string representations that are converted by
     * WGUtils.stringToBoolean().
     * 
     * @param name
     *            The name of the Attribute
     * @param defaultValue
     *            The default value to return, if the attribute is not
     *            interpretable as boolean
     * @return The boolean value of the attribute, or the default value if the
     *         attribute was not present or it's boolean value was not
     *         determinable
     */
    public boolean getBooleanAttribute(String name, boolean defaultValue) {
        return WGUtils.getBooleanMapValue(customAttributes, name, defaultValue);
    }

    /**
     * Executes a query in the database giving a result set of content documents
     * in return.
     * 
     * @param type
     *            The type of query. Available types vary by database
     *            implementation.
     * @param query
     *            The query. Format depends on query type.
     * @param parameters
     *            Additional parameters for the query. Use Constants
     *            WGDatabase.QUERYOPTION_... as map keys.
     * @return A result set containing the contents matching the query
     * @throws WGAPIException
     */
    public WGAbstractResultSet query(String type, String query, Map parameters) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (parameters == null) {
            parameters = new HashMap();
        }

        Boolean cachedResultSet = (Boolean) parameters.get(WGDatabase.QUERYOPTION_CACHERESULT);
        if (cachedResultSet == null) {
            cachedResultSet = new Boolean(false);
        }

        QueryCacheKey queryCacheKey = new QueryCacheKey(type, query, new HashMap(parameters));
        if (cachedResultSet.booleanValue() == true) {
            QueryCacheEntry cacheEntry = null;
            ;
            try {
                cacheEntry = (QueryCacheEntry) masterQueryCache.read(queryCacheKey);
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception reading query cache on database '" + getDbReference() + "'",
                        e);
            }
            if (cacheEntry != null) {
                parameters.put(WGDatabase.QUERYOPTION_RETURNQUERY, cacheEntry.getFullQuery());
                parameters.put(WGDatabase.QUERYOPTION_USEDCACHE, new Boolean(true));
                return new WGStandardResultSet(this, cacheEntry.getResultSet(), parameters, query);
            }
        }

        verboseBackendAccess(WGOperationKey.OP_QUERY, query);
        WGResultSetCore resultSetCore = this.core.query(type, query, parameters);

        if (!parameters.containsKey(WGDatabase.QUERYOPTION_RETURNQUERY)) {
            parameters.put(WGDatabase.QUERYOPTION_RETURNQUERY, query);
        }

        if (cachedResultSet.booleanValue() == true && getSessionContext().isCacheWritingEnabled()) {
            WGCachedResultSet resultSet = new WGCachedResultSet(resultSetCore);
            try {
                masterQueryCache.write(queryCacheKey, new QueryCacheEntry(resultSet,
                        (String) parameters.get(WGDatabase.QUERYOPTION_RETURNQUERY)));
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception writing query cache on database '" + getDbReference() + "'",
                        e);
            }
            return new WGStandardResultSet(this, resultSet, parameters, query);
        } else {
            return new WGStandardResultSet(this, resultSetCore, parameters, query);
        }

    }

    /**
     * Queries the database for specific features.
     * 
     * @param featureName
     *            The feature to query. Use Constants WGDatabase.FEATURE_...
     * @return true if the database implementation supports this feature, false
     *         otherwise
     */
    public boolean hasFeature(String featureName) {

        Boolean feature = (Boolean) this.features.get(featureName);
        if (feature == null) {

            if (featureName.equals(WGDatabase.FEATURE_SESSIONTOKEN)) {
                feature = (_authenticationModule != null && _authenticationModule.isGeneratesSessionToken());
            } else {
                feature = new Boolean(this.getCore().hasFeature(featureName));
            }
            this.features.put(featureName, feature);
        }

        return feature.booleanValue();
    }

    /**
     * Returns the roles of this db implementation, describing what documents
     * can be found in it.
     * 
     * @return List List of constants WGDatabase.ROLE_... <br/>
     *         WGDatabase.ROLE_CONTENT: Database contains content documents and
     *         related (WGContent, WGStructEntry, WGArea) <br/>
     *         WGDatabase.ROLE_DESIGN: Database contains design documents
     *         (WGTMLModule, WGCSSJSLibrary) <br/>
     *         WGDatabase.ROLE_REPOSITORY: Database contains file containers
     *         (WGFileContainer) <br/>
     *         WGDatabase.ROLE_USERPROFILES: Database contains user profiles
     *         (WGUserProfile) <br/>
     */
    public List getRoles() {
        return this.getCore().getRoles();
    }

    /**
     * Wraps a content core into a WGContent object
     * 
     * @param doc
     *            content core to wrap
     * @return WGContent
     * @throws WGAPIException
     */
    protected WGContent createContentObject(WGDocumentCore doc) throws WGAPIException {

        WGContent content = new WGContent(this, doc);
        mapDocumentObject(content);
        return content;

    }

    /**
     * Maps the document object on diverse document caches at the database For
     * content objects this method should only be called for new and modified
     * documents as it will remove the content from all user's private caches
     * 
     * @param doc
     * @throws WGAPIException
     */
    private void mapDocumentObject(WGDocument doc) throws WGAPIException {

        if (doc == null || doc.isTemporary() || doc.isDummy()) {
            return;
        }

        if (!getSessionContext().isCacheWritingEnabled()) {
            return;
        }

        if (!isDoctypeCacheable(doc.getType())) {
            return;
        }

        if (doc instanceof WGUserProfile) {
            return;
        }

        WGDocumentKey documentKey = doc.getDocumentKeyObj();
        updateCache(masterDocumentsByKey, String.valueOf(documentKey), doc);

        if (doc instanceof WGContent) {
            WGContent content = (WGContent) doc;

            // Map uname from content
            String uniqueName = content.getUniqueName();
            if (!WGUtils.isEmpty(uniqueName)) {
                mapUniqueName(uniqueName, content);
            }

            // Map uname from struct
            uniqueName = content.getStructEntry().getUniqueName();
            if (!WGUtils.isEmpty(uniqueName)) {
                mapUniqueName(uniqueName, content);
            }
        } else if (doc instanceof WGStructEntry) {
            WGStructEntry struct = (WGStructEntry) doc;
            String uname = struct.getUniqueName();
            if (!WGUtils.isEmpty(uname)) {
                unmapUniqueName(uname);
            }
        }

    }

    /**
     * Adds a listener for backend changes to the database
     * @param listener
     */
    public void addBackendChangeListener(WGBackendChangeListener listener) {
        if (!this.backendChangeListeners.contains(listener)) {
            this.backendChangeListeners.add(listener);
        }
    }

    /**
     * Removes a listener for backend changes from the database
     * @param listener
     */
    public void removeBackendChangeListener(WGBackendChangeListener listener) {
        this.backendChangeListeners.remove(listener);
    }

    private void mapUniqueName(String uniqueName, WGContent content) throws WGAPIException {
        if (content.getStatus().equals(WGContent.STATUS_RELEASE)) {
            String uniqueNameKey = buildUniqueNameCacheKey(uniqueName, content.getLanguage().getName());
            updateCache(this.masterDocumentByName, uniqueNameKey, content.getDocumentKeyObj());
        }
    }

    private void unmapUniqueName(String uniqueName) throws WGAPIException {

        if (!getSessionContext().isCacheWritingEnabled()) {
            return;
        }

        try {
            this.masterDocumentByName.flush(uniqueName);
            for (WGLanguage lang : getLanguages().values()) {
                String uniqueNameKey = buildUniqueNameCacheKey(uniqueName, lang.getName());
                this.masterDocumentByName.flush(uniqueNameKey);
            }
        } catch (CacheException e) {
            throw new WGBackendException("Exception in cache maintenance", e);
        }

    }

    /**
     * Method to update cache maps with values. This method takes
     * {@link WGSessionContext#isCacheWritingEnabled()} into credit and refuses
     * to put the value in the cache if cache writing is disabled. In that case
     * it just clears the cache entry if the previous entry is not the same as
     * the entry to set.
     * 
     * @param map
     * @param key
     * @param value
     */
    private void updateCacheMap(Map map, Object key, Object value) {

        // In case the correct object is already in cache we do nothing
        if (map.get(key) == value) {
            return;
        }

        if (getSessionContext().isCacheWritingEnabled()) {
            map.put(key, value);
        } else {
            map.remove(key);
        }

    }

    private void updateCache(Cache cache, String key, Object value) {

        try {
            if (!getSessionContext().isCacheWritingEnabled()) {
                return;
            }

            // In case the correct object is already in cache we do nothing
            if (WGUtils.nullSafeEquals(cache.read(key), value)) {
                return;
            }

            cache.write(key, value);
        } catch (CacheException e) {
            WGFactory.getLogger().error("Exception updating master document cache", e);
        }

    }

    protected WGDocument unmapDocumentObject(WGDocumentKey docKey) throws WGAPIException {

        WGDocument oldDoc = null;
        try {
            oldDoc = (WGDocument) masterDocumentsByKey.read(docKey.toString());
            masterDocumentsByKey.flush(docKey.toString());
        } catch (CacheException e) {
            WGFactory.getLogger().error("Exception flushing entry on master document cache", e);
        }

        int docType = docKey.getDocType();
        if (isDesignDocumentType(docType)) {
            WGDocumentListCache designs = (WGDocumentListCache) this.designDocumentLists.get(docType);
            if (designs != null) {
                designs.remove(docKey);
            }
        }

        return oldDoc;
    }

    /**
     * Creates a new content document. This version can determine the version to
     * use and is mainly for cloning content.
     * 
     * @param entry
     *            struct Entry, that this new content document will be attached
     *            to
     * @param language
     *            Language of the new content
     * @param title
     *            Title of the new content
     * @param Integer
     *            version The needed version for the created document
     * @throws WGAPIException
     */
    protected WGContent createContent(WGStructEntry entry, WGLanguage language, String title, Integer version)
            throws WGAPIException {

        if (!this.isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && (entry == null || language == null || title == null)) {
            throw new WGIllegalArgumentException("Mandatory parameter (Struct entry, language or title) is empty");
        }

        performContentCreationCheck(entry, language);

        // Evaluate new version number
        int newVersion = 0;
        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            if (version != null) {
                newVersion = version.intValue();
            } else {
                newVersion = findNewVersionNumber(entry, language);
            }

        }

        // Create core and pre-initialize to be a valid WGContent object
        WGDocumentCore docCore = this.getCore().createContent(entry, language, title, newVersion);
        if (docCore == null) {
            throw new WGCreationException("Unable to create content. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of content");
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            docCore.setMetaData(WGContent.META_STATUS, WGContent.STATUS_DRAFT);
        }

        // Create WGContent object
        WGContent newContent = this.createContentObject(docCore);

        // Initialize
        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            newContent.setVisible(true);
        }
        newContent.setTitle(title);
        newContent.setMetaData(WGContent.META_AUTHOR, getSessionContext().getUser());
        if (getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5) {
            newContent.setMetaData(WGContent.META_OWNER, getSessionContext().getUser());
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {

            // Create predefined item/meta fields based on schema definition
            WGContentTypeDefinition schemaDef = (WGContentTypeDefinition) entry.getContentType()
                    .getSchemaDefinition();
            if (schemaDef != null) {
                for (WGContentItemDefinition itemDef : schemaDef.getContentItemDefinitions()) {
                    if (itemDef.hasInitialValue()) {
                        newContent.setItemValue(itemDef.getName(), itemDef.getInitialValue());
                    }
                }
                for (WGMetaFieldDefinition metaDef : schemaDef.getContentMetaDefinitions()) {
                    newContent.setMetaData(metaDef.getName(), metaDef.getValues());
                }
            }

        }

        // Event contentCreated
        String contentType = null;
        if (entry != null && entry.getContentType() != null) {
            contentType = entry.getContentType().getName();
        }
        WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_CREATED, newContent.getDocumentKey(),
                contentType, this);
        event.setContent(newContent);
        fireContentEvent(event);

        // return
        return newContent;

    }

    /**
     * Controls if the current user may create a content document with the given data. If so the method exists normally. If not an exception is thrown.
     * @param entry The struct entry under which to create the content. If given null all struct entry data checks are bypassed.
     * @param language The language in which to create it
     * @throws WGAPIException If the user may not create the document. The exception informs about the reason.
     */
    public void performContentCreationCheck(WGStructEntry entry, WGLanguage language)
            throws WGAPIException, ResourceIsLockedException, WGAuthorisationException, WGIllegalStateException {
        // check entry lock
        if (entry != null && entry.getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException("Unable to create content. Given structentry is locked.");
        }

        if (getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_AUTHOR) {
            throw new WGAuthorisationException("You are not authorized to create content in this database",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_AUTHOR_LEVEL);
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && entry != null && !entry.isSaved()) {
            throw new WGIllegalStateException("Struct entry for this content has not yet been saved");
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && !language.isSaved()) {
            throw new WGIllegalStateException("Language for this content has not yet been saved");
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && language.getDatabase() != this) {
            throw new WGIllegalDataException("The language definition is from a different database");
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && entry != null) {

            // Test if draft content allready exists for this language code in
            // status DRAFT
            if (!getSessionContext().isMasterSession() && entry != null
                    && entry.hasContent(language.getName(), WGContent.STATUS_DRAFT)) {
                throw new WGIllegalStateException(
                        "There is already an existing draft copy in language " + language.getTitle() + ".",
                        WGIllegalStateException.ERRORCODE_CONTENT_DRAFT_EXISTS);
            }

            // Ask PageRightsFilter first end stop other checks if ALLOWED_SKIP_DEFAULT_CHECKS
            Right right = getPageRightsFilter().mayEditContent(entry, getSessionContext().getUserAccess(),
                    language);
            if (right == Right.ALLOWED_SKIP_DEFAULT_CHECKS)
                return; // cancel all other tests
            else if (right == Right.DENIED)
                throw new WGAuthorisationException("PageRightsFilter denies to edit content in this language",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGERIGHTSFILTER, language);

            // does contenttype allow usage:
            WGContentType contentType = entry.getContentType();
            if (contentType != null && !contentType.mayCreateContent()) {
                throw new WGAuthorisationException("User is not allowed to use this content type",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_CONTENTTYPE, contentType);
            } else if (contentType == null
                    && getSessionContext().getAccessLevel() != WGDatabase.ACCESSLEVEL_MANAGER) {
                throw new WGIllegalDataException("The page has no content type");
            }

            if (!language.mayCreateContent()) {
                throw new WGAuthorisationException("User is not allowed to create content in this language",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_LANGUAGE, language);
            }

            WGDocument restrictionDoc = entry.testEditPageHierarchyRights();
            if (restrictionDoc != null) {
                String code = (restrictionDoc instanceof WGArea
                        ? WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA
                        : WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGE);
                throw new WGAuthorisationException("User is not allowed to create content under this entry", code,
                        restrictionDoc);
            }

        }

    }

    private int findNewVersionNumber(WGStructEntry entry, WGLanguage language) throws WGAPIException {

        FreeContentVersionFinder finder = new FreeContentVersionFinder(entry, language);
        return finder.findNewVersion();

    }

    /**
     * Creates a new content document. This method flavor is meant for
     * "complete" content stores with struct entry hierarchy.
     * 
     * @param entry
     *            The struct entry for the new content
     * @param language
     *            The language of the new content
     * @param title
     *            The title of the new content
     * @return The newly created content document.
     * @throws WGAPIException
     */
    public WGContent createContent(WGStructEntry entry, WGLanguage language, String title) throws WGAPIException {
        return createContent(entry, language, title, null);
    }

    /**
     * Creates a simple content document with the content key given. This method
     * flavor is meant for content stores without struct entry hierarchy, like
     * those derived from the SimpleContentSource-Class
     * 
     * @param area
     *            The area to create the content in.
     * @param key
     *            The key of the new content.
     * @param title
     *            The title of the new content.
     * @return The newly created content.
     * @throws WGAPIException
     */
    public WGContent createContent(WGArea area, Object key, String title) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            throw new WGNotSupportedException(
                    "This creation method is not available with this database implementation, because this implementation needs more information to create a content.");
        }

        if (area == null) {
            throw new WGIllegalArgumentException("There was no area provided");
        }

        WGStructEntry entry = createStructEntry(key, area, null, title);
        entry.save();

        return this.createContent(entry, null, title);

    }

    /**
     * Equivalent to calling createStructKey(null, parent, contentType, title).
     * 
     * @param parent
     * @param contentType
     * @param title
     * @throws WGAPIException
     */
    public WGStructEntry createStructEntry(WGDocument parent, WGContentType contentType, String title)
            throws WGAPIException {
        return createStructEntry(null, parent, contentType, title);
    }

    /**
     * Creates a new struct entry
     * 
     * @param key
     *            The struct key of the new entry. Can be specified when
     *            database supports feature
     *            WGDatabase.FEATURE_ACCEPTS_STRUCTKEYS. Can be left out with
     *            null, when the database supports feature
     *            WGDatabase.FEATURE_GENERATES_STRUCTKEYS.
     * @param parent
     *            Parent object. Can be a WGArea object (new entry will be root)
     *            or another WGStructEntry object (new entry will be child)
     * @param contentType
     *            Content type for contents of this struct entry
     * @param title
     *            Title of this struct entry
     * @return WGStructEntry
     * @throws WGAPIException
     */
    public WGStructEntry createStructEntry(Object key, WGDocument parent, WGContentType contentType, String title)
            throws WGAPIException {

        if (!this.isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (parent == null || title == null) {
            throw new WGIllegalArgumentException("Mandatory parameter parent or title is empty");
        }

        performStructCreationCheck(key, parent, contentType);

        WGDocumentCore entryCore = this.getCore().createStructEntry(key, parent, contentType);
        if (entryCore == null) {
            throw new WGCreationException("Unable to create struct entry. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of struct entries");
        }
        WGStructEntry newEntry = this.getOrCreateStructEntryObject(entryCore, new WGDocumentObjectFlags());

        // Initialize
        newEntry.setMetaData(WGStructEntry.META_TITLE, title);

        // DISABLED: Area is set inside DatabaseCore.createStructEntry
        // WGArea area = (parent instanceof WGArea ? (WGArea) parent :
        // ((WGStructEntry) parent).getArea());
        // newEntry.setMetaData(WGStructEntry.META_AREA,
        // (hasFeature(FEATURE_USE_OBJECTS_AS_REFERENCES) ? area.getCore() :
        // (Object) area.getName()));

        newEntry.setMetaData(WGStructEntry.META_POSITION, new Integer(0));
        if (contentType != null) {
            newEntry.setMetaData(WGStructEntry.META_CONTENTTYPE,
                    (hasFeature(FEATURE_USE_OBJECTS_AS_REFERENCES) ? contentType.getCore()
                            : (Object) contentType.getName()));
        }

        // Save and return
        // newEntry.save();
        return newEntry;

    }

    /**
     * Controls if the current user may create a struct entry with the given data. If so the method exists normally. If not an exception is thrown.
     * @param key A struct key that is to be determined for the new struct entry. Give null if you want the WGAPI to generate the key.
     * @param parent The parent of the new struct entry. May be an {@link WGArea} or another {@link WGStructEntry}.
     * @param contentType The content type of the new struct entry.
     * @throws WGAPIException If the user may not create the document. The exception informs about the reason.
     */
    public void performStructCreationCheck(Object key, WGDocument parent, WGContentType contentType)
            throws WGAPIException {

        if (parent == null) {
            throw new WGIllegalDataException("No parent document was given");
        }

        if (parent.getDatabase() != this) {
            throw new WGIllegalDataException("The parent document is from a different database");
        }

        // check parent lock
        if (parent.getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException(
                    "Unable to create structentry. Parent (structentry or area) is locked.");
        }

        if (!parent.isSaved()) {
            throw new WGIllegalStateException(
                    "Parent object of this struct entry (either struct entry or area) has not yet been saved");
        }

        if (hasFeature(WGDatabase.FEATURE_FULLCONTENTFEATURES) && contentType == null) {
            throw new WGIllegalArgumentException(
                    "You did not provide a content type which is mandatory for this database type");
        }

        if (!(parent instanceof WGArea || parent instanceof WGStructEntry)) {
            throw new WGIllegalArgumentException("parent parameter of invalid document type: "
                    + (parent != null ? parent.getClass().getName() : "null"));
        }

        WGArea targetArea = (parent instanceof WGArea ? (WGArea) parent : ((WGStructEntry) parent).getArea());
        if (contentType != null) {

            if (!contentType.isSaved()) {
                throw new WGIllegalStateException("Content type for this struct entry has not yet been saved");
            }
            if (contentType.getDatabase() != this) {
                throw new WGIllegalDataException("The content type is from a different database");
            }

            if (!targetArea.isSystemArea() && !contentType.mayCreateChildEntry(parent)) {
                throw new WGAuthorisationException("User is not allowed to use this content type at this position",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_CONTENTTYPE, contentType);
            }

        }

        if (getSessionContext().getAccessLevel() < ACCESSLEVEL_AUTHOR) {
            throw new WGAuthorisationException("You are not authorized to create struct entries in this database",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_AUTHOR_LEVEL);
        }

        boolean isRoot = (parent instanceof WGArea);
        if (isRoot) {
            WGArea parentArea = (WGArea) parent;
            if (!parentArea.mayEditPages()) {
                throw new WGAuthorisationException("User is not allowed to edit entries in this area",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA, parent);
            }
            if (contentType != null) {
                parentArea.testRootPageRestrictions(contentType);
            }
        } else {
            WGStructEntry parentStruct = ((WGStructEntry) parent);
            WGDocument restrictingDoc = parentStruct.testEditChildPagesHierarchyRights();
            if (restrictingDoc != null) {
                String errorCode = (restrictingDoc instanceof WGArea
                        ? WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA
                        : WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGE);
                throw new WGAuthorisationException(
                        "User is not allowed to edit child entries under this parent entry", errorCode,
                        (WGStructEntry) restrictingDoc);
            }
            if (contentType != null) {
                parentStruct.testChildPageRestrictions(contentType);
            }
        }

        if (!hasFeature(FEATURE_ACCEPTS_STRUCTKEYS)) {
            if (key != null) {
                throw new WGCreationException(
                        "You provided a struct key, but this implementation does not accept struct keys from the creator");
            }
        }

        if (!hasFeature(FEATURE_GENERATES_STRUCTKEYS)) {
            if (key == null) {
                throw new WGCreationException(
                        "You did not provide a struct key, but this implementation cannot generate struct keys itself");
            }

            WGStructEntry otherEntry = getStructEntryByKey(key);
            if (otherEntry != null) {
                throw new WGDuplicateKeyException(
                        "There is already a struct entry under the given key '" + String.valueOf(key) + "'");
            }
        }
    }

    /**
     * Returns a WGContent object for the given content core by either
     * retrieving an existing or creating one
     * 
     * @param doc
     *            The content core
     * @return WGContent
     * @throws WGAPIException
     */
    protected WGContent getOrCreateContentObject(WGDocumentCore doc) throws WGAPIException {

        if (doc == null) {
            return null;
        }

        WGDocumentKey key = WGDocument.buildDocumentKey(doc, this);
        WGContent content;
        try {
            content = (WGContent) getDocumentByDocumentKeyFromCache(key, false);
        } catch (WGDocumentDoesNotExistException e) {
            content = null;
        }

        if (content == null) {
            content = this.createContentObject(doc);
        } else if (!content.isTemporary()) {
            if (!content.isEdited()) {
                content.setCore(doc);
            }
        }
        return content;

    }

    protected WGDesignDocument getOrCreateDesignDocumentObject(WGDocumentCore doc, WGDocumentObjectFlags flags)
            throws WGAPIException {

        WGDocumentKey key = WGDocument.buildDocumentKey(doc, this);
        WGDesignDocument design;
        try {
            design = (WGDesignDocument) getDocumentByDocumentKeyFromCache(key, false);
        } catch (WGDocumentDoesNotExistException e) {
            design = null;
        }

        if (design == null) {
            design = createDesignDocumentObject(doc, flags);
        } else {
            if (!design.isEdited()) {
                design.setCore(doc);
            }
        }
        return design;

    }

    /**
     * Wraps a user profile core into a WGUserProfile object.
     * 
     * @param doc
     *            core to wrap
     * @return WGUserProfile
     * @throws WGAPIException
     */
    private WGUserProfile createUserProfileObject(WGDocumentCore doc, WGDocumentObjectFlags flags)
            throws WGAPIException {

        WGUserProfile profile = new WGUserProfile(this, doc, flags);
        return profile;
    }

    /**
     * Returns date, where data in the database was last changed. This function
     * may not be supported by some implementations (Feature
     * WGDatabase.FEATURE_LASTCHANGED) and return new Date(Long.MIN_VALUE).
     * 
     * @return Date
     * @throws WGAPIException 
     */
    public Date getLastChanged() {
        return _revisionDate;
    }

    /**
     * Returns the revision state of the database. The return type depends on the backend database, but is normally either {@link Long} or {@link Date}.
     * The revision here is the state that the database and all its caches currently safely represents. The actual revision on the backend may be newer.
     * and can be retrieved using {@link #getBackendRevision()}.
     * You can use {@link #getRevisionObject()} instead which provides a high-level object easier to compare to other revisions and to serialize/deserialize.
     * To get dates corresponding to the given revision use {@link #getRevisionDate(Comparable)}.
     * To directly retrieve concrete dates use {@link #getLastChanged()} instead.
     */
    public Comparable getRevision() {
        return _revision.getRevisionValue();
    }

    /**
     * Returns the revision state of the database backend. This is the most up-to-date revision including all most recent changes, but may not yet be reflected by all
     * caches of the WGAPI.
     * @throws WGAPIException
     */
    public Comparable getBackendRevision() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return getCore().getRevision();

    }

    /**
     * Returns the revision state of the database as an high level object, usable for revision comparison and storage independently from the concrete backend type of revision value
     * Just like {@link #getRevision()} this returns the revision state of the database that is safely represented by all caches.
     */
    public WGDatabaseRevision getRevisionObject() {
        return _revision;
    }

    /**
     * Retrieve the datetime where a given database revision was done.
     * The indicators given as parameter must have been retrieved by {@link #getRevision()}.
     * If the revision is of wrong datatype or is no revision known in this database the method throws a WGWrongRevisionExcception.
     * @param lastModified The revision indicator
     * @return The date that the given revision was done
     * @throws WGAPIException
     * @throws WGWrongRevisionException if the given revision is no revision of the current database
     */
    public Date getRevisionDate(Comparable lastModified) throws WGAPIException, WGWrongRevisionException {
        if (lastModified instanceof WGDatabaseRevision) {
            return getCore().getRevisionDate(((WGDatabaseRevision) lastModified).getRevisionValue());
        } else {
            return getCore().getRevisionDate(lastModified);
        }
    }

    /**
     * Returns the map of creation options provided to this database when it was
     * initially opened
     * 
     * @return Map
     */
    public Map getCreationOptions() {

        return this.creationOptions;

    }

    /**
     * Returns a map of WGLanguage objects, mapped by their language name (code)
     * 
     * @return Map
     * @throws WGAPIException
     */
    public Map<String, WGLanguage> getLanguages() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        List languageList = this.getDesignObjects(WGDocument.TYPE_LANGUAGE);
        if (languageList == null) {
            return null;
        }

        Iterator languageIterator = languageList.iterator();

        HashMap languages = new HashMap<String, WGLanguage>();
        WGLanguage language = null;

        while (languageIterator.hasNext()) {
            language = (WGLanguage) languageIterator.next();
            languages.put(language.getName(), language);
        }

        return languages;

    }

    /**
     * Returns the session context, providing all data about the currently
     * opened database session. Null if no session is open.
     * 
     * @return WGSessionContext
     */
    public WGSessionContext getSessionContext() {

        return (WGSessionContext) this.sessionContext.get();
    }

    /**
     * Clears all document caches.
     */
    private synchronized void clearDocumentMappings() {

        try {
            this.masterDocumentsByKey.flushAll();
            this.masterDocumentByName.flushAll();
            this.masterQueryCache.flushAll();
            this.masterDocumentCaches.clear();
        } catch (CacheException e) {
            WGFactory.getLogger().error("Exception clearing master document cache", e);
        }
        this.designDocumentLists.clear();

    }

    /**
     * Completely closes this database object. This object and all child objects
     * must not be used after calling this.
     * 
     * @throws WGAPIException
     */
    public void close() throws WGAPIException {

        WGFactory.getInstance().removeDatabase(this);
        ready = false;

        if (isSessionOpen()) {
            this.closeSession();
        }

        clearDocumentMappings();
        try {
            this.masterDocumentsByKey.destroy();
            this.masterDocumentByName.destroy();
            this.masterQueryCache.destroy();
        } catch (CacheException e) {
            WGFactory.getLogger().error("Exception closing master document cache");
        }

        if (this.designProvider != null) {
            this.designProvider.removeDesignChangeListener(this);
            this.designProvider.dispose();
            this.designProvider = null;
        }

        if (isConnected()) {
            core.close();
            core = null;
        }

        closeAuthenticationModule();

        // Cannot release db listeners here, bc. the closing might be issued by
        // a fired event (e.g. connection error), and this would result in a
        // concurrent
        // modification.
        // databaseConnectListeners.clear();

    }

    private void closeAuthenticationModule() {
        if (_authenticationModule != null) {
            _authenticationModule.removeAuthenticationSourceListener(this);
            _authenticationModule.destroy();
        }
    }

    /**
     * Retrieves a dummy content object. Should support the native expression
     * language of the database, if there is one.
     * 
     * @return WGContent
     * @throws WGAPIException
     */
    public WGContent getDummyContent(String language) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (language == null) {
            language = getDefaultLanguage();
        }

        if (hasFeature(FEATURE_MULTILANGUAGE)) {
            WGLanguage lang = getLanguage(language);
            if (lang == null) {
                throw new WGIllegalArgumentException("Language '" + language + "' is not defined");
            }
        }

        return new WGContent(this, this.getCore().getDummyContent(language),
                new WGDocumentObjectFlags().setDummy(true));
    }

    /**
     * Returns a dummy profile object that does not store information.
     * 
     * @param name
     *            Profile name that the dummy profile should return
     * @throws WGAPIException
     */
    public WGUserProfile getDummyProfile(String name) throws WGAPIException {
        return createUserProfile(name, Constants.PERSMODE_SESSION, true,
                new WGDocumentObjectFlags().setDummy(true));
    }

    /**
     * Parses the string representation of a struct key (e.g. used in a URL) to
     * the real internal struct key format
     * 
     * @param key
     * @return The struct key object. Type varies by database implementation.
     * @throws WGAPIException
     */
    public Object parseStructKey(String key) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return this.getCore().parseStructKey(key);
    }

    /**
     * Return a content document by content key
     * 
     * @param key
     *            Key to retrieve content for
     * @return WGContent the found content, if none found null
     * @throws WGAPIException
     */
    public WGContent getContentByKey(WGContentKey key) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (key == null) {
            return null;
        }

        WGContent content = null;

        // A released version is needed. Find via struct entry
        if (key.getVersion() == 0) {
            WGStructEntry entry = getStructEntryByKey(key.getStructKey());
            if (entry == null) {
                return null;
            }

            return entry.getReleasedContent(key.getLanguage());
        }

        // A certain version is needed. Find via document key
        else {
            return (WGContent) getDocumentByKey(key.toDocumentKey());
        }
    }

    private WGContent retrieveContentByKey(WGContentKey key) throws WGAPIException {

        WGContent content = null;

        WGDocumentCore contentCore = this.getCore().getContentByKey(key);
        if (contentCore != null && !contentCore.isDeleted()) {
            content = this.getOrCreateContentObject(contentCore);
        } else {
            mapNonExistentDocIndicator(this.masterDocumentsByKey, key.toDocumentKey().toString(),
                    WGDocument.TYPE_CONTENT);
        }

        return content;

    }

    protected void mapNonExistentDocIndicator(Map cacheMap, Object cacheKey, int docType) {

        if (getSessionContext().isCacheWritingEnabled() && isDoctypeCacheable(docType)) {
            cacheMap.put(cacheKey, new NullPlaceHolder());
        }

    }

    protected void mapNonExistentDocIndicator(Cache cache, String cacheKey, int docType) {

        if (getSessionContext().isCacheWritingEnabled() && isDoctypeCacheable(docType)) {
            try {
                cache.writeEntry(cacheKey, new NullPlaceHolder());
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception mapping NDI to master document cache", e);
            }
        }

    }

    /**
     * Return a content document by content key string.
     * 
     * @param keyStr
     *            The content key in its string representation
     * @return The content with that content key or null if it does not exist
     * @throws WGAPIException
     */
    public WGContent getContentByKey(String keyStr) throws WGAPIException {
        WGContentKey contentKey = WGContentKey.parse(keyStr, this);
        if (contentKey != null) {
            return getContentByKey(contentKey);
        } else {
            throw new WGIllegalArgumentException("Invalid content key string: " + keyStr);
        }
    }

    /**
     * Fetches a document from any WGDatabase document cache. If the cache entry
     * contains an object that is no document this is taken as indicator that
     * the document does not exist (It already was tried to retrieve it but that
     * failed). To let the calling code differ between empty cache and
     * nonexistent document the method throws a WGDocumentDoesNotExistException
     * in that case.
     * This variant will not return results while session caching is disabled.
     * 
     * @param cache
     *            The cache map to use
     * @param key
     *            The cache key
     * @return The cached document. null if the cache entry is empty (not yet
     *         cached)
     * @throws WGAPIException 
     * @throws WGDocumentDoesNotExistException if the document is marked in cache as nonexistent
     * @throws WGAPIException
     */
    protected WGDocument fetchDocumentFromCache(Object cache, Object key) throws WGAPIException {
        return fetchDocumentFromCache(cache, key, false);
    }

    /**
     * Fetches a document from any WGDatabase document cache. If the cache entry
     * contains an object that is no document this is taken as indicator that
     * the document does not exist (It already was tried to retrieve it but that
     * failed). To let the calling code differ between empty cache and
     * nonexistent document the method throws a WGDocumentDoesNotExistException
     * in that case.
     * 
     * @param cache
     *            The cache map to use
     * @param key
     *            The cache key
     * @param internal call Specify true for internal API calls where the session caching setting should have no effect.
     * @return The cached document. null if the cache entry is empty (not yet
     *         cached)
     * @throws WGAPIException 
     * @throws WGDocumentDoesNotExistException if the document is marked in cache as nonexistent
     * @throws WGAPIException
     */
    protected WGDocument fetchDocumentFromCache(Object cache, Object key, boolean internalCall)
            throws WGAPIException {

        if (!internalCall && !getSessionContext().isCachingEnabled()) {
            return null;
        }

        Object cacheContent = null;
        if (cache instanceof Map) {
            cacheContent = ((Map) cache).get(key);
        } else if (cache instanceof Cache) {
            try {
                cacheContent = ((Cache) cache).read(String.valueOf(key));
            } catch (CacheException e) {
                WGFactory.getLogger().error("Exception reading master document cache", e);
            }
        } else {
            throw new WGIllegalArgumentException("Illegal cache object type " + cache.getClass().getName());
        }

        // Direct cache hit (normally only masterDocumentsByKey): Return doc if not read protected now
        if (cacheContent instanceof WGDocument) {
            WGDocument doc = (WGDocument) cacheContent;

            try {
                if (!doc.isReadableForUser()) {
                    throw new WGDocumentDoesNotExistException();
                }
            } catch (WGDeletedException e) {
                if (internalCall) {
                    return doc;
                } else {
                    return null;
                }
            }

            return doc;
        }

        // Indirect cache hit: Lookup document by its document key in master
        // cache
        else if (cacheContent instanceof WGDocumentKey) {
            return fetchDocumentFromCache(masterDocumentsByKey, (WGDocumentKey) cacheContent);
        }

        // Cache indicating document does not exist: Throw exception
        else if (cacheContent instanceof NullPlaceHolder) {
            throw new WGDocumentDoesNotExistException();
        }

        // Cache indication document is unknown. We must retrieve it from
        // backend.
        else {
            return null;
        }

    }

    /**
     * Returns a document by the given document key.
     * 
     * @param key
     *            The document key for the document in its string representation
     * @return The document of that key, null if there is none
     * @throws WGAPIException
     * @deprecated Use {@link #getDocumentByKey(String)} instead
     */
    public WGDocument getDocumentByDocumentKey(String key) throws WGAPIException {
        WGDocumentKey documentKey = new WGDocumentKey(key);
        return getDocumentByKey(documentKey);
    }

    /**
     * Returns a document by the given document key.
     * 
     * @param documentKey
     *            The document key for the document.
     * @return The document of that key, null if there is none
     * @throws WGAPIException
     */
    public WGDocument getDocumentByKey(WGDocumentKey documentKey) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!documentKey.isRealDocumentKey()) {
            throw new IllegalArgumentException(
                    "The document key '" + documentKey + "' does not belong to a real WGAPI document");
        }

        if (hasFeature(FEATURE_DISABLE_META_LOWERCASING)) {
            documentKey.useProperCaseNames();
        }

        try {
            // Check cache once unsynchronized, then synchronized
            WGDocument doc = getDocumentByDocumentKeyFromCache(documentKey, true);
            if (doc == null) {

                // Double-checked locking (some doctypes may choose not to)
                if (isSynchronizeAccessForDoctype(documentKey.getDocType())) {
                    WGOperationKey op = obtainOperationKey(WGOperationKey.OP_DOCUMENT_BY_KEY, documentKey);
                    synchronized (op) {
                        try {
                            op.setUsed(true);
                            doc = getDocumentByDocumentKeyFromCache(documentKey, true);

                            if (doc == null) {
                                verboseBackendAccess(WGOperationKey.OP_DOCUMENT_BY_KEY, documentKey);
                                doc = retrieveDocumentByKey(documentKey);
                            }
                        } finally {
                            op.setUsed(false);
                        }
                    }

                } else {
                    verboseBackendAccess(WGOperationKey.OP_DOCUMENT_BY_KEY, documentKey);
                    doc = retrieveDocumentByKey(documentKey);
                }
            }

            return doc;
        } catch (WGDocumentDoesNotExistException e) {
            return null;
        }

    }

    private boolean isSynchronizeAccessForDoctype(int docType) {

        WGDesignProvider designProvider = getDesignProvider();
        if (designProvider == null) {
            return true;
        }

        if (designProvider.providesType(docType)) {
            return designProvider.isSynchronizeAccess();
        }

        return true;

    }

    private WGDocument retrieveDocumentByKey(WGDocumentKey documentKey) throws WGAPIException {
        WGDocument doc;
        int docType = documentKey.getDocType();
        switch (docType) {
        case WGDocument.TYPE_CONTENT:
            WGContentKey contentKey = WGContentKey.parse(documentKey.getName(), this);
            doc = retrieveContentByKey(contentKey);
            break;

        case WGDocument.TYPE_USERPROFILE:
            doc = retrieveUserProfile(documentKey.getName());
            break;

        case WGDocument.TYPE_STRUCTENTRY:
            Object structKey = parseStructKey(documentKey.getName());
            doc = retrieveStructEntry(structKey);
            break;

        default:
            doc = retrieveDesignDocument(docType, documentKey.getName(), documentKey.getMediakey());
            break;
        }

        if (doc != null && doc.isReadableForUser()) {
            return doc;
        } else {
            throw new WGDocumentDoesNotExistException();
        }

    }

    /**
     * Returns a document by the given document key.
     * 
     * @param key
     *            The document key for the document in its string representation
     * @return The document of that key, null if there is none
     * @throws WGAPIException
     */
    public WGDocument getDocumentByKey(String key) throws WGAPIException {
        WGDocumentKey documentKey = new WGDocumentKey(key);
        return getDocumentByKey(documentKey);
    }

    /**
     * Returns the parent entry to the given struct entry
     * 
     * @param entry
     *            The entry, whose parent is searched
     * @return WGStructEntry
     * @throws WGAPIException
     */
    public WGStructEntry getParentEntry(WGStructEntry entry) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (entry == null || entry.isDeleted() || entry.isDummy()) {
            return null;
        }

        WGDocumentCore parentCore = this.getCore().getParentEntry(entry);
        if (parentCore != null && !parentCore.isDeleted()) {
            return getOrCreateStructEntryObject(parentCore, new WGDocumentObjectFlags());
        } else {
            return null;
        }

    }

    /**
     * Retrieves the value of an extension data field stored in this database.
     * Extension data fields are custom data that are persistently stored to an entity, in this case the database itself, and which may have custom purpose.
     * This feature is only available in WGA Content Stores of Version 5 or higher.
     * @param name The name of the field.
     * @return The value of the requested field for this database core. 
     * @throws WGAPIException 
     */

    public Object getExtensionData(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGContentStoreVersionException("Database metadata", WGDatabase.CSVERSION_WGA5);
        }

        return getCore().getExtensionData(name);
    }

    /**
     * Returns the names of stored extension data fields on this database
     * This feature is only available in WGA Content Stores of Version 5 or higher.
     * @return List of field mnames
     * @throws WGAPIException
     */
    public List<String> getExtensionDataNames() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGContentStoreVersionException("Database metadata", WGDatabase.CSVERSION_WGA5);
        }

        return getCore().getExtensionDataNames();
    }

    /**
     * Writes a extension data field. The data is immediately stored.
     * Extension data fields are custom data that are persistently stored to an entity, in this case the database itself, and which may have custom purpose.
     * This feature is only available in WGA Content Stores of Version 5 or higher.
     * @param name Name of the field
     * @param value The value to store
     * @throws WGAPIException
     */
    public void writeExtensionData(String name, Object value) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGContentStoreVersionException("Database metadata", WGDatabase.CSVERSION_WGA5);
        }

        if (getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_CHIEF_EDITOR) {
            throw new WGAuthorisationException("Only managers and chief editors can modify database metadata",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_MANAGER_LEVEL);
        }

        getCore().writeExtensionData(name, value);
    }

    /**
     * Removes a extension data field from the database. The removal is immediately commited.
     * This feature is only available in WGA Content Stores of Version 5 or higher.
     * @param name Name of the field to remove
     * @throws WGAPIException
     */
    public void removeExtensionData(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGContentStoreVersionException("Database metadata", WGDatabase.CSVERSION_WGA5);
        }

        if (getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_CHIEF_EDITOR) {
            throw new WGAuthorisationException("Only managers and chief editors can modify database metadata",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_MANAGER_LEVEL);
        }

        getCore().removeExtensionData(name);

    }

    /**
     * Returns the default language of this database
     * 
     * @return String
     * @throws WGAPIException
     */
    public String getDefaultLanguage() throws WGAPIException {

        if (_defaultLanguage == null) {
            _defaultLanguage = Locale.getDefault().getLanguage();
        }

        return _defaultLanguage;
    }

    /**
     * Trigger the determination of a default language.
     * This method tries to find a reasonable safe language to use as default language with limited effort.
     * It does not take into account which language is used the most but will take a language with root contents available.
     * @throws WGAPIException
     */
    public synchronized void determineDefaultLanguage() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        // Look for root content languages 
        WGContent content = null;
        Map langs = getLanguages();
        Iterator langsIt = getLanguages().values().iterator();
        while (langsIt.hasNext()) {
            WGLanguage lang = (WGLanguage) langsIt.next();
            content = getFirstReleasedContent(lang.getName(), true);
            if (content != null) {
                break;
            }
        }

        // If we found a root content in some language we take it as default language
        if (content != null) {
            _defaultLanguage = content.getLanguage().getName();
            return;
        }

        // If langs defined: Use the one matching the platform default
        // or the "first best" when no definition matches
        if (langs.size() > 0) {
            WGLanguage lang = getLanguageForLocale(Locale.getDefault());
            if (lang == null) {
                lang = (WGLanguage) langs.values().iterator().next();
            }
            _defaultLanguage = lang.getName();
            return;
        }

        // No langs defined: Use platform default language
        else {
            _defaultLanguage = Locale.getDefault().getLanguage();
        }

        _defaultLanguageAutoDetermined = true;
    }

    /**
     * Returns the name of the database server (if there is one).
     * 
     * @return String
     * @throws WGAPIException
     */
    public String getServerName() throws WGAPIException {
        return getCore().getServerName();
    }

    /**
     * Returns, if the database is ready for work.
     * 
     * @return Returns a boolean
     */
    public boolean isReady() {
        return ready;
    }

    /**
     * Returns the core implementation object of this database.
     * 
     * @return WGDatabaseCore
     */
    public WGDatabaseCore getCore() {
        return core;
    }

    /**
     * Returns the personalisation core implementation of this database, if it is a personalisation database.
     * @throws WGNotSupportedException 
     * @throws WGNotSupportedException If the database is no personalisation database
     */
    public WGPersonalisationDatabaseCore getPersonalisationCore() throws WGNotSupportedException {

        WGDatabaseCore theCore = getCore();
        if (theCore instanceof WGPersonalisationDatabaseCore) {
            return (WGPersonalisationDatabaseCore) theCore;
        } else {
            throw new WGNotSupportedException("This database is no personalisation database");
        }

    }

    /**
     * Indicates if a database session is open
     */
    public boolean isSessionOpen() {
        return (this.getSessionContext() != null);
    }

    /**
     * Returns the database path, given as parameter of the method
     * WGDatabase.open()
     * 
     * @return String
     */
    public String getPath() {
        return path;
    }

    /**
     * Returns the first released content document of this database in the
     * specified language. It is up to the db implementation to decide, which
     * document is regarded "the first" of the database.
     * 
     * @param language
     *            The language of the content
     * @param onlyRoots
     *            Specify true, if you want to get only root documents
     * @return WGContent The content, null if there is no content of that
     *         language.
     * @throws WGAPIException
     */
    public WGContent getFirstReleasedContent(final String language, boolean onlyRoots) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        WGLanguageChooser chooser = new WGLanguageChooser() {

            public WGContent selectContentForName(WGDatabase db, String name, boolean isBI) throws WGAPIException {
                return db.getContentByName(name, language);
            }

            public WGContent selectContentForPage(WGStructEntry page, boolean isBI) throws WGAPIException {
                return page.getReleasedContent(language);
            }

            public WGLanguage selectDatabaseLanguage(WGDatabase db) throws WGAPIException {
                return db.getLanguage(language);
            }

            public WGContentKey chooseQueryContent(WGDatabase db, Map<String, WGContentKey> contents)
                    throws WGAPIException {
                return contents.get(language);
            }

            public List<WGLanguage> getQueryLanguages(WGDatabase db) throws WGAPIException {
                return Collections.singletonList(db.getLanguage(language));
            }

            @Override
            public List<WGContent> selectContentPriorityOrder(WGStructEntry page, boolean isBI)
                    throws WGAPIException {
                return null; // Not used
            }

        };

        Iterator areas = this.getAreas().iterator();
        while (areas.hasNext()) {
            WGArea area = (WGArea) areas.next();
            if (area.isSystemArea()) {
                continue;
            }

            Iterator rootEntries = area.getRootEntryIterator(10);
            while (rootEntries.hasNext()) {
                WGContent content = this.getFirstReleasedContent((WGStructEntry) rootEntries.next(), chooser,
                        onlyRoots);
                if (content != null) {
                    return content;
                }
            }
        }

        return null;

    }

    /**
     * Returns the first released content document of this database. 
     * It is up to the db implementation to decide, which document is regarded "the first" of the database.
     * 
     * @param chooser
     *            A language chooser object determining what language versions may be used
     * @param onlyRoots
     *            Specify true, if you want to get only root documents
     * @return WGContent The content, null if there is no content of that
     *         language.
     * @throws WGAPIException
     */
    public WGContent getFirstReleasedContent(WGLanguageChooser chooser, boolean onlyRoots) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        Iterator areas = this.getAreas().iterator();
        while (areas.hasNext()) {
            WGArea area = (WGArea) areas.next();
            if (area.isSystemArea()) {
                continue;
            }

            Iterator rootEntries = area.getRootEntryIterator(10);
            while (rootEntries.hasNext()) {
                WGContent content = getFirstReleasedContent((WGStructEntry) rootEntries.next(), chooser, onlyRoots);
                if (content != null) {
                    return content;
                }
            }
        }

        return null;

    }

    /**
     * Returns the first released content of the given language to be found in
     * this database.
     * 
     * @param language
     * @throws WGAPIException
     */
    public WGContent getFirstReleasedContent(String language) throws WGAPIException {
        return getFirstReleasedContent(language, false);
    }

    /**
     * Returns the first released content in a given language, that is connected
     * to the given struct entry or to his descendant entries.
     * 
     * @param entry
     *            The reference entry. Content will be attached to this one or
     *            to a descendant entry of it.
     * @param language
     *            Language of the needed content
     * @param onlyRoots
     *            Specify true, if you only want root documents returned
     * @return WGContent The content, null if there was no content of this
     *         language found.
     * @throws WGAPIException
     */
    private WGContent getFirstReleasedContent(WGStructEntry entry, WGLanguageChooser chooser, boolean onlyRoots)
            throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (entry == null || entry.isDeleted() || entry.isDummy()) {
            return null;
        }

        WGContent content = chooser.selectContentForPage(entry, false);
        if (content != null) {
            return content;
        } else if (!onlyRoots) {
            Iterator childEntries = entry.getChildEntryIterator(10);
            while (childEntries.hasNext()) {
                content = this.getFirstReleasedContent((WGStructEntry) childEntries.next(), chooser, onlyRoots);
                if (content != null) {
                    return content;
                }
            }
            return null;
        } else {
            return null;
        }
    }

    /**
     * Retrieves a user profile for the given profile name.
     * 
     * @param name
     * @throws WGAPIException
     */
    public WGUserProfile getUserProfile(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (name == null) {
            return null;
        }

        return (WGUserProfile) getDocumentByKey(new WGDocumentKey(WGDocument.TYPE_USERPROFILE, name, null));

    }

    private WGUserProfile retrieveUserProfile(String name) throws WGAPIException {

        WGUserProfile profile = null;
        WGDocumentCore profileCore = getPersonalisationCore().getUserProfile(name);
        if (profileCore != null && !profileCore.isDeleted()) {
            profile = this.createUserProfileObject(profileCore, new WGDocumentObjectFlags());
        }

        return profile;
    }

    /**
     * Creates a new user profile.
     * 
     * @param userNameWish
     *            Name of the new user profile. May be null to let the
     *            personalisation database generate a name.
     * @param type
     *            Type of the user profile. Can be any integer chosen by the
     *            personalisation implementation to differ profile types.
     * @param forceNew
     *            Forces the creation of a new profile. If there is already an
     *            existing profile with the given name then a new profile with
     *            a different name is created and returned.
     * @return The new user profile.
     * @throws WGAPIException
     *             instance of WGCreationException if a profile with this name
     *             already exists or the profile creation fails otherwise
     *             WGAPIExceptions on BackendErrors
     */
    public WGUserProfile createUserProfile(String userNameWish, int type, boolean forceNew) throws WGAPIException {
        return createUserProfile(userNameWish, type, forceNew, new WGDocumentObjectFlags());
    }

    protected WGUserProfile createUserProfile(String userNameWish, int type, boolean forceNew,
            WGDocumentObjectFlags flags) throws WGAPIException {
        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        // check db lock
        if (getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException("Unable to create userprofile. Database is locked.");
        }

        if (getSessionContext().getAccessLevel() < ACCESSLEVEL_AUTHOR) {
            if (!(readerProfileCreation && getSessionContext().getAccessLevel() >= ACCESSLEVEL_READER)) {
                throw new WGAuthorisationException("User is not allowed to create user profiles in this database",
                        WGAuthorisationException.ERRORCODE_OP_NEEDS_AUTHOR_LEVEL);
            }
        }

        if (userNameWish != null) {

            WGUserProfile profile = retrieveUserProfile(userNameWish);

            // The profile may have been created simultaneously in another thread. We do not fail here, just return the profile.
            if (profile != null) {
                if (forceNew) {
                    userNameWish = null;
                } else {
                    return profile;
                }
                //throw new WGDuplicateKeyException("There is already a user profile with id '" + userNameWish + "'");
            }

        }

        WGDocumentCore profileCore = getPersonalisationCore().createUserProfile(userNameWish, type);
        if (profileCore == null) {
            throw new WGCreationException("Unable to create user profile. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creating user profiles.");
        }

        WGUserProfile profile = this.createUserProfileObject(profileCore, flags);
        return profile;

    }

    /**
     * Creates a new user profile.
     * 
     * @param userNameWish
     *            Name of the new user profile. May be null to let the
     *            personalisation database generate a name.
     * @param type
     *            Type of the user profile. Can be any integer chosen by the
     *            personalisation implementation to differ profile types.
     * @return The new user profile.
     * @throws WGAPIException
     *             instance of WGCreationException if a profile with this name
     *             already exists or the profile creation fails otherwise
     *             WGAPIExceptions on BackendErrors
     */
    public WGUserProfile createUserProfile(String userNameWish, int type) throws WGAPIException {
        return createUserProfile(userNameWish, type, false, new WGDocumentObjectFlags());
    }

    /**
     * Returns, if the database contains content
     * 
     * @return boolean
     */
    public boolean isContentRole() {
        return contentRole;
    }

    /**
     * Returns if the database contains design documents
     * 
     * @return boolean
     */
    public boolean isDesignRole() {
        return designRole;
    }

    /**
     * Returns if if the database contains file containers.
     * 
     * @return boolean
     */
    public boolean isRepositoryRole() {
        return repositoryRole;
    }

    /**
     * Indicates if the database contains user profiles
     */

    public boolean isUserProfilesRole() {
        return userProfilesRole;
    }

    /**
     * Determines if this database is completely empty of (content store)
     * documents. This does not work on personalisation databases.
     * 
     * @throws WGAPIException
     */
    public boolean isEmpty() throws WGAPIException {

        boolean contentEmpty = true;
        boolean designEmpty = true;

        if (isContentRole()) {
            contentEmpty = isContentEmpty();
        }

        if (isDesignRole() && getDesignProvider() == null) {
            designEmpty = (isDesignEmpty());
        }

        return (contentEmpty && designEmpty);

    }

    /**
     * Returns if this database has no design documents (file containers, script
     * modules, WebTML modules) and can be regarded "design-empty".
     * 
     * @throws WGAPIException
     */
    public boolean isDesignEmpty() throws WGAPIException {
        return getFileContainers().size() == 0 && getCSSJSModules().size() == 0 && getTMLModules().size() == 0;
    }

    /**
     * Returns if this database has neither content hierarchy (areas, struct
     * entries, contents) nor content schema documents (content types,
     * languages) and can be regarded "content-empty"
     * 
     * @throws WGAPIException
     */
    public boolean isContentEmpty() throws WGAPIException {
        return (getAreas().size() == 0 && getContentTypes().size() == 0 && getLanguages().size() == 0);
    }

    /**
     * Gets the name of the master login, which is the login user name used with
     * the initial call to WGDatabase.open().
     * 
     * @return String
     */
    public String getMasterLoginName() {
        return masterLoginName;
    }

    /**
     * Gets the workflow engine object for this database.
     * 
     * @return WGWorkflowEngine
     * @throws WGWorkflowException
     * @throws WGBackendException
     */
    public WGWorkflowEngine getWorkflowEngine() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return workflowEngine;

    }

    /**
     * Returns an object containing internal statistics about the database
     * 
     * @return WGDatabaseStatistics
     */
    public WGDatabaseStatistics getStatistics() {

        return new WGDatabaseStatistics();
    }

    /**
     * Tests, if the current user can be associated with any entry in the list.
     * This is up to the database implementation to decide since name/group/role
     * mechanims may vary widely in different databases.
     * 
     * @param list
     * @throws WGAPIException
     */
    public boolean isMemberOfUserList(List list) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        // Lookup the member decisions cache
        Boolean decision;
        Map<Integer, Boolean> memberDecisions = null;
        int listHash = 0;
        if (this.userCachesEnabled) {
            memberDecisions = (Map<Integer, Boolean>) getUserCache().get(USERCACHE_MEMBERDECISIONS);
            if (memberDecisions == null) {
                memberDecisions = new ConcurrentHashMap<Integer, Boolean>();
                getUserCache().put(USERCACHE_MEMBERDECISIONS, memberDecisions);
            }
            listHash = list.hashCode();
            decision = memberDecisions.get(listHash);
            if (decision != null) {
                return decision;
            }
        }

        // Determine membership 
        decision = this.getCore().isMemberOfUserList(list);

        // Write to cache
        if (this.userCachesEnabled) {
            memberDecisions.put(listHash, decision);
        }
        return decision;
    }

    /**
     * Creates a simple content document. This method can only be used, when the
     * underlying database implementation doesn't support full content features
     * like struct entries, content types etc.
     * 
     * @param title
     *            Title of the new content document.
     * @return WGContent
     * @throws WGAPIException
     */
    public WGContent createContent(String title) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            return this.createContent((WGStructEntry) null, null, title);
        } else {
            throw new WGNotSupportedException(
                    "This creation method is not available with this database implementation, because this implementation needs more information to create a content.");
        }
    }

    /**
     * Creates a simple content document. This variant can be used in
     * implementations that don't have struct entries or content types and
     * generate their own keys. (e.g. some descendants of SimpleContentSource
     * like JDBCSource).
     * 
     * @return The newly created content document.
     * @throws WGAPIException
     */
    public WGContent createContent() throws WGAPIException {
        return createContent("");
    }

    /**
     * Returns a list with all content types stored in this database
     * 
     * @return List List of WGContentType objects
     * @throws WGAPIException
     */
    public List<WGContentType> getContentTypes() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return (List<WGContentType>) getDesignObjects(WGDocument.TYPE_CONTENTTYPE);
    }

    /**
     * Returns a content type document of the given name
     * 
     * @param name
     *            The name of the content type
     * @return WGContentType The content type if one with that name is present,
     *         null otherwise
     * @throws WGAPIException
     */
    public WGContentType getContentType(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return (WGContentType) getDesignObject(WGDocument.TYPE_CONTENTTYPE, name);
    }

    /**
     * Returns the list of TML modules in this database.
     * 
     * @throws WGAPIException
     */
    public List<WGTMLModule> getTMLModules() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return (List<WGTMLModule>) getDesignObjects(WGDocument.TYPE_TML);
    }

    /**
     * Returns the TML module for the given name and media key
     * 
     * @param name
     * @param mediaKey
     * @throws WGAPIException
     */
    public WGTMLModule getTMLModule(String name, String mediaKey) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return (WGTMLModule) getDesignObject(WGDocument.TYPE_TML, name, mediaKey);
    }

    /**
     * Returns the language of that name (code) if it exists. If the language
     * does not exist a dummy language definition is returned to prevent
     * NullPointers bc. of nonexistent language definitions. You can test for a
     * dummy language definition by calling language.isDummy().
     * 
     * @param name
     *            The language name
     * @return WGLanguage
     * @throws WGAPIException
     */
    public WGLanguage getLanguage(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (name == null) {
            return null;
        }

        WGLanguage lang = (WGLanguage) getDesignObject(WGDocument.TYPE_LANGUAGE, name);
        if (lang == null) {
            lang = (WGLanguage) createDesignDocumentObject(new WGFakeLanguage(this, name, "(Not defined)"),
                    new WGDocumentObjectFlags().setDummy(true));
        }
        return lang;
    }

    /**
     * Retrieves the best matching language definition for the given locale.
     * Language definitions are searched in descending details order:
     * language_COUNTRY_VARIANT language_COUNTRY language
     * 
     * @param locale
     *            The locale to match
     * @return The language that matches the locale best, null if none could be
     *         found
     * @throws WGAPIException
     */
    public WGLanguage getLanguageForLocale(Locale locale) throws WGAPIException {

        if (locale == null) {
            return getLanguage(getDefaultLanguage());
        }

        String langKey;
        WGLanguage lang;
        if (!WGUtils.isEmpty(locale.getCountry()) && !WGUtils.isEmpty(locale.getVariant())) {
            langKey = locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant();
            lang = getLanguage(langKey);
            if (lang != null && !lang.isDummy()) {
                return lang;
            }
        }

        if (!WGUtils.isEmpty(locale.getCountry())) {
            langKey = locale.getLanguage() + "_" + locale.getCountry();
            lang = getLanguage(langKey);
            if (lang != null && !lang.isDummy()) {
                return lang;
            }
        }

        langKey = locale.getLanguage();
        lang = getLanguage(langKey);
        if (lang != null && !lang.isDummy()) {
            return lang;
        }

        return null;

    }

    /**
     * Creates a new area definition.
     * 
     * @param name
     *            The Name of the new area
     * @return The newly created area object.
     * @throws WGAPIException
     */
    public WGArea createArea(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        performDesignCreationCheck(WGDocument.TYPE_AREA, name, null);

        name = name.toLowerCase();

        WGArea area = this.getArea(name);
        if (area != null) {
            throw new WGDuplicateKeyException("There is already an area of name '" + name + "'");
        }

        WGDocumentCore areaCore = this.getCore().createDesignDocument(WGDocument.TYPE_AREA, name, null);
        if (areaCore == null) {
            throw new WGCreationException("Unable to create area. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of areas");
        }

        area = (WGArea) createDesignDocumentObject(areaCore, new WGDocumentObjectFlags());
        return area;
    }

    /**
     * Creates a new content type.
     * 
     * @param name
     *            Name of the new content type.
     * @return The newly created content type object.
     * @throws WGAPIException
     */
    public WGContentType createContentType(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        performDesignCreationCheck(WGDocument.TYPE_CONTENTTYPE, name, null);

        WGContentType contentType = this.getContentType(name);
        if (contentType != null) {
            throw new WGDuplicateKeyException("There is already a content type of name '" + name + "'");
        }

        name = name.toLowerCase();

        WGDocumentCore ctCore = createDesignDocumentCore(WGDocument.TYPE_CONTENTTYPE, name, null);
        if (ctCore == null) {
            throw new WGCreationException("Unable to create content type. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of content types");
        }

        contentType = (WGContentType) createDesignDocumentObject(ctCore, new WGDocumentObjectFlags());
        contentType.setPositioning(WGContentType.POSITIONING_EVERYWHERE);
        // contentType.save();
        return contentType;

    }

    private WGDocumentCore createDesignDocumentCore(int type, String name, String mediaKey) throws WGAPIException {

        // check db lock
        if (getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException(
                    "Unable to create designobject with name '" + name + "'. Database is locked.");
        }

        if (designProvider != null && designProvider.providesType(type) && !isMetadataModule(type, name)) {
            return designProvider.createDesignDocument(type, name, mediaKey);
        } else {
            return getCore().createDesignDocument(type, name, mediaKey);
        }

    }

    /**
     * Creates a new TML module.
     * 
     * @param name
     *            name of the new TML module.
     * @param mediaKey
     *            Media key of the new TML module.
     * @return The newly created TML module.
     * @throws WGAPIException
     */
    public WGTMLModule createTMLModule(String name, String mediaKey) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        performDesignCreationCheck(WGDocument.TYPE_TML, name, mediaKey);

        name = name.toLowerCase();
        mediaKey = mediaKey.toLowerCase();

        WGTMLModule tmlModule = this.getTMLModule(name, mediaKey);
        if (tmlModule != null) {
            throw new WGDuplicateKeyException(
                    "There is already a tml module of name '" + name + "' and media key '" + mediaKey + "'");
        }

        WGDocumentCore tmlCore = createDesignDocumentCore(WGDocument.TYPE_TML, name, mediaKey);
        if (tmlCore == null) {
            throw new WGCreationException("Unable to create TML module. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creating TML modules.");
        }

        tmlModule = (WGTMLModule) createDesignDocumentObject(tmlCore, new WGDocumentObjectFlags());
        tmlModule.setDirectAccessAllowed(true);
        // tmlModule.save();
        return tmlModule;
    }

    /**
     * Moves a struct entry to a new position.
     * 
     * @param entry
     *            The entry to move
     * @param newParent
     *            The new parent of the entry. If it is an WGArea object the
     *            struct entry will become a root document in this area. If it
     *            is an WGStructEntry it will become a child entry of this
     *            entry.
     * @return true, if the operation succeeded, false otherwise
     * @throws WGAPIException
     */
    public boolean moveStructEntry(WGStructEntry entry, WGDocument newParent) throws WGAPIException {

        if (isSessionOpen() == false) {
            throw new WGClosedSessionException();
        }

        performStructMoveCheck(entry, newParent);

        WGDocument oldParent = (entry.isRoot() ? (WGDocument) entry.getArea() : entry.getParentEntry());
        boolean result = getCore().moveStructEntry(entry, newParent);
        if (result == false) {
            return false;
        }

        oldParent.dropCache();
        newParent.dropCache();
        entry.dropCache();

        // Must ensure that these object versions are the ones in cache
        mapDocumentObject(oldParent);
        mapDocumentObject(newParent);
        mapDocumentObject(entry);

        // Fire event "content has been moved" for each and every influenced content
        processMovedDocuments(entry, true);

        return true;

    }

    private void processMovedDocuments(WGStructEntry entry, final boolean doMoveModifications)
            throws WGAPIException {

        // Clear cache on old parent (if still in cache #00004198)
        try {
            for (Serializable keyStr : this.masterDocumentsByKey.getEntryKeys()) {
                WGDocumentKey key = new WGDocumentKey((String) keyStr);
                if (key.getDocType() == WGDocument.TYPE_STRUCTENTRY) {
                    Object structObj = this.masterDocumentsByKey.read(keyStr);
                    if (structObj instanceof WGStructEntry) { // May still be a NullPlaceHolder
                        WGStructEntry struct = (WGStructEntry) structObj;
                        if (struct.isContainedInChildEntryCache(entry)) {
                            struct.dropCache();
                        }
                    }
                }
                if (key.getDocType() == WGDocument.TYPE_AREA) {
                    Object areaObj = this.masterDocumentsByKey.read(keyStr);
                    if (areaObj instanceof WGArea) {
                        WGArea area = (WGArea) areaObj;
                        if (area.isContainedInRootEntryCache(entry)) {
                            area.dropCache();
                        }
                    }
                }
            }
        } catch (CacheException e1) {
            WGFactory.getLogger().error("Exception processing moved page " + entry.getStructKey(), e1);
        }

        // Clear caches on moved tree
        entry.visit(new WGPageVisitor() {
            public void visit(WGArea area) {
            };

            public void visit(WGStructEntry entry) {
                try {
                    entry.dropCache();
                } catch (WGAPIException e) {
                    WGFactory.getLogger().error("Exception firing event 'contentHasBeenMoved'", e);
                }
            };

            public void visit(WGContent content) {
                try {
                    content.dropCache();

                    if (doMoveModifications) {
                        boolean shouldBeVisible = !(content.hasCompleteRelationships()
                                && content.getStructEntry().getArea().isSystemArea());
                        if (content.isVisible() != shouldBeVisible) {
                            content.setVisible(shouldBeVisible);
                            content.save(content.getLastModified());
                        }
                    }

                    WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_HASBEENMOVED,
                            content.getDocumentKey(), content.getStructEntry().getContentType().getName(),
                            content.getDatabase());
                    event.setContent(content);
                    fireContentEvent(event);
                } catch (WGAPIException e) {
                    WGFactory.getLogger().error("Exception firing event 'contentHasBeenMoved'", e);
                }
            }
        });
    }

    /**
     * Controls if the current user may move a struct entry to the given new parent. If so the method exists normally. If not an exception is thrown.
     * @param entry The entry to move
     * @param newParent The new parent of the entry. May be an {@link WGArea} or another {@link WGStructEntry}.
     * @throws WGAPIException If the user may not move the document. The exception informs about the reason.
     */
    public void performStructMoveCheck(WGStructEntry entry, WGDocument newParent)
            throws WGIllegalArgumentException, WGAPIException, WGAuthorisationException, ResourceIsLockedException {
        if (entry == null) {
            throw new WGIllegalArgumentException("Entry to move is null");
        }

        if (newParent == null) {
            throw new WGIllegalArgumentException("New parent is null");
        }

        if (newParent.getDocumentKey().equals(entry.getDocumentKey())) {
            throw new WGIllegalArgumentException("You are trying to make a structentry the child entry of itself!");
        }

        if (newParent instanceof WGStructEntry && ((WGStructEntry) newParent).isDescendantOf(entry)) {
            throw new WGIllegalArgumentException(
                    "You are trying to move a structentry into it's own child hierarchy!");
        }

        if (entry.getDatabase() != newParent.getDatabase()) {
            throw new WGIllegalArgumentException(
                    "The databases of the entry to move and the new parent document are different");
        }

        WGDocument restrictingDoc = entry.testEditPageHierarchyRights();
        if (restrictingDoc != null) {
            String errorCode = (restrictingDoc instanceof WGArea
                    ? WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA
                    : WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGE);
            throw new WGAuthorisationException("You are not allowed to edit this struct entry because document '"
                    + restrictingDoc.getDocumentKey() + "' disallows it.", errorCode, restrictingDoc);
        }

        if (!getSessionContext().getUserAccess().mayMoveStructEntries()) {
            throw new WGAuthorisationException("You are not allowed to move struct entries in this database",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_MOVEPAGE_RIGHTS);
        }

        if (newParent instanceof WGArea) {
            WGArea area = (WGArea) newParent;
            if (!area.mayEditPages()) {
                throw new WGAuthorisationException("You are not allowed to move structs to this area",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA);
            }
            if (entry.getContentType() != null) {
                area.testRootPageRestrictions(entry.getContentType());
            }
        } else if (newParent instanceof WGStructEntry) {
            WGStructEntry parentEntry = (WGStructEntry) newParent;
            restrictingDoc = parentEntry.testEditChildPagesHierarchyRights();
            if (restrictingDoc != null) {
                String errorCode = (restrictingDoc instanceof WGArea
                        ? WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA
                        : WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGE);
                throw new WGAuthorisationException(
                        "You are not allowed to move a struct entry below this parent entry because document '"
                                + restrictingDoc.getDocumentKey() + "' disallows it.",
                        errorCode, restrictingDoc);
            }
            if (entry.getContentType() != null) {
                parentEntry.testChildPageRestrictions(entry.getContentType());
            }
        }

        WGArea targetArea = (newParent instanceof WGArea ? (WGArea) newParent
                : ((WGStructEntry) newParent).getArea());
        if (!targetArea.isSystemArea() && !entry.getContentType().mayCreateChildEntry(newParent)) {
            throw new WGAuthorisationException(
                    "The content type of the struct cannot be used at the target position",
                    WGAuthorisationException.ERRORCODE_OP_DENIED_BY_AREA, entry.getContentType());
        }

        // check lock status of entry to move
        if (entry.getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException("Cannot move structentry. Given entry is locked.");
        }

        // check lock status of newParent
        if (newParent.getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException("Cannot move structentry. New parentEntry is locked.");
        }

        // Check edit rights for all documents in moved page hierarchy
        entry.performSubtreeModificationCheck();

    }

    /**
     * Creates a new Script module
     * 
     * @param name
     *            Name of the new module
     * @param type
     *            Type of the module. A constant WGCSSJSModule.CODETYPE_...
     * @throws WGAPIException
     */
    public WGCSSJSModule createCSSJSModule(String name, String type) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        performDesignCreationCheck(WGDocument.TYPE_CSSJS, name, type);

        name = name.toLowerCase();

        WGCSSJSModule cssLib = this.getCSSJSModule(name, type);
        if (cssLib != null) {
            throw new WGDuplicateKeyException(
                    "There is already a css/js library of name '" + name + "' and type '" + type + "'");
        }

        WGDocumentCore cssCore = createDesignDocumentCore(WGDocument.TYPE_CSSJS, name, type);
        if (cssCore == null) {
            throw new WGCreationException("Unable to create css/js library. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of css/js libraries");
        }

        cssLib = (WGCSSJSModule) createDesignDocumentObject(cssCore, new WGDocumentObjectFlags());
        cssLib.setMetaData(WGCSSJSModule.META_CODETYPE, type);
        // cssLib.save();
        return cssLib;

    }

    /**
     * Controls if the current user may create a design document with the given data. If so the method exists normally. If not an exception is thrown.
     * @param type The type of design document. Use constants WGDocument.TYPE_...
     * @param name The name of design document
     * @param mediaKey For WebTML modules specify media key, for script modules specify code type (Constants WGScriptModule.CODETYPE_...). For other types specify null.
     * @throws WGAPIException If the creation for the given data would fail
     */
    public void performDesignCreationCheck(int type, String name, String mediaKey) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getLockStatus() == Lock.LOCKED_BY_FOREIGN_OBJECT) {
            throw new ResourceIsLockedException("Unable to create design document. Database is locked.");
        }

        if (type == WGDocument.TYPE_AREA || type == WGDocument.TYPE_CONTENTTYPE
                || type == WGDocument.TYPE_LANGUAGE) {
            if (getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_CHIEF_EDITOR) {
                throw new WGAuthorisationException("You are not allowed to create schema documents",
                        WGAuthorisationException.ERRORCODE_OP_NEEDS_CHIEF_EDITOR_RIGHTS);
            }
        } else {
            if (getSessionContext().getAccessLevel() != WGDatabase.ACCESSLEVEL_MANAGER) {
                throw new WGAuthorisationException("You are not allowed to create design documents",
                        WGAuthorisationException.ERRORCODE_OP_NEEDS_DESIGNER_RIGHTS);
            }
        }

        if (type == WGDocument.TYPE_CSSJS && isMetadataModule(WGDocument.TYPE_CSSJS, name)
                && !WGCSSJSModule.CODETYPE_XML.equals(mediaKey)) {
            throw new WGIllegalArgumentException("Metadata modules must be of codetype XML");
        }

        if (!isDoctypeModifiable(type)) {
            throw new WGIllegalStateException(
                    "Updating this type of design document via WGAPI is not permitted in this database: "
                            + WGDocument.doctypeNumberToName(type),
                    WGIllegalStateException.ERRORCODE_DOCTYPE_READONLY);
        }
    }

    /**
     * Returns the script modules in this database.
     * 
     * @throws WGAPIException
     */
    public List getCSSJSModules() throws WGAPIException {
        return getDesignObjects(WGDocument.TYPE_CSSJS);
    }

    /**
     * Returns the script module of the given name.
     * 
     * @param name
     * @throws WGAPIException
     */
    public WGScriptModule getCSSJSModule(String name) throws WGAPIException {

        // Changed backend semantics with WGA5:
        // When no codetype given we iterate over the available codetypes and request the separately

        Iterator types = WGScriptModule.METAINFO_CODETYPE.getAllowedValues().iterator();
        while (types.hasNext()) {
            String type = (String) types.next();
            WGScriptModule mod = getCSSJSModule(name, type);
            if (mod != null) {
                return mod;
            }
        }

        return null;
    }

    /**
     * Returns the script module of the given name and type
     * 
     * @param name
     * @throws WGAPIException
     */
    public WGScriptModule getCSSJSModule(String name, String type) throws WGAPIException {
        return (WGScriptModule) getDesignObject(WGDocument.TYPE_CSSJS, name, type);
    }

    /**
     * Returns the list of file containers in this database
     * 
     * @throws WGAPIException
     */
    public List<WGFileContainer> getFileContainers() throws WGAPIException {
        return (List<WGFileContainer>) getDesignObjects(WGDocument.TYPE_FILECONTAINER);
    }

    /**
     * Return the file container of the given name
     * 
     * @param name
     * @throws WGAPIException
     */
    public WGFileContainer getFileContainer(String name) throws WGAPIException {
        return (WGFileContainer) getDesignObject(WGDocument.TYPE_FILECONTAINER, name);
    }

    /**
     * Creates a new file container.
     * 
     * @param name
     *            The name of the file container.
     * @return The newly created file container.
     * @throws WGAPIException
     */
    public WGFileContainer createFileContainer(String name) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        performDesignCreationCheck(WGDocument.TYPE_FILECONTAINER, name, null);

        name = name.toLowerCase();

        WGFileContainer file = this.getFileContainer(name);
        if (file != null) {
            throw new WGDuplicateKeyException("There is already a file container of name '" + name + "'");
        }

        WGDocumentCore fileCore = createDesignDocumentCore(WGDocument.TYPE_FILECONTAINER, name, null);
        if (fileCore == null) {
            throw new WGCreationException("Unable to create file container. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of file containers");
        }

        file = (WGFileContainer) createDesignDocumentObject(fileCore, new WGDocumentObjectFlags());
        // file.save();
        return file;

    }

    /**
     * Creates a new language definition only by name, which is also used as title
     * 
     * @param name
     *            The name (i.e. language code) of the new language.
     *            language
     * @return The newly created language definition.
     * @throws WGAPIException
     */
    public WGLanguage createLanguage(String name) throws WGAPIException {
        return createLanguage(name, name);
    }

    /**
     * Creates a new language definition.
     * 
     * @param name
     *            The name (i.e. language code) of the new language.
     * @return The newly created language definition.
     * @throws WGAPIException
     */
    public WGLanguage createLanguage(String name, String title)
            throws WGClosedSessionException, WGAPIException, WGDuplicateKeyException, WGCreationException {
        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        performDesignCreationCheck(WGDocument.TYPE_LANGUAGE, name, null);

        name = name.toLowerCase();

        WGLanguage language = this.getLanguage(name);
        if (language != null && language.isDummy() == false) {
            throw new WGDuplicateKeyException("There is already a language of name '" + name + "'");
        }

        WGDocumentCore langCore = this.getCore().createDesignDocument(WGDocument.TYPE_LANGUAGE, name, null);
        if (langCore == null) {
            throw new WGCreationException("Unable to create language. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support creation of languages");
        }

        language = (WGLanguage) createDesignDocumentObject(langCore, new WGDocumentObjectFlags());
        language.setTitle(title);
        return language;
    }

    /**
     * Creates a draft copy of the given content document, keeping it's language
     * 
     * @param content
     * @return The draft copy of the content.
     * @throws WGAPIException
     */
    public WGContent createDraftCopy(WGContent content) throws WGAPIException {

        return createDraftCopy(content, null);
    }

    /**
     * Creates a draft copy of the given content, using the language given as
     * parameter.
     * 
     * @param content
     *            The content to copy
     * @param language
     *            The language for the draft copy. Specify null to keep the
     *            language of the original.
     * @return The draft copy. This document is already stored when it is
     *         returned.
     * @throws WGAPIException
     */
    public WGContent createDraftCopy(WGContent content, WGLanguage language) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (language == null) {
            language = content.getLanguage();
        }

        if (!content.isSaved()) {
            throw new WGIllegalStateException("Source content has not yet been saved");
        }

        // check if structentry is saved
        if (!content.getStructEntry().isSaved()) {
            throw new WGIllegalStateException("StructEntry of source content has no yet been saved.");
        }

        if (!language.isSaved()) {
            throw new WGIllegalStateException("Language for this content has not yet been saved");
        }

        // Test for validity and access rights
        if (content.getStatus().equals(WGContent.STATUS_RELEASE) == false
                && content.getStatus().equals(WGContent.STATUS_ARCHIVE) == false) {
            throw new WGIllegalStateException("This content is not in status RELEASE");
        }

        if (getSessionContext().getAccessLevel() < ACCESSLEVEL_AUTHOR) {
            throw new WGAuthorisationException("User is not allowed to create content in this database",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_AUTHOR_LEVEL);
        }

        if (getSessionContext().getAccessLevel() < ACCESSLEVEL_EDITOR) {
            if (!content.isAuthorOrOwner()) {
                throw new WGAuthorisationException(
                        "You are not allowed to create a draft copy of this content bc. you are not the author of the published version.",
                        WGAuthorisationException.ERRORCODE_OP_NEEDS_AUTHORING_RIGHTS);
            }
        }

        /*
        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && content.getStructEntry().testEditPageHierarchyRights() != null) {
        throw new WGAuthorisationException("User is not allowed to create content under this struct entry", WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGE, content.getStructEntry());
        }
        */
        if (hasFeature(FEATURE_FULLCONTENTFEATURES) && !content.mayEditContent()) {
            throw new WGAuthorisationException("User is not allowed to create content under this struct entry",
                    WGAuthorisationException.ERRORCODE_OP_DENIED_BY_PAGE, content.getStructEntry());
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            WGContentType contentType = content.getStructEntry().getContentType();
            if (contentType != null && !contentType.mayCreateContent()) {
                throw new WGAuthorisationException("User is not allowed to use this content type",
                        WGAuthorisationException.ERRORCODE_OP_DENIED_BY_CONTENTTYPE, contentType);
            } else if (contentType == null
                    && getSessionContext().getAccessLevel() != WGDatabase.ACCESSLEVEL_MANAGER) {
                throw new WGIllegalArgumentException("You cannot create struct entries without a content type");
            }
        }

        WGContent newContent = createNewDraftVersion(content, language);

        // Initialize
        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            newContent.setStatus(WGContent.STATUS_DRAFT);
            newContent.setMetaData(WGContent.META_AUTHOR, getSessionContext().getUser());
            newContent.setValidity(null, null);
        }

        /*
        // Initialize by workflow engine
        WGWorkflow workflow = getWorkflowEngine().getWorkflow(newContent);
        workflow.initialize();
        */

        if (!projectMode) {
            newContent.addWorkflowHistoryEntry("Draft copy created");
        } else {
            newContent.addWorkflowHistoryEntry("Changed to draft status in project mode");
        }

        WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, newContent.getDocumentKey(),
                content.getStructEntry().getContentType().getName(), this);
        event.setContent(newContent);
        fireContentEvent(event);

        // Save and return
        newContent.save();
        return newContent;

    }

    private synchronized WGContent createNewDraftVersion(WGContent content, WGLanguage language)
            throws WGAPIException, WGCreationException {
        // Evaluate new version number
        int maxVersion = findNewVersionNumber(content.getStructEntry(), language);

        // We will update the last changed date only, when there are no
        // pending changes in background
        boolean updateLastChanged = !isDatabaseUpdatedInBackground();

        // Create core and pre-initialize to be a valid WGContent object
        WGDocumentCore docCore = getCore().createCopy(content.getCore());
        if (docCore == null) {
            throw new WGCreationException("Unable to create draft copy. Maybe databases of type "
                    + getCore().getClass().getName() + " do not support copying content");
        }

        if (hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            docCore.setMetaData(WGContent.META_VERSION, new Integer(maxVersion));
            docCore.setMetaData(WGContent.META_LANGUAGE, language.getName());
        }

        // Create WGContent object
        WGContent newContent = createContentObject(docCore);
        return newContent;
    }

    /**
     * Indicates, if document caching is enabled globally for the database
     */
    public boolean isCachingEnabled() {
        return cachingEnabled;
    }

    /**
     * Controls if the document caching is enabled for this database. It is
     * enabled by default. Turning it off may speed up writing operations but
     * will mean significant overhead to recurring reading operations.
     */
    public void setCachingEnabled(boolean b) {
        cachingEnabled = b;
    }

    /**
     * Starts a database transaction. The database must support this, which is
     * indicated by the feature WGDatabase.FEATURE_TRANSACTIONS.
     * <p>
     * NOTE: This transaction functionality is for WGAPI internal use only!  Use {@link WGDatabase#startTransaction()} for external use.
     * </p>
     * @return If the starting of the transaction succeeds
     * @deprecated Use {@link #startTransaction()}
     */
    public boolean beginTransaction() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        boolean result = getCore().beginTransaction();
        if (result == true) {
            getSessionContext().addTransactionToStack(getSessionContext().getLegacyFakeTransaction());
            return true;
        } else {
            return false;
        }
    }

    /**
     * Starts a transaction and returns a transaction object.
     * Changes done while the transaction runs will not be written to backend until {@link WGTransaction#commit()} is called.
     * Calling {@link WGTransaction#rollback()} will revert all changes done in the transaction.
     * If this is called while already in a transaction it returns a cascaded transaction. The commit of the cascaded transaction will be a no-op. Rolling back will roll back the real transaction.
     * If this is called on a database backend which does not support transaction it returns a fake transaction with no-op operations.
     * @return The transaction object
     * @throws WGAPIException
     */
    public WGTransaction startTransaction() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        WGTransaction trans = null;
        if (!hasFeature(FEATURE_TRANSACTIONS)) {
            trans = new WGFakeTransaction();
        } else if (!getSessionContext().getTransactionsStack().isEmpty()) {
            trans = new WGCascadedTransaction();
        } else {
            boolean result = getCore().beginTransaction();
            if (result == true) {
                trans = new WGRealTransaction();
            } else {
                throw new WGBackendException("Starting transaction failed");
            }
        }

        getSessionContext().addTransactionToStack(trans);
        return trans;

    }

    /**
     * Commits a database transaction, executing all data changes since the
     * start of the transaction. A transaction must be started with
     * beginTransaction() before calling this. The database must support
     * transactions, which is indicated by the feature
     * WGDatabase.FEATURE_TRANSACTIONS.
     * <p>
     * NOTE: This transaction functionality is for WGAPI internal use only!  Use {@link WGDatabase#startTransaction()} for external use.
     * </p>
     * @return If the committing of the transaction succeeds
     * @deprecated Use {@link WGTransaction#commit()}
     */
    public boolean commitTransaction() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        boolean result = getCore().commitTransaction();
        if (result == true) {
            getSessionContext().removeTransactionFromStack(getSessionContext().getLegacyFakeTransaction());
            MasterSessionTask performDBUpdates = new MasterSessionTask(this) {

                @Override
                protected void exec(WGDatabase db) throws Throwable {
                    db.checkDatabaseUpdates(true);
                }
            };
            try {
                performDBUpdates.runWithExceptions();
            } catch (Throwable e) {
                throw new WGBackendException("Unable to perform db update on transaction commit.", e);
            }
            return true;
        } else {
            return false;
        }
    }

    protected boolean doCommitTransaction() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        boolean result = getCore().commitTransaction();
        if (result == true) {
            verboseCacheManagement("Committing transaction. Doing backup changes catchup");
            catchupBackendChanges();
            return true;
        } else {
            throw new WGBackendException("Transaction commit failed");
        }

    }

    /**
     * Rolls a database transaction back, canceling all data changes done since
     * the start of the transaction. A transaction must be started with
     * beginTransaction() prior to doing this. The database must support
     * transactions, which is indicated by the feature
     * WGDatabase.FEATURE_TRANSACTIONS.
     * <p>
     * NOTE: This transaction functionality is for WGAPI internal use only!  Use {@link WGDatabase#startTransaction()} for external use.
     * </p>
     * @return If the rolling back of the transaction succeeds
     * @deprecated use {@link WGTransaction#rollback()}
     */
    public boolean rollbackTransaction() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        boolean result = getCore().rollbackTransaction();
        if (result == true) {
            getSessionContext().removeTransactionFromStack(getSessionContext().getLegacyFakeTransaction());
            return true;
        } else {
            return false;
        }
    }

    protected boolean doRollbackTransaction() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        boolean result = getCore().rollbackTransaction();
        if (result == true) {
            getSessionContext().clearCache();
            return true;
        } else {
            return false;
        }

    }

    /**
     * Retrieves a WGACL object which manages the access control to this
     * database. Is only supported by databases with feature
     * WGDatabase.FEATURE_ACL_MANAGEABLE
     */
    public WGACL getACL() {

        WGACLCore core = getCore().getACL();
        if (core != null) {
            return new WGACL(this, core);
        } else {
            return null;
        }
    }

    /**
     * Returns a HashMap that will store userdefined data for the currently
     * logged in user. Each user has his own hash map. This information persists
     * sessions and can be fetched again in later sessions.
     */
    public UserHashMap getUserCache() {
        return _userCache;
    }

    /**
     * Returns the group of UserHashMaps for this database. Should not be used
     * outside the WGAPI.
     */
    public UserHashMapGroup getUserHashMapGroup() {
        return userHashMapGroup;
    }

    /**
     * Returns the native backend object for this database if there is one.
     * 
     * @throws WGBackendException
     */
    public Object getNativeObject() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return getCore().getNativeObject();

    }

    /**
     * Performs all the neccessary operations after a document has been saved: -
     * Update caches - fire events - set states Either param document or param
     * log must be filled to indicate the document
     * 
     * @param document
     *            The document that has been saved, if it is available, else
     *            null
     * @param log
     *            An Update log that triggered this document saved operation
     * @param isNewDoc
     *            Determines if this is a newly created doc that has been saved
     *            for the first date
     * @param savedViaWGA
     *            Determines if the document has been saved via WGA (and not in
     *            background)
     * @throws WGAPIException
     */
    protected void documentSaved(WGDocument document, WGDocumentKey docKey, boolean isNewDoc, boolean savedByWGAPI,
            WGDatabaseRevision revision) throws WGAPIException {

        if (getSessionContext().isTransactionActive()) {
            return;
        }

        int docType = docKey.getDocType();

        // In case of contents and struct entries we must always load the document to do effective cache management
        if (document == null && (docType == WGDocument.TYPE_CONTENT || docType == WGDocument.TYPE_STRUCTENTRY)) {
            document = getDocumentByKey(docKey);
        }

        // Operations only neccessary if the document is currently in cache =
        // document is present
        if (document != null) {

            document.dropCache();

            // Must be done to ensure that the most up-to-date object is mapped
            mapDocumentObject(document);

            // Clear related caches
            if (document instanceof WGContent) {
                WGContent content = (WGContent) document;

                if (!content.getStatus().equals(WGContent.STATUS_DRAFT)
                        && !content.getStatus().equals(WGContent.STATUS_REVIEW)) {

                    // Flush the query cache
                    try {
                        verboseCacheManagement(
                                "Flushing query cache on behalf of saved document " + docKey.toString());
                        masterQueryCache.flushAll();
                    } catch (CacheException e) {
                        WGFactory.getLogger()
                                .error("Exception flushing query cache on database '" + getDbReference() + "'", e);
                    }
                    content.getStructEntry().dropCache();
                }

                if (isNewDoc) {
                    content.getStructEntry().dropCache();
                }
            } else if (document instanceof WGStructEntry) {
                WGStructEntry structEntry = (WGStructEntry) document;
                if (structEntry.isRoot()) {
                    structEntry.getArea().dropCache();
                } else {
                    structEntry.getParentEntry().dropCache();
                }
            }
        }

        // Clear indirect caches
        if (isDesignDocumentType(docType)) {
            designDocumentLists.remove(new Integer(docType));
        }

        _userCache.clear();

        // Fire content has been saved event
        if (docType == WGDocument.TYPE_CONTENT) {
            String contentType = null;
            if (document != null) {
                WGContent content = (WGContent) document;
                if (content.hasCompleteRelationships()) {
                    contentType = content.getStructEntry().getContentType().getName();
                }
            }
            WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_HASBEENSAVED, docKey.toString(),
                    contentType, this);
            if (document != null) {
                event.setContent((WGContent) document);
            }
            fireContentEvent(event);
        }

        // Fire design change event
        if (docType == WGDocument.TYPE_FILECONTAINER || docType == WGDocument.TYPE_TML
                || docType == WGDocument.TYPE_CSSJS) {
            // Only fire when no design provider present. Otherwise the
            // listeners are registered at the provider and it is his
            // responsibility to throw the event
            if (getDesignProvider() == null) {
                List logs = new ArrayList();
                logs.add(new WGUpdateLog(WGUpdateLog.TYPE_UPDATE, new Date(), getSessionContext().getUser(),
                        docKey.toString(), null, revision));
                fireDatabaseDesignChangedEvent(new WGDesignChangeEvent(null, this, logs));
            }
        }

        updateCacheMaintenanceData();

        if (savedByWGAPI) {
            this.getSessionContext().setDatabaseUpdated(true);
            // Fire database event if the remove has been done by WGAPI
            // When this is done in background change processing, the event will
            // get fired after all changes by checkDatabaseUpdates
            fireDatabaseEvent(new WGDatabaseEvent(this, WGDatabaseEvent.TYPE_UPDATE, document));

        }

        if (document != null && revision != null) {
            document.setCacheRevision(revision);
        }

    }

    private void updateRevision(WGDatabaseRevision revision) throws WGAPIException {
        if (getSessionContext() != null && getSessionContext().isTransactionActive()) {
            return;
        }
        _revision = revision;

        if (_revision == null) {
            _revision = WGDatabaseRevision.forValue(new Date(Long.MIN_VALUE));
            _revisionDate = new Date(Long.MIN_VALUE);
        } else {
            if (_revision.getRevisionValue() instanceof Date) {
                _revisionDate = (Date) _revision.getRevisionValue();
            } else {
                _revisionDate = getCore().getRevisionDate(_revision.getRevisionValue());
            }
        }

    }

    private void updateCacheMaintenanceData() {
        lastCacheMaintenance = new Date();
    }

    /**
     * Tests if the given document type is the type of a design document, i.e.
     * an area, contenttype, script module, file container, language definition
     * or tml module
     * 
     * @param docType
     * @return true, if the doctype is a design document type, false if not
     */
    public static boolean isDesignDocumentType(int docType) {

        return (docType == WGDocument.TYPE_AREA || docType == WGDocument.TYPE_CONTENTTYPE
                || docType == WGDocument.TYPE_CSSJS || docType == WGDocument.TYPE_FILECONTAINER
                || docType == WGDocument.TYPE_LANGUAGE || docType == WGDocument.TYPE_TML);

    }

    /**
     * Returns the authentication module used to authenticate sessions, if this database supports and uses a auth module.
     * If this database uses a {@link RedirectionAuthModule} this method returns the real backend module. So this method should be used
     * to access the module if some special, non-standard, capabilities are to be used
     */
    public AuthenticationModule getAuthenticationModule() {

        AuthenticationModule backendModule = _authenticationModule;
        while (backendModule instanceof RedirectionAuthModule) {
            backendModule = ((RedirectionAuthModule) backendModule).getBackendModule();
        }

        return backendModule;
    }

    /**
     * Returns the password of the master login used to open this database.
     */
    public String getMasterLoginPassword() {
        return masterLoginPassword;
    }

    /**
     * Returns the maximum cores (i.e. backend document) that a session is
     * allowed to hold at once. If more are retrieved, the oldest cores will be
     * dropped to remain under this threshold.
     */
    public int getMaxCores() {
        return maxCores;
    }

    /**
     * Returns collected statistics of sessions that exceeded the maxdocs
     * threshold. A maximum of 100 statistics is collected. After that the
     * statistics of the top 100 statistics with the most docs retrieved are
     * kept.
     */
    public SortedBag getSessionStatistics() {
        return sessionStatistics;
    }

    /**
     * Sets the title of the database. This is not stored in the backend
     * database but only served via "getTitle()" until the database object is
     * closed.
     * 
     * @param string
     *            The title to set
     */
    public void setTitle(String string) {
        title = string;
    }

    /**
     * Retrieves logs of documents that were modified since a cutoff revision. This
     * is only available in databases with feature WGDatabase.FEATURE_FIND_UPDATED_DOCS. 
     * The returned list does not reflect all changes done to the documents but returns
     * a "condensed" list of operations reflecting the current state of documents
     * in the smallest possible way:
     * <ul>
     * <li>If a document has been modified multiple times in a row there will only be one update log, no matter how much updates have been done
     * <li>If a document was moved multiple times in a row and still exists there will be one move log, no matter how much updates have been done
     * </ul>
     * 
     * @param cutoff
     *            The cutoff revision. Documents modified earlier will not be
     *            included in the result
     * @return List containing the log objects of type WGUpdateLog
     * @throws WGAPIException
     */
    public List<WGUpdateLog> getUpdatedDocumentsSince(Comparable cutoff) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (cutoff instanceof WGDatabaseRevision) {
            cutoff = ((WGDatabaseRevision) cutoff).getRevisionValue();
        }

        List<WGUpdateLog> logs = getCore().getUpdateLogs(cutoff);
        List<WGUpdateLog> condensedLogs = new ArrayList<WGUpdateLog>();

        // Filter logs to get the "condensed" list described in javadoc
        Map<String, Integer> lastOperation = new HashMap<String, Integer>();

        for (WGUpdateLog log : logs) {

            Integer op = lastOperation.get(log.getDocumentKey());
            if (op == null || op != log.getType()) {
                condensedLogs.add(log);
                lastOperation.put(log.getDocumentKey(), log.getType());
            }
        }

        return condensedLogs;

    }

    /**
     * Returns if the given struct entry contains a content document. This
     * method is capable of regarding content documents that the current user is
     * not allowed to see.
     * 
     * @param entry
     *            The entry to test
     * @return true, if there exists a content (even those that the user may not
     *         see), false otherwise
     * @throws WGAPIException
     */
    public boolean hasContents(WGStructEntry entry) throws WGAPIException {

        if (isSessionOpen() == false) {
            throw new WGClosedSessionException();
        }

        if (entry == null || entry.isDeleted() || entry.isDummy()) {
            return false;
        }

        return (entry.getContentCount() > 0);

    }

    /**
     * Returns a document by it's document key, only if it is contained in
     * cache.
     * 
     * @param documentKeyStr
     *            The document key in its string representation
     * @return The document if it already is cached, null otherwise
     * @throws WGAPIException
     */
    public WGDocument getDocumentByDocumentKeyFromCache(String documentKeyStr) throws WGAPIException {

        WGDocumentKey documentKey = new WGDocumentKey(documentKeyStr);

        return getDocumentByDocumentKeyFromCache(documentKey);

    }

    /**
     * Returns a document by it's document key, only if it is contained in
     * cache.
     * 
     * @param documentKey
     *            The document key
     * @return The document if it already is cached, null otherwise
     * @throws WGAPIException
     */
    public WGDocument getDocumentByDocumentKeyFromCache(WGDocumentKey documentKey) throws WGAPIException {
        try {
            return getDocumentByDocumentKeyFromCache(documentKey, true);
        } catch (WGDocumentDoesNotExistException e) {
            return null;
        }
    }

    /**
     * Retrieves a document from document cache.
     * @param documentKey The key of the document.
     * @param userMode true means that this is a fetch for an individual user and his access rights and session caching setting have to take effect. false is "internal mode" where the API tries to retrieve a document object without any checks.
     * @return Fetched document or null
     * @throws WGAPIException
     */
    private WGDocument getDocumentByDocumentKeyFromCache(WGDocumentKey documentKey, boolean userMode)
            throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        // Test session private cache
        WGSessionContext.DocumentContext docContext = getSessionContext().getDocumentContext(documentKey);
        if (docContext != null && docContext.getDocument() != null && !docContext.getDocument().isDeleted()) {
            if (docContext.getDocument().isReadableForUser()) {
                return docContext.getDocument();
            } else {
                throw new WGDocumentDoesNotExistException();
            }
        }

        Object cache;
        Object cacheKey;
        int docType = documentKey.getDocType();
        if (!isDoctypeCacheable(docType)) {
            return null;
        }

        // From here user calls may only proceed if caching is enabled on the session
        if (userMode && !getSessionContext().isCachingEnabled()) {
            return null;
        }

        // Test public cache
        cache = masterDocumentsByKey;
        cacheKey = documentKey;
        WGDocument doc = fetchDocumentFromCache(cache, cacheKey, !userMode);
        if (doc != null && !doc.isDeleted()) {
            return doc;
        } else {
            return null;
        }
    }

    /**
     * Given a list of user/group names from an access control field (e.g.
     * META_READERS in Content) tests if anyone is allowed the operation.
     * 
     * @param users
     *            The list of user/group names
     * @param emptyMeansYes
     *            Determines if an empty field (size = 0; first element null or
     *            empty string) should return true
     * @return true, if regarding this user/group list anyone is allowed
     */
    public static boolean anyoneAllowed(List users, boolean emptyMeansYes) {

        if (users == null) {
            return emptyMeansYes;
        }

        if (users.size() == 0
                || (users.size() == 1 && (users.get(0) == null || users.get(0).toString().trim().equals("")))) {
            return emptyMeansYes;
        }

        if (users.contains("*")) {
            return true;
        }

        return false;

    }

    /**
     * Given a list of user/group names from an access control field (e.g.
     * META_READERS in Content) tests if anyone is allowed the operation. In
     * this version of the method, an empty field (size = 0 or first element
     * null or empty string) is always treated as true.
     * 
     * @param users
     *            The list of user/group names
     * @return true, if regarding this user/group list anyone is allowed
     */
    public static boolean anyoneAllowed(List users) {
        return anyoneAllowed(users, true);
    }

    /**
     * Returns an Iterator iterating over all content documents in this database
     * does not include archived documents
     * 
     * @throws WGAPIException
     */
    public Iterator<WGContent> getAllContent() throws WGAPIException {
        return getAllContent(false);
    }

    /**
     * Returns an Iterator iterating over all content documents in this database
     * 
     * @param includeArchived
     *            Should archived documents been included? true/false
     * @throws WGAPIException
     */
    public Iterator<WGContent> getAllContent(boolean includeArchived) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return new WGContentIterator(this, includeArchived);
    }

    /**
     * Returns a database reference to be used throughout the application (in
     * WGA: the database key)
     */
    public String getDbReference() {
        return dbReference;
    }

    /**
     * Sets a reference for this database to use throughout the application.
     */
    public void setDbReference(String dbReference) {
        this.dbReference = dbReference;
    }

    /**
     * Default implementation for building user details for an authentication
     * session. This one can be used by DB implementations that implement
     * FEATURE_ACL_MANAGEABLE. This method is not for direct usage outside of
     * the WGAPI.
     * 
     * @param authSession
     *            The authentication session to create details for
     * @return User details object
     * @throws WGBackendException
     */
    public WGUserDetails defaultBuildUserDetails(AuthenticationSession authSession) throws WGAPIException {

        // First try to retrieve from user cache
        WGUserDetails userDetails = null;
        Map<Object, Object> userCache = null;
        if (this.userCachesEnabled) {
            userCache = getUserCache().getMapForUser(getUserCacheKey(authSession));
            userDetails = (WGUserDetails) userCache.get(USERCACHE_USERDETAILS);
            if (userDetails != null && !userDetails.isOutdated()) {
                return userDetails;
            }
        }

        WGACL acl = getACL();
        Map users = acl.getUsers();
        Set entries = users.keySet();
        Set roles = new HashSet();
        List<String> allMatchingEntries = new ArrayList<String>();
        List<String> bestMatchingEntries = new ArrayList<String>();
        boolean inheritRoles = true;

        // Try to find matching username (always has highest priority for
        // accessLevelEntry)
        String matchingEntryName = (String) WGUtils.containsAny(entries, authSession.getNames());
        if (matchingEntryName != null) {
            WGACLEntry accessLevelEntry = acl.getEntry(matchingEntryName);
            bestMatchingEntries.add(accessLevelEntry.getName());
            allMatchingEntries.add(accessLevelEntry.getName());
            WGACLEntryFlags parsedFlags = acl.parseFlags(accessLevelEntry);
            roles.addAll(parsedFlags.getRoles());
            if (parsedFlags.isNoRoleInheritance()) {
                inheritRoles = false;
            }
        }

        // Try to find matching groups (largest access level will be used as
        // accessLevelEntry)

        Set groups = new HashSet(entries);
        groups.retainAll(authSession.getGroups());
        boolean addToBestMatching = bestMatchingEntries.size() == 0;
        if (groups.size() > 0) {
            Iterator groupsIt = groups.iterator();
            boolean stopInheritingRoles = false;
            while (groupsIt.hasNext()) {
                WGACLEntry currentEntry = (WGACLEntry) acl.getEntry((String) groupsIt.next());
                if (addToBestMatching) {
                    bestMatchingEntries.add(currentEntry.getName());
                }
                allMatchingEntries.add(currentEntry.getName());
                WGACLEntryFlags parsedFlags = acl.parseFlags(currentEntry);
                if (inheritRoles) {
                    roles.addAll(parsedFlags.getRoles());
                }
                if (parsedFlags.isNoRoleInheritance()) {
                    stopInheritingRoles = true;
                }

            }

            if (stopInheritingRoles) {
                inheritRoles = false;
            }

        }

        // If User != anonymous he is member of the predefined group
        // "authenticated"
        if (!WGDatabase.ANONYMOUS_USER.equals(authSession.getDistinguishedName())) {
            WGACLEntry authenticatedEntry = acl.getEntry(AUTHENTICATED_GROUP);
            if (authenticatedEntry != null) {
                allMatchingEntries.add(authenticatedEntry.getName());
                WGACLEntryFlags parsedFlags = acl.parseFlags(authenticatedEntry);

                if (inheritRoles) {
                    roles.addAll(parsedFlags.getRoles());
                }

                if (bestMatchingEntries.size() == 0) {
                    bestMatchingEntries.add(authenticatedEntry.getName());
                }

                if (parsedFlags.isNoRoleInheritance()) {
                    inheritRoles = false;
                }
            }
        }

        // Try to find default entry.
        WGACLEntry defaultEntry = acl.getEntry("*");
        if (defaultEntry != null) {
            allMatchingEntries.add(defaultEntry.getName());
            if (inheritRoles) {
                roles.addAll(acl.parseFlags(defaultEntry).getRoles());
            }

            if (bestMatchingEntries.size() == 0) {
                bestMatchingEntries.add(defaultEntry.getName());
            }
        }

        // Of all best matching entries: Find the highest access level, collect all flags (we will only use privileges since roles are collected separately)
        int accessLevel = WGDatabase.ACCESSLEVEL_NOACCESS;
        WGACLEntryFlags flags = null;
        for (String entryName : bestMatchingEntries) {
            WGACLEntry entry = acl.getEntry(entryName);
            if (entry.getLevel() > accessLevel) {
                accessLevel = entry.getLevel();
            }
            WGACLEntryFlags entryFlags = acl.parseFlags(entry);
            if (flags == null) {
                flags = entryFlags;
            } else {
                flags.mergeFlags(entryFlags);
            }
        }

        // Feed cache
        String authSource = "(none)";
        if (_authenticationModule != null) {
            authSource = _authenticationModule.getAuthenticationSource();
        }
        userDetails = new WGUserDetails(accessLevel, authSession.getDistinguishedName(),
                authSession.getMailAddress(), authSource, authSession.getNames(), authSession.getGroups(), roles,
                allMatchingEntries, flags);

        if (authSession instanceof LabeledNamesProvider) {
            userDetails.setLabeledNames(((LabeledNamesProvider) authSession).getLabeledNames());
        }

        if (userCache != null) {
            userCache.put(USERCACHE_USERDETAILS, userDetails);
        }
        return userDetails;

    }

    protected String getUserCacheKey(AuthenticationSession authSession) {

        StringBuffer key = new StringBuffer();

        if (authSession instanceof AuthSessionWithUserCacheQualifier) {
            String cacheQualifier = ((AuthSessionWithUserCacheQualifier) authSession).getCacheQualifier();
            if (cacheQualifier != null) {
                key.append(cacheQualifier).append("/////");
            }
        }

        key.append(authSession.getDistinguishedName());
        return key.toString();

    }

    /**
     * Default implementation for determining user list membership. This may be
     * used by DB-Implementations that return {@link WGUserDetails} to specify
     * detailed user information. This method is not intended for direct usage
     * outside of the WGAPI.
     * 
     * @param userList
     *            A list of user/group/rolenames to determine membership for.
     * @return true, if the current user is member, false if not.
     * @throws WGAPIException
     */
    public boolean defaultIsMemberOfUserList(List userList) throws WGAPIException {

        if (getSessionContext().isMasterSession()) {
            return true;
        }

        WGUserDetails userDetails = getSessionContext().getUserDetails();
        if (userDetails == null) { // Only possible when no authentication configured
            return true;
        }

        return defaultIsMemberOfUserList(userList, userDetails);

    }

    /**
     * Default implementation for determining user list membership against any custom user details object.
     * @param userListOrig A list of user/group/rolenames to determine membership for. 
     * @param userDetails The user details representing the users authorisations
     * @return true, if the current user is member, false if not.
     */
    public static boolean defaultIsMemberOfUserList(List userListOrig, WGUserDetails userDetails) {

        if (userListOrig.size() == 0) {
            return false;
        }

        List userList = WGUtils.toLowerCase(userListOrig);

        if (userList.contains(userDetails.getPrimaryName().toLowerCase())) {
            return true;
        }

        if (WGUtils.containsAny(userList, WGUtils.toLowerCase(userDetails.getAliases())) != null) {
            return true;
        }

        // Test groups
        if (WGUtils.containsAny(userList, WGUtils.toLowerCase(userDetails.getGroups())) != null) {
            return true;
        }

        // Test roles
        if (WGUtils.containsAny(userList, WGUtils.toLowerCase(userDetails.getRoles())) != null) {
            return true;
        }

        // Test predefined groups
        if (!userDetails.getPrimaryName().equalsIgnoreCase(WGDatabase.ANONYMOUS_USER)
                && userList.contains(WGDatabase.AUTHENTICATED_GROUP)) {
            return true;
        }

        // Test access level groups
        for (AccessLevel level : REAL_ACCESSLEVELS.values()) {
            if (userDetails.getAccessLevel() >= level.getCode()) {
                if (userList.contains(level.getGroupName())) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Returns if this database allows modification of design documents for the
     * current session This method returs false if any of the doctypes file
     * container, script or tml module are not modifiable. For getting more
     * detailed information the method {@link #isDoctypeModifiable(int)} should
     * be used.
     */
    public boolean isAllowDesignModification() {

        if (getSessionContext() != null && getSessionContext().isMasterSession()) {
            return true;
        }

        return isDoctypeModifiable(WGDocument.TYPE_FILECONTAINER) || isDoctypeModifiable(WGDocument.TYPE_CSSJS)
                || isDoctypeModifiable(WGDocument.TYPE_TML);

    }

    /**
     * Sets if this database should allow modification of design documents This
     * call equals calling {@link #setDoctypeModifiable(int, boolean)} for the
     * doctypes file container, script and tml module to false.
     */
    public void setAllowDesignModification(boolean allowDesignUpdates) {
        setDoctypeModifiable(WGDocument.TYPE_TML, false);
        setDoctypeModifiable(WGDocument.TYPE_CSSJS, false);
        setDoctypeModifiable(WGDocument.TYPE_FILECONTAINER, false);
    }

    /**
     * Returns the names of all database attributes as a Set
     */
    public Set getAttributeNames() {
        return Collections.unmodifiableSet(customAttributes.keySet());
    }

    /**
     * Returns an external design provider that was set to this database, if
     * available
     */
    public WGDesignProvider getDesignProvider() {
        return designProvider;
    }

    /**
     * Sets an external design provider from which this database should retrieve
     * design documents. Which design document types are provided is up to the
     * provider.
     * 
     * @param designProvider
     *            The design provider to set.
     * @throws LockException
     */
    public void setDesignProvider(WGDesignProvider designProvider) throws WGAPIException {

        if (this.designProvider != null) {
            this.designProvider.removeDesignChangeListener(this);
        }

        this.designProvider = designProvider;

        if (this.designProvider.isNotifying()) {
            this.designProvider.addDesignChangeListener(this);
        }

        if (isConnected()) {
            refresh();
        }
    }

    /**
     * Converts the name of a file to attach to a format that is accepted by the
     * database backend
     * 
     * @param name
     *            file name
     * @return converted file name
     */
    protected String convertFileNameForAttaching(String name) {
        return getCore().convertFileNameForAttaching(name);
    }

    /**
     * Determines if certificate authentication is enabled for this database
     */
    public boolean certAuthEnabled() {

        // Overriding option COPTION_DISABLE_CERTAUTH has priority
        String disableCertAuth = (String) this.getCreationOptions().get(COPTION_DISABLE_CERTAUTH);
        if (disableCertAuth != null && disableCertAuth.equalsIgnoreCase("true")) {
            return false;
        }

        // Faked cert auth
        String fakeCertAuth = (String) this.getCreationOptions().get(COPTION_FAKE_CERTAUTH);
        if ("true".equals(fakeCertAuth)) {
            return true;
        }

        // If auth module is cert auth capable we return its status
        AuthenticationModule authModule = getAuthenticationModule();
        if (authModule instanceof CertAuthCapableAuthModule) {
            CertAuthCapableAuthModule certMod = (CertAuthCapableAuthModule) authModule;
            return certMod.isCertAuthEnabled();
        }

        // No cert auth
        return false;
    }

    /**
     * Sets an option to a map if it is not already set
     * 
     * @param options
     *            the map to use
     * @param key
     *            the key to set to the map
     * @param value
     *            the value to write to the map
     */
    public static void putDefaultOption(Map options, String key, Object value) {

        if (!options.containsKey(key)) {
            options.put(key, value);
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * de.innovationgate.webgate.api.locking.Lockable#lock(de.innovationgate
     * .webgate.api.locking.LockOwner)
     */
    public void lock(LockOwner owner) throws WGAPIException {
        getLockManager().obtainLock(this, owner);
    }

    /**
     * locks the database for the current sessioncontext
     * 
     * @throws WGAPIException
     */
    public void lock() throws WGAPIException {
        WGSessionContext sessionContext = getSessionContext();
        lock((LockOwner) sessionContext);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * de.innovationgate.webgate.api.locking.Lockable#unlock(de.innovationgate
     * .webgate.api.locking.LockOwner)
     */
    public void unlock(LockOwner owner) {
        getLockManager().releaseLock(this, owner);
    }

    /**
     * unlocks the database for the current sessioncontext
     */
    public void unlock() {
        WGSessionContext sessionContext = getSessionContext();
        unlock(sessionContext);
    }

    /*
     * (non-Javadoc)
     * 
     * @seede.innovationgate.webgate.api.locking.Lockable#getLockStatus(de.
     * innovationgate.webgate.api.locking.LockOwner)
     */
    public int getLockStatus(LockOwner owner) throws WGAPIException {
        return getLockManager().getLockStatus(this, owner);
    }

    /**
     * gets the lockstatus for the current sessioncontext
     * 
     * @return A value indication the lock status. See constants on
     *         de.innovationgate.webgate.api.locking.Lock.
     * @throws WGAPIException
     */
    public int getLockStatus() throws WGAPIException {
        return getLockStatus(getSessionContext());
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.innovationgate.webgate.api.locking.Lockable#getParentLockable()
     */
    public Lockable getParentLockable() {
        return null; // db has no parent
    }

    protected LockManager getLockManager() {
        return _lockManager;
    }

    public void designChanged(final WGDesignChangeEvent event) {

        // If this is not yet connected there is no need for cache maintenance
        if (!isConnected()) {
            return;
        }

        WGDesignProvider designProvider = event.getDesignProvider();

        // If the source is a virtual design provider, then
        // cache management will happen automatically by WGDocument.save()
        if (designProvider instanceof WGVirtualDesignProvider) {
            return;
        }

        MasterSessionTask task = new MasterSessionTask(this) {
            protected void exec(WGDatabase db) throws Throwable {
                processChanges(event.getUpdateLogs());
            }
        };

        if (!task.run()) {
            WGFactory.getLogger().error("Exception on design change processing", task.getThrowable());
        }
    }

    /**
     * Queries a user profile database for stored profiles, based on their data.
     * The query syntax is up to the profile database implementation.
     * 
     * @param type The type of the query
     * @param query Query to use for searching user profiles
     * @param params Parameters given to the query
     * @return A list of names of user profiles whose profile data match the query
     * @throws WGQueryException
     */
    public List queryUserProfileNames(String type, String query, Map params) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (params == null) {
            params = new HashMap();
        }

        return getPersonalisationCore().queryUserProfileNames(type, query, params);

    }

    /**
     * Returns the names of all user profiles that are stored in the personalisation database
     * @throws WGNotSupportedException If the database is not personalisation database
     * @throws WGAPIException
     */
    public Iterator getAllUserProfileNames() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return getPersonalisationCore().getAllUserProfileNames();

    }

    /**
     * Queries a user profile database for stored profiles, based on their data.
     * The query syntax is up to the profile database implementation.
     * This variant uses the default query language for profile queries without parameters
     * 
     * @param query Query to use for searching user profiles
     * @return A list of names of user profiles whose profile data match the query
     * @throws WGQueryException
     */

    public List queryUserProfileNames(String query) throws WGAPIException {
        return queryUserProfileNames(null, query, null);
    }

    /**
     * Returns if this database is already connected to its backend. This may be
     * false, when the database was opened with WGFactory.prepareDatabase() and
     * not session has been opened since.
     * 
     */
    public boolean isConnected() {
        return connected;
    }

    /**
     * A metadata module is a special version of a script module that is used to
     * store metadata about the state of the database. It has some special
     * behaviours: - It is never stored or retrieved from a design provider but
     * always from the original db (even if the design provider target hosts
     * metadata modules) - It is not synchronized or migrated along with other
     * data - It is always of code type XML Metadata modules are stored
     * technically as normal script modules with a prefix qualifier, retrieved
     * from constant WGDatabase.METADATA_MODULE_QUALIFIER.
     * 
     * @param name
     *            The name of the metadata module
     * @return A metadata module
     * @throws WGAPIException
     */
    public WGScriptModule getMetadataModule(String name) throws WGAPIException {
        return getScriptModule(WGCSSJSModule.METADATA_MODULE_QUALIFIER + name, WGScriptModule.CODETYPE_XML);
    }

    /**
     * Creates a new metadata module. For details about metadata modules see
     * method getMetadataModule.
     * 
     * @param name
     *            Name of the module to create
     * @return The created module
     * @throws LockException
     * @throws WGCreationException
     * @throws WGAuthorisationException
     */
    public WGCSSJSModule createMetadataModule(String name) throws WGAPIException {
        return createCSSJSModule(WGCSSJSModule.METADATA_MODULE_QUALIFIER + name, WGCSSJSModule.CODETYPE_XML);
    }

    /**
     * Returns the user cache latency in minutes. This is the time after which
     * all user specific caches get discarded automatically to allow changes in
     * the authentication backend to take effect. This setting can be specified
     * via creation option COPTION_USERCACHELATENCY. If it is 0 the user caches
     * are kept eternal unless the database data changes.
     */
    public int getUserCacheLatency() {
        return userCacheLatency;
    }

    /**
     * Clears all user-specific caches. These caches normally contain either
     * cached authentication/authorisation information or links to content
     * documents that these users are allowed to see.
     */
    public void clearUserCaches() {
        userHashMapGroup.clearAllMaps();
    }

    /**
     * Returns the currently configured behaviour of this database regarding the
     * request of nonexistent items
     */
    public NoItemBehaviour getNoItemBehaviour() {
        return noItemBehaviour;
    }

    protected Collator getDefaultLanguageCollator() {
        if (_defaultLanguageCollator == null) {
            try {
                _defaultLanguageCollator = Collator
                        .getInstance(WGLanguage.languageNameToLocale(getDefaultLanguage()));
            } catch (Exception e) {
                WGFactory.getLogger()
                        .error("Error determining default language collator. Using default platform collator", e);
                _defaultLanguageCollator = Collator.getInstance();
            }
        }
        return _defaultLanguageCollator;
    }

    /**
     * Returns if users with reader access to this profile database may create
     * profile documents. This setting is effective when WGA Content Stores are
     * used directly as personalisation databases, where the Content Store ACL
     * also is effective for profile creation (while normal personalisation
     * databases are always opened via master login).
     */
    public boolean isReaderProfileCreation() {
        return readerProfileCreation;
    }

    /**
     * Returns a list of all content keys in the current database. This method
     * may use optimized backend operations for this task, which do not need to
     * traverse through WGAPI documents.
     * 
     * @param includeArchived
     *            Specify true, to also retrieve content keys for archived
     *            contents
     * @return List of content key objects {@link WGContentKey}
     * @throws WGAPIException
     */
    public List getAllContentKeys(boolean includeArchived) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!hasFeature(FEATURE_RETRIEVE_ALL_CONTENTKEYS)) {
            throw new WGNotSupportedException("This operation is not supported by this WGAPI implementation");
        }

        return getCore().getAllContentKeys(includeArchived);

    }

    /**
     * Returns the system time where the last cache maintenance happened
     */
    public Date getLastCacheMaintenance() {
        if (lastCacheMaintenance != null) {
            return lastCacheMaintenance;
        } else {
            return new Date(Long.MIN_VALUE);
        }
    }

    /**
     * Notify database of the beginning of update operations via this
     * connection. This is only neccessary in cluster environments when the
     * underlying database connections are kept "readOnly" until a real update
     * happens.
     */
    public void beginUpdate() throws WGAPIException {
        getCore().beginUpdate();
    }

    /**
     * Returns if backend deletion checks are enabled, see
     * {@link #COPTION_DELETIONCHECK}
     */
    public boolean isDeletionCheck() {
        return deletionCheck;
    }

    public Class getChildNodeType() {
        return DocumentCollectionHierarchy.class;
    }

    public List getChildNodes() throws WGAPIException {
        return _allDocumentsHierarchy.getChildNodes();
    }

    @Override
    public SkippingIterator<PageHierarchyNode> getChildNodeIterator(int pageSize) throws WGAPIException {
        return new SkippingIteratorWrapper<PageHierarchyNode>(_allDocumentsHierarchy.getChildNodes().iterator());
    }

    public String getNodeKey() {
        return "db/" + getDbReference();
    }

    public String getNodeTitle(String language) throws WGAPIException {
        return getTitle();
    }

    public PageHierarchyNode getParentNode() {
        return null;
    }

    /**
     * Returns if project mode is enabled, see {@link #COPTION_PROJECTMODE}
     */
    public boolean isProjectMode() {
        return projectMode;
    }

    /**
     * Registers an action that should be executed when the database is
     * connected. If it already is connected the action is executed at once and
     * the method returns true. If it is not yet connected but only prepared for
     * connecting the method returns false. In that case the action is executed
     * at the time the database will be connected. This action is always
     * executed under master session rights. Exceptions occurring in the action
     * will be logged but never thrown to the outside.
     * 
     * @param action
     *            The action to execute.
     * @return true if the action was executed immediately, false if it is not.
     */
    public synchronized boolean onConnect(DatabaseAction action) {

        if (isConnected()) {
            try {
                action.run(this);
            } catch (Exception e) {
                WGFactory.getLogger().error("Error executing connect action", e);
            }
            return true;
        } else {
            _connectActions.add(action);
            return false;
        }

    }

    /**
     * @deprecated Use {@link #onConnect(DatabaseAction)}
     */
    public synchronized boolean onConnect(ConnectAction action) {
        return onConnect((DatabaseAction) action);
    }

    protected synchronized boolean onOpen(DatabaseAction action) {

        if (isConnected()) {
            try {
                action.run(this);
            } catch (Exception e) {
                WGFactory.getLogger().error("Error executing connect action", e);
            }
            return true;
        } else {
            _openActions.add(action);
            return false;
        }

    }

    /**
     * Returns if automatic approval is enabled, see
     * {@link #COPTION_AUTOAPPROVE}.
     */
    public boolean isAutoApprove() {
        return autoApprove;
    }

    /**
     * Sets if automatic approval is enabled
     */
    public void setAutoApprove(boolean autoApprove) {
        this.autoApprove = autoApprove;
    }

    /**
     * Sets if documents of a given doctype are modifiable for non-master
     * sessions Functionalities that feed the design of a database with external
     * data might want to disable modifiability of those types that are served.
     * This functionality is only effective for design and schema documents,
     * those that are retrievable via
     * {@link #getDesignObject(int, String, String)}.
     * 
     * @param type
     *            The doctype to set. Use WGDocument.TYPE_... constants
     * @param modifiable
     *            Whether to set the doctype modifiable or unmodifiable
     */
    public void setDoctypeModifiable(int type, boolean modifiable) {

        if (modifiable) {
            this.unmodifiableDoctypes.remove(new Integer(type));
        } else {
            this.unmodifiableDoctypes.add(new Integer(type));
        }
    }

    /**
     * Tests if documents of a given doctypes are modifiable for the current
     * session
     * 
     * @param type
     *            The type to test
     * @return true, if docs of that type are modifiable
     */
    public boolean isDoctypeModifiable(int type) {

        // Master session may modify anything
        if (getSessionContext() != null && getSessionContext().isMasterSession()) {
            return true;
        }

        // If the doctype is provided by a design provider, it is not modifiable
        // by users
        WGDesignProvider provider = getDesignProvider();
        if (provider != null) {
            boolean providesType = provider.providesType(type);
            if (providesType == true) {
                return false;
            }
        }

        // Check for manually set unmodifiability
        return !this.unmodifiableDoctypes.contains(new Integer(type));
    }

    /**
     * Returns a content with the given name in any language. Prefers the
     * default language of the database. If no content with that name is found
     * it tries every language defined in the database
     * 
     * @param name
     *            The name to search
     * @return The content if any was found or null
     * @throws WGAPIException
     */
    public WGContent getAnyContentByName(String name) throws WGAPIException {

        // Try default language first
        WGContent content = getContentByName(name);
        if (content != null) {
            return content;
        }

        // Iterate over languages and return the first match
        Iterator langs = getLanguages().values().iterator();
        while (langs.hasNext()) {
            WGLanguage lang = (WGLanguage) langs.next();
            content = getContentByName(name, lang.getName());
            if (content != null) {
                return content;
            }
        }

        return null;

    }

    /**
     * Manually set a default language for this database
     * 
     * @param defaultLanguage
     *            The default language
     */
    public void setDefaultLanguage(String defaultLanguage) {
        _defaultLanguage = defaultLanguage;
        _defaultLanguageCollator = Collator.getInstance(WGLanguage.languageNameToLocale(_defaultLanguage));
    }

    /**
     * Adds a design change listener to this database. If the database uses a
     * design provider it is registered with the provider. Otherwise it is
     * registered as normal WGDatabaseEventListener. Not all design change
     * events will carry information about the document being updated.
     * 
     * @param changeListener
     */
    public void addDesignChangeListener(WGDesignChangeListener changeListener) {
        if (getDesignProvider() != null) {
            getDesignProvider().addDesignChangeListener(changeListener);
        } else {
            this.databaseDesignChangeListeners.add(changeListener);
        }
    }

    /**
     * Removes a design change listener to this database. If the database uses a
     * design provider it is removed from the provider. Otherwise it is removed
     * as normal WGDatabaseEventListener.
     * 
     * @param changeListener
     */
    public void removeDesignChangeListener(WGDesignChangeListener changeListener) {
        if (getDesignProvider() != null) {
            getDesignProvider().removeDesignChangeListener(changeListener);
        } else {
            this.databaseDesignChangeListeners.remove(changeListener);
        }
    }

    private void fireDatabaseDesignChangedEvent(WGDesignChangeEvent e) {

        if (!isSessionOpen()) {
            return;
        }

        if (!getSessionContext().isEventsEnabled()) {
            return;
        }

        if (getDesignProvider() != null) {
            return;
        }

        Iterator listeners = this.databaseDesignChangeListeners.iterator();
        while (listeners.hasNext()) {
            WGDesignChangeListener listener = (WGDesignChangeListener) listeners.next();
            listener.designChanged(e);
        }

    }

    /**
     * Returns the script module of the given name and codetype
     * 
     * @param name
     * @param codetype
     * @throws WGAPIException
     */
    public WGScriptModule getScriptModule(String name, String codetype) throws WGAPIException {
        return getCSSJSModule(name, codetype);
    }

    /**
     * Returns the script module of the given name and any codetype
     * 
     * @param name
     * @throws WGAPIException
     */
    public WGCSSJSModule getScriptModule(String name) throws WGAPIException {
        return getCSSJSModule(name);
    }

    /**
     * Returns the script modules in this database.
     * 
     * @throws WGAPIException
     */
    public List<WGScriptModule> getScriptModules() throws WGAPIException {
        return getCSSJSModules();
    }

    /**
     * Create a new script module. Same as {@link #createCSSJSModule(String, String)} but better named.
     * @param name Name of the script module
     * @param type Code type of the module. Use constants WGScriptModule.CODETYPE_...
     * @return The newly created script module
     * @throws WGAPIException
     */
    public WGScriptModule createScriptModule(String name, String type) throws WGAPIException {
        return (WGScriptModule) createCSSJSModule(name, type);
    }

    protected int getListCacheRebuildThreshold() {
        return listCacheRebuildThreshold;
    }

    /**
     * Returns the operation keys of backend operations that are currently running
     * @return List of {@link WGOperationKey} objects
     */
    public List getCurrentBackendOperations() {

        List currentOps = new ArrayList();
        Iterator ops = operations.values().iterator();
        while (ops.hasNext()) {
            WGOperationKey op = (WGOperationKey) ops.next();
            if (op.isUsed()) {
                currentOps.add(op);
            }
        }
        return currentOps;

    }

    /**
     * Returns a map of operation keys currently or recently in usage
     */
    public Map getOperationKeys() {
        return Collections.unmodifiableMap(this.operations);
    }

    /**
     * Returns the operation keys of backend operations that have run and are not yet cleant up
     * @return List of {@link WGOperationKey} objects
     */
    public List getRecentBackendOperations() {
        List currentOps = new ArrayList(operations.values());
        return currentOps;
    }

    /**
     * Injects an authentication module to the database that should be used to authenticate users.
     * @param authenticationModule The module
     * @throws WGIllegalArgumentException
     */
    public void setAuthenticationModule(AuthenticationModule authenticationModule)
            throws WGIllegalArgumentException {

        closeAuthenticationModule();

        // Unpack the auth module to see its real capabilities
        AuthenticationModule backendModule = authenticationModule;
        while (backendModule instanceof RedirectionAuthModule) {
            backendModule = ((RedirectionAuthModule) backendModule).getBackendModule();
        }

        if (certAuthEnabled() && (!(backendModule instanceof CertAuthCapableAuthModule))) {
            throw new WGIllegalArgumentException(
                    "The given authentication module is not capable for certificate authentication, which is used by database '"
                            + getDbReference() + "':" + authenticationModule.getAuthenticationSource());
        }

        _authenticationModule = authenticationModule;
        _authenticationModule.addAuthenticationSourceListener(this);
    }

    public void authenticationDataChanged() {
        getUserCache().clear();
    }

    /**
     * Returns the version of content store that this database represents, i.e the version of its storage format.
     * The content store version equals the (Open)WGA feature version which introduced this storage format. Returns constants CSVERSION_...
     * @throws WGAPIException
     */
    public double getContentStoreVersion() throws WGAPIException {
        return _csVersion;
    }

    /**
     * Returns the patch level of this content store. The patch level indicates automatic updates to the storage format of the content store which were applied.
     * Generally the patch level complements the content store version, fetchable via {@link #getContentStoreVersion()}, to indicate the feature set of the content store.
     * This is the patch level that is currently active on the connected database. If the backend patch level changed since the last reconnect this will not be reflected. 
     * @throws WGAPIException
     */
    public int getContentStorePatchLevel() throws WGAPIException {
        return _csPatchLevel;
    }

    protected AllDocumentsHierarchy getAllDocumentsHierarchy() {
        return _allDocumentsHierarchy;
    }

    /**
     * Implements the visitor pattern on the page hierarchy. The visitor visits all areas, all of their struct entries and all of their (non-archived) contents.
     * @param visitor
     * @throws WGAPIException
     */
    public void visit(WGPageVisitor visitor) throws WGAPIException {

        Iterator<WGArea> areas = getAreas().values().iterator();
        while (areas.hasNext()) {
            WGArea area = areas.next();
            area.visit(visitor);

        }

    }

    /**
     * Returns the server used to connect to this database
     */
    public WGDatabaseServer getServer() {
        return server;
    }

    /**
     * Completely clears this document of all content related documents, which is all contents, struct entries, areas, content types and languages
     * WARNING (in case this has not yet become clear): This single method call will delete ALL documents of the given types in the database!
     * @throws WGAPIException 
     */
    public void clearContentData() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_MANAGER) {
            throw new WGAuthorisationException("This operation only may be done by a manager",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_MANAGER_LEVEL);
        }

        Iterator areas = getAreas().values().iterator();
        while (areas.hasNext()) {
            WGArea area = (WGArea) areas.next();
            area.remove();
        }

        Iterator cts = getContentTypes().iterator();
        while (cts.hasNext()) {
            WGContentType ct = (WGContentType) cts.next();
            ct.remove();
        }

        Iterator langs = getLanguages().values().iterator();
        while (langs.hasNext()) {
            WGContentType ct = (WGContentType) langs.next();
            ct.remove();
        }

    }

    /**
     * Returns if the database can be accessed by anonymous users
     * @throws WGBackendException
     */
    public boolean isAnonymousAccessible() throws WGAPIException {
        // check if file is anonymous accessible

        WGACL acl = getACL();
        if (acl == null)
            return true;

        boolean isAnonymousAccessible = false;
        // first check anonymous db access
        WGACLEntry anonymousEntry = acl.getEntry(WGDatabase.ANONYMOUS_USER);
        if (anonymousEntry != null && anonymousEntry.getLevel() >= WGDatabase.ACCESSLEVEL_READER) {
            isAnonymousAccessible = true;
        } else if (anonymousEntry == null) {
            WGACLEntry defaultEntry = acl.getEntry("*");
            if (defaultEntry != null && defaultEntry.getLevel() >= WGDatabase.ACCESSLEVEL_READER) {
                isAnonymousAccessible = true;
            }
        }
        return isAnonymousAccessible;
    }

    /**
     * Enforces the given schema definition on the database
     * @param schemaDef
     * @throws WGAPIException
     */
    public void enforceSchema(WGSchemaDefinition schemaDef) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!getSessionContext().isMasterSession()) {
            throw new WGIllegalStateException("Enforcing schema is only possible under master session");
        }

        if (!hasFeature(FEATURE_FULLCONTENTFEATURES)) {
            throw new WGIllegalStateException("Enforcing schema is only possible on full WGA Content Stores");
        }

        this.schemaDefinition = schemaDef;
        boolean languageCreated = false;

        for (WGSchemaDocumentDefinition docDef : this.schemaDefinition.getDocumentDefinitionsCache().values()) {
            if (docDef.isAutoCreate()) {
                WGDocument newDoc = null;
                if (docDef instanceof WGContentTypeDefinition) {
                    WGDocument ct = getContentType(docDef.getName());
                    if (ct == null) {
                        WGFactory.getLogger()
                                .info("Creating content type '" + docDef.getName() + "' from schema definition");
                        newDoc = createContentType(docDef.getName());
                    }
                } else if (docDef instanceof WGAreaDefinition) {
                    WGArea area = getArea(docDef.getName());
                    if (area == null) {
                        WGFactory.getLogger()
                                .info("Creating area '" + docDef.getName() + "' from schema definition");
                        newDoc = createArea(docDef.getName());
                    }
                } else if (docDef instanceof WGLanguageDefinition) {
                    WGLanguage lang = getLanguage(docDef.getName());
                    if (lang == null || lang.isDummy()) {
                        WGFactory.getLogger()
                                .info("Creating language '" + docDef.getName() + "' from schema definition");
                        newDoc = createLanguage(docDef.getName());
                        languageCreated = true;
                    }
                }

                if (newDoc != null) {
                    newDoc.save();
                }
            }
        }

        if (this.schemaDefinition.isCreateDefaultLanguage()) {
            if (getLanguages().size() == 0) {
                WGFactory.getLogger().info("Creating default language from schema definition");
                WGLanguage lang = createLanguage(getDefaultLanguage());
                lang.save();
            }
        }

        // If a language was created and the default language of this db was automatically determined we now redetermine it
        if (languageCreated && _defaultLanguageAutoDetermined) {
            determineDefaultLanguage();
        }

    }

    /**
     * Returns the schema definition in effect on this database
     */
    public WGSchemaDefinition getSchemaDefinition() {
        return schemaDefinition;
    }

    /**
     * Creates a schema definition containing all content types and areas in this database with their current metadata.
     * @throws WGAPIException 
     */
    public WGSchemaDefinition createSchemaDefinition() throws WGAPIException {

        WGSchemaDefinition schemaDef = new WGSchemaDefinition();
        for (WGContentType ct : getContentTypes()) {
            schemaDef.addDocumentDefinition(ct.createSchemaDefinition());
        }
        for (WGArea area : getAreas().values()) {
            schemaDef.addDocumentDefinition(area.createSchemaDefinition());
        }
        for (WGLanguage lang : getLanguages().values()) {
            schemaDef.addDocumentDefinition(lang.createSchemaDefinition());
        }

        return schemaDef;

    }

    /**
     * Lowercases a metadata field value for this database if metadata lowercasing is not disabled
     * @param meta The value to conditionally lowercase
     */
    public String toLowerCaseMeta(String meta) {

        if (!hasFeature(WGDatabase.FEATURE_DISABLE_META_LOWERCASING)) {
            return meta.toLowerCase();
        } else {
            return meta;
        }

    }

    /**
     * Returns if visibility constraints of the hierarchical reader fields on areas and pages are enabled
     */
    public boolean isPageReadersEnabled() {
        return pageReadersEnabled;
    }

    /**
     * Returns a list of readers that automatically have read access to all read-protected documents  
     */
    public List<String> getMandatoryReaders() {
        return _mandatoryReaders;
    }

    /**
     * Returns if a backend service is supported
     * @param serviceName Name of the service
     */
    public boolean isBackendServiceSupported(String serviceName) throws WGAPIException {
        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        return getCore().isBackendServiceSupported(serviceName);
    }

    /**
     * Calls a backend service whose support is optional. Should throw WGNotSupportedException if the backend service is not supported.
     * Calling backend services is only allowed on master sessions.
     * @param serviceName The service name. Use WGDatabase.BACKENDSERVICE_*
     * @param params The matching parameters for the called service
     * @return An optional return value of the service
     */
    public Object callBackendService(String serviceName, Object[] params) throws WGAPIException {
        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (!USER_BACKENDSERVICES.contains(serviceName) && !getSessionContext().isMasterSession()) {
            throw new WGAuthorisationException("This backend service may only get called on master sessions");
        }

        if (params == null) {
            params = new Object[0];
        }

        Object result = getCore().callBackendService(serviceName, params);

        // Subsequent cleanup operations per service
        if (BACKENDSERVICE_CLEAR_CONTENT.equals(serviceName)
                || BACKENDSERVICE_CLEAR_CONTENT_AND_SCHEMA.equals(serviceName)) {
            refresh();
        } else if (BACKENDSERVICE_CLEAR_DATABASE.equals(serviceName)) {
            refresh();
            updateRevision(WGDatabaseRevision.forValue(getCore().getRevision()));
        } else if (BACKENDSERVICE_CLEAR_USERPROFILES.equals(serviceName)) {
        } else if (BACKENDSERVICE_SELECT_PENDINGRELEASE_DOCS.equals(serviceName)) {
        }

        return result;
    }

    /**
     * Fetches all content documents pending release and releases those that are ready to be released
     * @throws WGAPIException 
     */
    public void releasePendingContents() throws WGAPIException {

        if (!isBackendServiceSupported(WGDatabase.BACKENDSERVICE_SELECT_PENDINGRELEASE_DOCS)) {
            return;
        }

        WGAbstractResultSet resultSet = (WGAbstractResultSet) callBackendService(
                WGDatabase.BACKENDSERVICE_SELECT_PENDINGRELEASE_DOCS, null);
        for (WGContent content : resultSet) {
            if (content.getValidFrom() == null) {
                content.release(
                        "Released by pending release publishing task on "
                                + WGUtils.DATEFORMAT_STANDARD.format(new Date()),
                        content.getWorkflow(), "Released");
                content.save();
                WGFactory.getLogger()
                        .info("Releasing content '" + content.getTitle() + "' (" + getDbReference() + "/"
                                + content.getContentKey().toString()
                                + ") which was pending without defined validity period");
            } else if (!content.getValidFrom().after(new Date())) {
                content.release(
                        "Released by pending release publishing task on "
                                + WGUtils.DATEFORMAT_STANDARD.format(new Date()),
                        content.getWorkflow(), "Released");
                content.save();
                WGFactory.getLogger()
                        .info("Releasing content '" + content.getTitle() + "' (" + getDbReference() + "/"
                                + content.getContentKey().toString() + ") because its validity period (from "
                                + (new SimpleDateFormat("dd.MM.yyyy HH:mm")).format(content.getValidFrom())
                                + ") has begun.");
            }
        }

    }

    protected Semaphore getWgOperationKeySemaphore() {
        return _wgOperationKeySemaphore;
    }

    protected boolean isMaintainOperationKeys() {
        return _maintainOperationKeys;
    }

    /**
     * Enforces the given version compliance behaviour on this database
     * @param compliance Compliance string from CSConfig.VERSIONCOMPLIANCE_*
     * @param enforceNoItemBehaviour If the behaviour regarding nonexistent items for the compliance should be enforced. May be false if this was already set by a higher priority configuration.
     */
    public void enforceVersionCompliance(String compliance, boolean enforceNoItemBehaviour) {

        if (enforceNoItemBehaviour) {
            getNoItemBehaviour().compliantTo(compliance);
        }

        _complianceVersion = CSConfig.getComplianceVersion(compliance);

    }

    /**
     * Returns the WGA version whose behaviour is enforced on this database via {@link #enforceVersionCompliance(String, boolean)}
     */
    public Version getComplianceVersion() {
        return _complianceVersion;
    }

    /**
     * Returns a universal ID for the current database.
     * This ID is stored on the data backend as database extension data field and therefor really identifies the backend databases data set rather than the WGAPI database instance.
     * It is initialized on the first connection that is done via WGAPI to this database. Only OpenWGA Content Stores of version 5 or higher own a UUID. On other databases this returns null.
     * @throws WGAPIException 
     */
    public String getUUID() {
        return _uuid;
    }

    /**
     * Returns an WGAPI sequence object for the given sequence name. The sequence does not need to exist yet.
     * This feature is only available on WGA Content Stores of version 5 or higher.
     * @param sequenceName Name of the sequence
     * @throws WGAPIException
     */
    public WGSequence getSequence(String sequenceName) throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGContentStoreVersionException("Sequences", WGDatabase.CSVERSION_WGA5);
        }

        return new WGSequence(this, sequenceName);
    }

    /**
     * Returns the names of sequences that are initialized and/or in use
     * @throws WGAPIException
     */
    public List<String> getUsedSequenceNames() throws WGAPIException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }

        if (getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGContentStoreVersionException("Sequences", WGDatabase.CSVERSION_WGA5);
        }

        return ((WGDatabaseCoreFeatureSequenceProvider) getCore()).getUsedSequenceNames();

    }

    /**
     * checks for backend changes
     * @return revision
     * @throws WGAPIException
     */
    public Comparable catchupBackendChanges() throws WGAPIException {
        if (isDatabaseUpdatedInBackground()) {
            MasterSessionTask masterTask = new MasterSessionTask(this) {
                @Override
                protected void exec(WGDatabase db) throws Throwable {
                    db.getSessionContext().setTask("CatchUp Backend Changes");
                    db.getSessionContext().setBatchProcess(true);

                    // Since we manage the cache here, we need caching although having a batch process
                    db.getSessionContext().setCachingEnabled(true);

                    db.checkDatabaseUpdates(true);
                }
            };
            try {
                masterTask.runWithExceptions();
            } catch (WGAPIException e) {
                throw e;
            } catch (Throwable e) {
                throw new WGAPIException("Error retrieving backend changes", e);
            }
        }
        return getRevision();
    }

    /**
     *Sets annotators which are called when files are attached to this database and is able to annotate the file with additional information
     * @param annotators The annotators
     */
    public void setFileAnnotators(List<WGFileAnnotator> annotators) {
        List<WGFileAnnotator> newAnnotators = new ArrayList(annotators);
        Collections.sort(newAnnotators, new Comparator<WGFileAnnotator>() {

            @Override
            public int compare(WGFileAnnotator o1, WGFileAnnotator o2) {
                return new Integer(o1.getOrderNr()).compareTo(o2.getOrderNr());
            }

        });

        this.fileAnnotators = newAnnotators;
    }

    /**
     * Returns the currently active file annotators on this database
     */
    public List<WGFileAnnotator> getFileAnnotators() {
        return Collections.unmodifiableList(this.fileAnnotators);
    }

    /**
     * Returns the list of content event listeners for this database. This list
     * is the actual event listener list used by this WGDatabase. Code using
     * iterators of this list should synchronize against the list
     */
    public List getContentEventListeners() {
        return contentEventListeners;
    }

    /**
     * Tool method for annotating a metadata object based on the given file, using the default and optionally given additional annotators
     * @param file The file data
     * @param meta The metadata object to annotate
     * @param additionalAnnotators Additional annotators to run.
     * @throws WGAPIException
     */
    public void annotateMetadata(final File file, WGFileMetaData meta, List<WGFileAnnotator> additionalAnnotators)
            throws WGAPIException {

        DataSource ds = new DataSource() {

            @Override
            public String getContentType() {
                return "application/octet-stream";
            }

            @Override
            public InputStream getInputStream() throws IOException {
                return new BufferedInputStream(new FileInputStream(file));
            }

            @Override
            public String getName() {
                return file.getName();
            }

            @Override
            public OutputStream getOutputStream() throws IOException {
                throw new IOException("This data source does not provide an output stream");
            }

        };

        annotateMetadata(ds, meta, additionalAnnotators);
    }

    /**
     * Tool method for annotating a metadata object based on the given file, using the default and optionally given additional annotators
     * @param ds The file data
     * @param meta The metadata object to annotate
     * @param additionalAnnotators Additional annotators to run.
     * @throws WGAPIException
     */
    protected void annotateMetadata(DataSource ds, WGFileMetaData meta, List<WGFileAnnotator> additionalAnnotators)
            throws WGAPIException {
        List<WGFileAnnotator> annotators = new ArrayList<WGFileAnnotator>(getFileAnnotators());
        if (additionalAnnotators != null) {
            annotators.addAll(additionalAnnotators);
        }

        for (WGFileAnnotator annotator : annotators) {
            try {
                annotator.annotateFile(ds, meta);
            } catch (Throwable e) {
                WGFactory.getLogger().error("Exception running annotator " + annotator.getClass().getName()
                        + " on attaching file '" + ds.getName() + "'", e);
            }
        }

    }

    protected WGDocument.Cache getDocumentCache(WGDocument wgDocument) {

        WGDocument.Cache cache = this.masterDocumentCaches.get(wgDocument.getDocumentKeyObj());
        if (cache == null) {
            cache = wgDocument.createDocumentCache();
            this.masterDocumentCaches.put(wgDocument.getDocumentKeyObj(), cache);
        }

        return cache;
    }

    public void maintenance(boolean fullMaintenance) throws WGException {

        getUserHashMapGroup().maintenance(fullMaintenance);

        if (fullMaintenance) {

            masterDocumentCaches.maintenance();

            if (isBackendServiceSupported(WGDatabase.BACKENDSERVICE_DAILY_MAINTENANCE)
                    && WGFactory.getInstance().isDatabaseBackendMaintenanceEnabled()) {
                callBackendService(WGDatabase.BACKENDSERVICE_DAILY_MAINTENANCE, null);
            }
        }

    }

    public PageRightsFilter getPageRightsFilter() {
        return _pageRightsFilter;
    }

    public void setPageRightsFilter(PageRightsFilter pageRightsFilter) throws WGAPIException {
        _pageRightsFilter = pageRightsFilter;
        refresh();
    }

    public void setFileConverter(WGFileConverter conv) {
        this.fileConverter = conv;
    }

    public WGFileConverter getFileConverter() {
        return this.fileConverter;
    }

    public void createPageSequence(WGStructEntry struct, boolean forceCreate)
            throws WGAPIException, InstantiationException, IllegalAccessException {

        if (!isSessionOpen()) {
            throw new WGClosedSessionException();
        }
        if (!(getCore() instanceof WGDatabaseCoreFeaturePageSequences))
            throw new WGNotSupportedException("Page sequences are not supported for this database");

        if (getSessionContext().getAccessLevel() < ACCESSLEVEL_MANAGER) {
            throw new WGAuthorisationException("You are not authorized to create page sequences in this database",
                    WGAuthorisationException.ERRORCODE_OP_NEEDS_AUTHOR_LEVEL);
        }

        ((WGDatabaseCoreFeaturePageSequences) getCore()).createPageSequence(struct.getCore(), forceCreate);
        struct.save();
    }

}