/*
 * Copyright 2000-2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jetspeed.page.impl;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jetspeed.idgenerator.IdGenerator;
import org.apache.jetspeed.om.common.SecuredResource;
import org.apache.jetspeed.om.folder.DocumentSet;
import org.apache.jetspeed.om.folder.Folder;
import org.apache.jetspeed.om.folder.impl.FolderImpl;
import org.apache.jetspeed.om.page.Fragment;
import org.apache.jetspeed.om.page.Link;
import org.apache.jetspeed.om.page.Page;
import org.apache.jetspeed.om.page.Property;
import org.apache.jetspeed.om.page.psml.FragmentImpl;
import org.apache.jetspeed.om.page.psml.PageImpl;
import org.apache.jetspeed.om.page.psml.PropertyImpl;
import org.apache.jetspeed.page.PageManager;
import org.apache.jetspeed.page.document.AbstractNode;
import org.apache.jetspeed.page.document.Node;
import org.apache.jetspeed.page.document.NodeException;
import org.apache.jetspeed.page.document.NodeSet;
import org.apache.jetspeed.page.document.NodeSetImpl;
import org.apache.jetspeed.profiler.ProfileLocator;
import org.apache.jetspeed.profiler.ProfiledPageContext;

/**
 * AbstractPageManagerService
 *
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 * @version $Id: AbstractPageManager.java,v 1.11 2005/01/25 00:56:38 rwatler Exp $
 */
public abstract class AbstractPageManager 
    implements PageManager    
{
    private final static Log log = LogFactory.getLog(AbstractPageManager.class);
    
    protected Class fragmentClass = FragmentImpl.class;
    protected Class pageClass = PageImpl.class;
    protected Class propertyClass = PropertyImpl.class;
    protected IdGenerator generator = null;

    private boolean permissionsEnabled;

    private boolean constraintsEnabled;

    public AbstractPageManager(IdGenerator generator, boolean permissionsEnabled, boolean constraintsEnabled)
    {    
        this.generator = generator;
        this.permissionsEnabled = permissionsEnabled;
        this.constraintsEnabled = constraintsEnabled;
    }
    
    public AbstractPageManager(IdGenerator generator, boolean permissionsEnabled, boolean constraintsEnabled, List modelClasses)
    {
        this.generator = generator;     
        if (modelClasses.size() > 0)
        {
            this.fragmentClass = (Class)modelClasses.get(0);
            if (modelClasses.size() > 1)
            {
                this.pageClass  = (Class)modelClasses.get(1);
                if (modelClasses.size() > 2)
                {
                    this.propertyClass  = (Class)modelClasses.get(2);
                }                
            }
        }                                 
        this.permissionsEnabled = permissionsEnabled;
        this.constraintsEnabled = constraintsEnabled;
    }
    
    /**
     * <p>
     * getPermissionsEnabled
     * </p>
     *
     * @see org.apache.jetspeed.page.PageManager#getPermissionsEnabled()
     * @return
     */
    public boolean getPermissionsEnabled()
    {
        return permissionsEnabled;
    }

    /**
     * <p>
     * getConstraintsEnabled
     * </p>
     *
     * @see org.apache.jetspeed.page.PageManager#getConstraintsEnabled()
     * @return
     */
    public boolean getConstraintsEnabled()
    {
        return constraintsEnabled;
    }

    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManager#newPage()
     */
    public Page newPage()
    {
        Page page = null;
        try
        {
            // factory create the page
            page = (Page)createObject(this.pageClass);            
            page.setId(generator.getNextPeid());
            
            // create the default fragment
            Fragment fragment = (Fragment)createObject(this.fragmentClass);
            fragment.setId(generator.getNextPeid());
            fragment.setType(Fragment.LAYOUT);
            page.setRootFragment(fragment);            
        }
        catch (ClassCastException e)
        {
            String message = "Failed to create page object for " + this.pageClass;
            log.error(message, e);
        }
        return page;        
    }
    
    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManager#newFragment()
     */
    public Fragment newFragment()
    {
        Fragment fragment = null;
        try
        {
            fragment = (Fragment)createObject(this.fragmentClass);
            fragment.setId(generator.getNextPeid());
            fragment.setType(Fragment.LAYOUT);          
            
        }
        catch (ClassCastException e)
        {
            String message = "Failed to create page object for " + this.pageClass;
            log.error(message, e);
            // throw new JetspeedException(message, e);
        }
        return fragment;        
    }
    
    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManager#newProperty()
     */
    public Property newProperty()
    {
        Property property = null;
        try
        {
            property = (Property)createObject(this.propertyClass);
            
        }
        catch (ClassCastException e)
        {
            String message = "Failed to create fragment-property object for " + this.propertyClass;
            log.error(message, e);
            // throw new JetspeedException(message, e);
        }
        return property;        
    }

    public Object createObject(Class classe)
    {
        Object object = null;
        try
        {
            object = classe.newInstance();
        }
        catch (Exception e)
        {
            log.error("Factory failed to create object: " + classe.getName(), e);            
        }
        
        return object;        
    }
    
    protected class CacheablePageContext
    {
        public Page page;
        public Folder folder;
        public NodeSet siblingPages;
        public Folder parentFolder;
        public NodeSet siblingFolders;
        public NodeSet rootLinks;
        public NodeSet documentSets;
        public Map documentSetNames;
        public Map documentSetNodeSets;
        public List profiledFolders;
        public NodeSet nestedDocumentSets;

        public CacheablePageContext(Page page, Folder folder, NodeSet siblingPages, Folder parentFolder, NodeSet siblingFolders, NodeSet rootLinks, NodeSet documentSets, Map documentSetNames, Map documentSetNodeSets, List profiledFolders)
        {
            this.page = page;
            this.folder = folder;
            this.siblingPages = siblingPages;
            this.parentFolder = parentFolder;
            this.siblingFolders = siblingFolders;
            this.profiledFolders = profiledFolders;
            this.rootLinks = rootLinks;
            this.documentSets = documentSets;
            this.documentSetNames = documentSetNames;
            this.documentSetNodeSets = documentSetNodeSets;

            // compute nested document sets nodes set: these
            // do not appear in the document set but are mapped
            // in docuemnt set names/node sets
            if ((this.documentSetNames != null) && (this.documentSets != null) && (this.documentSetNames.size() > this.documentSets.size()))
            {
                Iterator mappedIter = this.documentSetNames.entrySet().iterator();
                while (mappedIter.hasNext())
                {
                    Map.Entry mappedEntry = (Map.Entry)mappedIter.next();
                    DocumentSet documentSet = (DocumentSet)mappedEntry.getKey();
                    if (!this.documentSets.contains(documentSet))
                    {
                        if (this.nestedDocumentSets == null)
                        {
                            this.nestedDocumentSets = new NodeSetImpl(null);
                        }
                        this.nestedDocumentSets.add(documentSet);
                    }
                }
            }

            // debug profiled page context elements
            if (log.isDebugEnabled())
            {
                log.debug("CacheablePageContext(), folder = " + this.folder + ", url = " + this.folder.getUrl());
                log.debug("CacheablePageContext(), page = " + this.page + ", url = " + this.page.getUrl());
                if ((this.siblingPages != null) && !this.siblingPages.isEmpty())
                {
                    Iterator debugIter = this.siblingPages.iterator();
                    while (debugIter.hasNext())
                    {
                        Page debug = (Page) debugIter.next();
                        log.debug("CacheablePageContext(), siblingPage = " + debug + ", url = " + debug.getUrl());
                    }
                }
                else
                    log.debug("CacheablePageContext(), siblingPages = null/empty");
                log.debug("CacheablePageContext(), parentFolder = " + this.parentFolder + ", url = " + ((this.parentFolder != null) ? this.parentFolder.getUrl() : "null"));
                if ((this.siblingFolders != null) && !this.siblingFolders.isEmpty())
                {
                    Iterator debugIter = this.siblingFolders.iterator();
                    while (debugIter.hasNext())
                    {
                        Folder debug = (Folder) debugIter.next();
                        log.debug("CacheablePageContext(), siblingFolder = " + debug + ", url = " + debug.getUrl());
                    }
                }
                else
                    log.debug("CacheablePageContext(), siblingFolders = null/empty");
                if ((this.rootLinks != null) && !this.rootLinks.isEmpty())
                {
                    Iterator debugIter = this.rootLinks.iterator();
                    while (debugIter.hasNext())
                    {
                        Link debug = (Link) debugIter.next();
                        log.debug("CacheablePageContext(), rootLink = " + debug + ", url = " + debug.getUrl());
                    }
                }
                else
                    log.debug("CacheablePageContext(), rootLinks = null/empty");
                if ((this.documentSets != null) && !this.documentSets.isEmpty() &&
                    (this.documentSetNames != null) && !this.documentSetNames.isEmpty() &&
                    (this.documentSetNodeSets != null) && !this.documentSetNodeSets.isEmpty())
                {
                    Iterator debugIter = this.documentSets.iterator();
                    while (debugIter.hasNext())
                    {
                        log.debug("CacheablePageContext(), " + debugDocumentSetMessage((DocumentSet)debugIter.next()));
                    }
                    if ((this.nestedDocumentSets != null) && !this.nestedDocumentSets.isEmpty())
                    {
                        debugIter = this.nestedDocumentSets.iterator();
                        while (debugIter.hasNext())
                        {
                            log.debug("CacheablePageContext(), nested " + debugDocumentSetMessage((DocumentSet)debugIter.next()));
                        }
                    }
                }
                else
                    log.debug("CacheablePageContext(), documentSets/documentSetNames/documentSetNodeSets = null/empty");
                if ((this.profiledFolders != null) && !this.profiledFolders.isEmpty())
                {
                    Iterator debugIter = this.profiledFolders.iterator();
                    while (debugIter.hasNext())
                    {
                        Folder debug = (Folder) debugIter.next();
                        log.debug("CacheablePageContext(), profiledFolder = " + debug + ", url = " + debug.getUrl());
                    }
                }
                else
                    log.debug("CacheablePageContext(), profiledFolders = null/empty");
            }
        }

        private String debugDocumentSetMessage(DocumentSet debug)
        {
            String debugName = (String) this.documentSetNames.get(debug);
            NodeSet debugNodes = (NodeSet) this.documentSetNodeSets.get(debug);
            String debugMessage = "document set " + debug + ", name = " + debugName + ", nodes = {";
            Iterator nodesIter = debugNodes.iterator();
            if (nodesIter.hasNext())
            {
                debugMessage += ((Node) nodesIter.next()).getUrl();
            }
            while (nodesIter.hasNext())
            {
                debugMessage += ", " + ((Node) nodesIter.next()).getUrl();
            }
            debugMessage += "}, url = " + debug.getUrl();
            return debugMessage;
        }
    }

    protected void populateProfiledPageContext(CacheablePageContext cachedPageContext, ProfiledPageContext pageContext)
    {
        // populate supplied page context object while checking
        // page and folder visibility using access permissions
        // and/or constraints; by definition, page visibility implies
        // folder and parent folder visibility
        pageContext.setPage((Page)checkVisibility((AbstractNode)cachedPageContext.page, cachedPageContext.profiledFolders));
        pageContext.setFolder(cachedPageContext.folder);
        pageContext.setSiblingPages(checkVisibility(cachedPageContext.siblingPages, cachedPageContext.profiledFolders));
        pageContext.setParentFolder(cachedPageContext.parentFolder);
        pageContext.setSiblingFolders(checkVisibility(cachedPageContext.siblingFolders, cachedPageContext.profiledFolders));
        pageContext.setRootLinks(checkVisibility(cachedPageContext.rootLinks, cachedPageContext.profiledFolders));
        if (cachedPageContext.documentSets != null)
        {
            Iterator documentSetIter = checkVisibility(cachedPageContext.documentSets, cachedPageContext.profiledFolders).iterator();
            while (documentSetIter.hasNext())
            {
                // populate root document set
                DocumentSet documentSet = (DocumentSet)documentSetIter.next();
                String documentSetName = (String)cachedPageContext.documentSetNames.get(documentSet);
                NodeSet documentSetNodes = checkVisibility((NodeSet)cachedPageContext.documentSetNodeSets.get(documentSet), cachedPageContext.profiledFolders);
                pageContext.setDocumentSet(documentSetName, documentSet, documentSetNodes);
            }
            if (cachedPageContext.nestedDocumentSets != null)
            {
                Iterator nestedDocumentSetIter = checkVisibility(cachedPageContext.nestedDocumentSets, cachedPageContext.profiledFolders).iterator();
                while (nestedDocumentSetIter.hasNext())
                {
                    // populate nested document set
                    DocumentSet documentSet = (DocumentSet)nestedDocumentSetIter.next();
                    String documentSetName = (String)cachedPageContext.documentSetNames.get(documentSet);
                    NodeSet documentSetNodes = checkVisibility((NodeSet)cachedPageContext.documentSetNodeSets.get(documentSet), cachedPageContext.profiledFolders);
                    pageContext.setNestedDocumentSet(documentSetName, documentSet, documentSetNodes);
                }
            }
        }
    }

    protected AbstractNode checkVisibility(AbstractNode node, List profiledFolders) throws SecurityException
    {
        // check access constraints/permissions of node
        // to determine visibility, (hidden status already
        // tested by profiler); throws SecurityExceptiond
        // if view access not granted
        if (node != null)
        {
            if (node instanceof Folder)
            {
                // visibility of folder determined by visibility
                // of pages in folder; accessible pages in subfolders
                // are not checked since navigating into a folder
                // with no visible pages is not permitted; finally,
                // all equivalent profiled folders must be checked
                // if available
                boolean securityException = false;
                try
                {
                    Iterator pagesIter = ((FolderImpl) node).getPages(false).iterator();
                    while (pagesIter.hasNext())
                    {
                        try
                        {
                            AbstractNode page = (AbstractNode)pagesIter.next();
                            if (!page.isHidden())
                            {
                                checkVisibility(page, profiledFolders);
                                return node;
                            }
                        }
                        catch (SecurityException se)
                        {
                            securityException = true;
                        }
                    }
                }
                catch (NodeException ne)
                {
                }
                if (profiledFolders != null)
                {
                    String folderUrl = node.getUrl();
                    Iterator foldersIter = profiledFolders.iterator();
                    while (foldersIter.hasNext())
                    {
                        FolderImpl profiledFolder = (FolderImpl)foldersIter.next();
                        if ((profiledFolder != node) && folderUrl.equals(profiledFolder.getUrl()))
                        {
                            try
                            {
                                Iterator pagesIter = profiledFolder.getPages(false).iterator();
                                while (pagesIter.hasNext())
                                {
                                    try
                                    {
                                        AbstractNode page = (AbstractNode)pagesIter.next();
                                        if (!page.isHidden())
                                        {
                                            checkVisibility(page, profiledFolders);
                                            return node;
                                        }
                                    }
                                    catch (SecurityException se)
                                    {
                                        securityException = true;
                                    }
                                }
                            }
                            catch (NodeException ne)
                            {
                            }
                        }
                    }
                }

                // no visible pages found in folder or equivalent
                // folder: throw security or runtime exception
                if (securityException)
                {
                    throw new SecurityException("AbstractPageManager.checkVisibility(): Access for " + SecuredResource.VIEW_ACTION + " not permitted to any folder page.");
                }
                throw new RuntimeException("AbstractPageManager.checkVisibility(): Empty folders not visible.");
            }
            else
            {
                // simple access test for document
                node.checkAccess(SecuredResource.VIEW_ACTION);
            }
        }
        return node;
    }

    protected NodeSet checkVisibility(NodeSet nodes, List profiledFolders)
    {
        // check access constraints/permissions of nodes
        // to determine visibility; filter nodes accordingly
        if ((nodes != null) && !nodes.isEmpty())
        {
            NodeSetImpl filteredNodes = null;
            Iterator checkAccessIter = nodes.iterator();
            while (checkAccessIter.hasNext())
            {
                AbstractNode node = (AbstractNode)checkAccessIter.next();
                try
                {
                    // check visibility
                    checkVisibility(node, profiledFolders);

                    // add to filteredNodes nodes if copying
                    if (filteredNodes != null)
                    {
                        // permitted, add to filteredNodes nodes
                        filteredNodes.add(node);
                    }
                }
                catch (RuntimeException rte)
                {
                    // create filteredNodes nodes if not already copying
                    if (filteredNodes == null)
                    {
                        // not permitted, copy previously permitted nodes
                        // to new filteredNodes node set with same comparator
                        filteredNodes = new NodeSetImpl(null, ((NodeSetImpl)nodes).getComparator());
                        Iterator copyIter = nodes.iterator();
                        while (copyIter.hasNext())
                        {
                            AbstractNode copyNode = (AbstractNode)copyIter.next();
                            if (copyNode != node)
                            {
                                filteredNodes.add(copyNode);
                            }
                            else
                            {
                                break;
                            }
                        }
                    }
                }
            }

            // return filteredNodes nodes if generated
            if (filteredNodes != null)
            {
                return filteredNodes;
            }
        }
        return nodes;
    }

    protected ProfileLocator selectPageProfileLocator(Map profileLocators)
    {
        // select page profile locator from session/principal profile locators
        return (ProfileLocator) profileLocators.get(ProfileLocator.PAGE_LOCATOR);
    }

    protected List selectAlternatePageProfileLocators(Map profileLocators)
    {
        // select alternate page profile locators from session/principal profile locators
        List locators = new ArrayList(4);
        Iterator locatorsIter = profileLocators.entrySet().iterator();
        while (locatorsIter.hasNext())
        {
            Map.Entry locatorEntry = (Map.Entry) locatorsIter.next();
            if (! ((String) locatorEntry.getKey()).equals(ProfileLocator.PAGE_LOCATOR))
            {
                locators.add(locatorEntry.getValue());
            }
        }
        return locators;
    }

    protected ProfileLocator selectNavigationProfileLocator(String profileLocatorName, Map profileLocators)
    {
        // select navigation profile locator from session/principal profile locators
        ProfileLocator locator = null;
        if (profileLocatorName != null)
        {
            locator = (ProfileLocator) profileLocators.get(profileLocatorName);
        }
        else
        {
            locator = (ProfileLocator) profileLocators.get(ProfileLocator.DOCSET_LOCATOR);
            if (locator == null)
            {
                locator = (ProfileLocator) profileLocators.get(ProfileLocator.PAGE_LOCATOR);
            }
        }
        return locator;
    }
}
