/*
 * Copyright 2000-2001,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.services.psmlmanager;

//Jetspeed stuff
import org.apache.jetspeed.om.profile.ProfileLocator;
import org.apache.jetspeed.om.profile.QueryLocator;
import org.apache.jetspeed.util.FileCopy;
import org.apache.jetspeed.util.DirectoryUtils;
import org.apache.jetspeed.services.Profiler;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.JetspeedSecurity;
import org.apache.jetspeed.services.resources.JetspeedResources;

//Castor defined API
import org.apache.jetspeed.om.profile.Portlets;
import org.apache.jetspeed.om.profile.*;

//turbine stuff
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.servlet.TurbineServlet;
import org.apache.turbine.services.resources.ResourceService;
import org.apache.turbine.services.servlet.ServletService;

import org.apache.jetspeed.om.security.JetspeedUser;
import org.apache.jetspeed.om.security.Role;
import org.apache.jetspeed.om.security.JetspeedRoleFactory;
import org.apache.jetspeed.om.security.Group;
import org.apache.jetspeed.om.security.JetspeedGroupFactory;
import org.apache.jetspeed.om.security.JetspeedUserFactory;

//castor support
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.ValidationException;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

// serialization support
import org.apache.xml.serialize.Serializer;
import org.apache.xml.serialize.XMLSerializer;
import org.apache.xml.serialize.OutputFormat;

//standard java stuff
import java.io.File;
import java.io.Reader;
import java.io.FileReader;
import java.io.Writer;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import javax.servlet.ServletConfig;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.jetspeed.cache.FileCache;
import org.apache.jetspeed.cache.FileCacheEventListener;
import org.apache.jetspeed.cache.FileCacheEntry;


/**
 * This service is responsible for loading and saving PSML documents.
 *
 * @author <a href="mailto:raphael@apache.org">Raphaël Luta</a>
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 * @author <a href="mailto:sgala@apache.org">Santiago Gala</a>
 * @version $Id: CastorPsmlManagerService.java,v 1.44 2004/03/31 00:23:02 jford Exp $
 */
public class CastorPsmlManagerService extends TurbineBaseService
                                      implements FileCacheEventListener,
                                                 PsmlManagerService
{
    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(CastorPsmlManagerService.class.getName());
    
    // resource path constants
    protected static final String PATH_GROUP              = "group";
    protected static final String PATH_ROLE               = "role";
    protected static final String PATH_USER               = "user";

    // configuration keys
    protected final static String CONFIG_ROOT             = "root";
    protected final static String CONFIG_EXT              = "ext";
    protected final static String CONFIG_SCAN_RATE        = "scanRate";
    protected final static String CONFIG_CACHE_SIZE       = "cacheSize";

    // default configuration values
    public final static String DEFAULT_ROOT             = "/WEB-INF/psml";
    public final static String DEFAULT_EXT              = ".psml";

    // default resource
    public final static String DEFAULT_RESOURCE         = "default.psml";

    // the root psml resource directory
    protected String root;
    // base store directory
    protected File rootDir = null;
    // file extension
    protected String ext;

    /** The documents loaded by this manager */
    protected FileCache documents = null;

    /** the output format for pretty printing when saving registries */
    protected OutputFormat format = null;

    /** the base refresh rate for documents */
    protected long scanRate = 1000 * 60; // every minute

    /** the default cache size */
    protected int cacheSize = 100;

    /** the import/export consumer service **/
    protected PsmlManagerService consumer = null;
    protected boolean importFlag = false;

    // castor mapping
    public static final String DEFAULT_MAPPING = "${webappRoot}/WEB-INF/conf/psml-mapping.xml";
    protected String mapFile = null;

    /** the Castor mapping file name */
    protected Mapping mapping = null;

    /** The default encoding used to serialize PSML files to disk */
    protected String defaultEncoding = JetspeedResources.getString(JetspeedResources.CONTENT_ENCODING_KEY, "utf-8");

    /**
     * This is the early initialization method called by the
     * Turbine <code>Service</code> framework
     */
    public void init( ServletConfig conf ) throws InitializationException
    {
        if (getInit())
        {
            return;
        }

        //Ensure that the servlet service is initialized
        TurbineServices.getInstance().initService(ServletService.SERVICE_NAME, conf);

        // get configuration parameters from Jetspeed Resources
        ResourceService serviceConf = ((TurbineServices)TurbineServices.getInstance())
                                                     .getResources(PsmlManagerService.SERVICE_NAME);
        // get the PSML Root Directory
        this.root = serviceConf.getString( CONFIG_ROOT, DEFAULT_ROOT );
        this.rootDir = new File(root);

        //If the rootDir does not exist, treat it as context relative
        if ( !rootDir.exists() )
        {
            try
            {
                this.rootDir = new File(conf.getServletContext().getRealPath(root));
            }
            catch (Exception e)
            {
                // this.rootDir = new File("./webapp" + this.rootDir.toString());
            }
        }
        //If it is still missing, try to create it
        if (!rootDir.exists())
        {
            try
            {
                rootDir.mkdirs();
            }
            catch (Exception e)
            {
            }
        }

        // get default extension
        this.ext = serviceConf.getString( CONFIG_EXT, DEFAULT_EXT );

        // create the serializer output format
        this.format = new OutputFormat();
        format.setIndenting(true);
        format.setIndent(4);
        format.setLineWidth(0);

        // psml castor mapping file
        mapFile = serviceConf.getString("mapping",DEFAULT_MAPPING);
        mapFile = TurbineServlet.getRealPath( mapFile );
        loadMapping();

        this.scanRate = serviceConf.getLong(CONFIG_SCAN_RATE, this.scanRate);
        this.cacheSize= serviceConf.getInt(CONFIG_CACHE_SIZE, this.cacheSize);

        documents = new FileCache(this.scanRate, this.cacheSize);
        documents.addListener(this);
        documents.startFileScanner();


        //Mark that we are done
        setInit(true);

        // Test
        //testCases();

    }


    /** Late init method from Turbine Service model */
    public void init() throws InitializationException
    {
        while( !getInit() )
        {
            //Not yet...
            try
            {
                Thread.sleep( 500 );
            }
            catch (InterruptedException ie )
            {
                logger.error("Exception", ie);
            }
        }
    }


    /**
     * This is the shutdown method called by the
     * Turbine <code>Service</code> framework
     */
    public void shutdown()
    {
        documents.stopFileScanner();
    }

    /**
     * Returns a PSML document of the given name.
     * For this implementation, the name must be the document
     * URL or absolute filepath
     *
     * @deprecated
     * @param name the name of the document to retrieve
     */
    public PSMLDocument getDocument( String name )
    {
        if (name == null)
        {
            String message = "PSMLManager: Must specify a name";
            logger.error( message );
            throw new IllegalArgumentException( message );
        }

        if (logger.isDebugEnabled())
        {
            logger.debug( "PSMLManager: asked for " + name );
        }

        PSMLDocument doc = null;

        doc = (PSMLDocument)documents.getDocument(name);

        if (doc == null)
        {
            doc = loadDocument(name);

            synchronized (documents)
            {
                // store the document in the hash and reference it to the watcher
                try
                {
                    documents.put(name, doc);
                }
                catch (java.io.IOException e)
                {
                    logger.error("Error putting document", e);
                }
            }
        }

        return doc;
    }

    /**
     * Returns a cached PSML document for the given locator
     * 
     * @param locator The locator descriptor of the document to be retrieved.
     * @return PSML document  from cache (or disk if not yet cached)
     */
    public PSMLDocument getDocument( ProfileLocator locator)
    {
        return getDocument(locator, true);
    }

    /**
     * Returns a PSML document for the given locator
     * 
     * @param locator   The locator descriptor of the document to be retrieved.
     * @param getCached Look in the cache (true) or umarshall a fresh copy from disk (false)
     * @return 
     */
    protected PSMLDocument getDocument( ProfileLocator locator, boolean getCached )
    {
        if (locator == null)
        {
            String message = "PSMLManager: Must specify a name";
            logger.error( message );
            throw new IllegalArgumentException( message );
        }
        File base = this.rootDir;
        String path = mapLocatorToFile(locator);
        File file = new File(base, path);
        String name = null;

        try
        {
            name = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }

        if (logger.isDebugEnabled())
        {
            logger.debug("PSMLManager: calculated resource:" + path + ". Base: " + base + " File: " + name);
        }

        PSMLDocument doc = null;
        Profile profile = null;

        if (getCached == true)
        {
            profile = (Profile)documents.getDocument(name);
        }

        if (profile == null)
        {
            doc = loadDocument(name);
            if (null == doc)
            {
                if (logger.isWarnEnabled())
                {
                    logger.warn( "PSMLManager: " + name + " not found, returning null document" );
                }
                return null;
            }

            synchronized (documents)
            {
                // store the document in the hash and reference it to the watcher
                Profile newProfile = createProfile(locator);
                newProfile.setDocument(doc);
                try
                {
                    documents.put(name, newProfile);
                }
                catch (IOException e)
                {
                    logger.error("Error putting document", e);
                }
            }
        }
        else
        {
            doc = profile.getDocument();
        }

        return doc;
    }

    /**
     * Loads a PSML document from disk bypassing the cache
     * 
     * @param locator
     * @return PSML document from disk
     */
    public PSMLDocument refresh(ProfileLocator locator)
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("CastorPsmlManagerService: psml document refreshed from disk: " + locator.getPath());
        }
        return getDocument(locator, false);
    }

    /**
     * Load a PSMLDOcument from disk
     *
     * @param fileOrUrl a String representing either an absolute URL or an
     * absolute filepath
     */
    protected PSMLDocument loadDocument(String fileOrUrl)
    {
        PSMLDocument doc = null;

        if (fileOrUrl!=null)
        {
            if (!fileOrUrl.endsWith(DEFAULT_EXT))
            {
                fileOrUrl = fileOrUrl.concat(DEFAULT_EXT);
            }

            // load the document and add it to the watcher
            // we'll assume the name is the the location of the file

            File f = getFile(fileOrUrl);
            if (null == f)
                return null;

            doc = new BasePSMLDocument();
            doc.setName(fileOrUrl);

            // now that we have a file reference, try to load the serialized PSML
            Portlets portlets = null;
            try
            {
                DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = dbfactory.newDocumentBuilder();

                Document d = builder.parse(f);

                Unmarshaller unmarshaller = new Unmarshaller(this.mapping);
                portlets = (Portlets)unmarshaller.unmarshal((Node) d);

                doc.setPortlets(portlets);

            }
            catch (IOException e)
            {
                logger.error("PSMLManager: Could not load the file "+f.getAbsolutePath(), e);
                doc = null;
            }
            catch (MarshalException e)
            {
                logger.error("PSMLManager: Could not unmarshal the file "+f.getAbsolutePath(), e);
                doc = null;
            }
            catch (MappingException e)
            {
                logger.error("PSMLManager: Could not unmarshal the file "+f.getAbsolutePath(), e);
                doc = null;
            }
            catch (ValidationException e)
            {
                logger.error("PSMLManager: document "+f.getAbsolutePath()+" is not valid", e);
                doc = null;
            }
            catch (ParserConfigurationException e)
            {
                logger.error("PSMLManager: Could not load the file "+f.getAbsolutePath(), e);
                doc = null;
            }
            catch (SAXException e)
            {
                logger.error("PSMLManager: Could not load the file "+f.getAbsolutePath(), e);
                doc = null;
            }
        }

        return doc;
    }

    /** Store the PSML document on disk, using its locator
     *
     * @param profile the profile locator description.
     * @return true if the operation succeeded
     */
    public boolean store(Profile profile)
    {
        PSMLDocument doc = profile.getDocument();

        File base = this.rootDir;
        String path = mapLocatorToFile(profile);

        File file = new File(base, path);
        String fullpath = null;

        try
        {
            fullpath = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }

        boolean ok = saveDocument(fullpath, doc);

        // update it in cache
        synchronized (documents)
        {
            try
            {
                documents.put(fullpath, profile);
            }
            catch (IOException e)
            {
                logger.error("Error storing document", e);
            }
        }

        return ok;
    }

    /** Save the PSML document on disk, using its name as filepath
     * @deprecated
     * @param doc the document to save
     */
    public boolean saveDocument(PSMLDocument doc)
    {
        return saveDocument(doc.getName(), doc);
    }

    /** Save the PSML document on disk to the specififed fileOrUrl
     *
     * @param fileOrUrl a String representing either an absolute URL
     * or an absolute filepath
     * @param doc the document to save
     */
    public boolean saveDocument(String fileOrUrl, PSMLDocument doc)
    {
        boolean success = false;

        if (doc == null) return false;
        File f = getFile(fileOrUrl);
        if (f == null)
        {
            f = new File(fileOrUrl);
        }

        OutputStreamWriter writer = null;
        FileOutputStream fos = null;
        try
        {
            String encoding = this.defaultEncoding;
            fos = new FileOutputStream(f);
            writer = new OutputStreamWriter(fos, encoding);

            save(writer, doc.getPortlets());
            success = true;
        }
        catch (MarshalException e)
        {
            logger.error("PSMLManager: Could not marshal the file "+f.getAbsolutePath(), e);
        }
        catch (MappingException e)
        {
            logger.error("PSMLManager: Could not marshal the file "+f.getAbsolutePath(), e);
        }
        catch (ValidationException e)
        {
            logger.error("PSMLManager: document "+f.getAbsolutePath()+" is not valid", e);
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: Could not save the file "+f.getAbsolutePath(), e);
        }
        catch (Exception e)
        {
            logger.error("PSMLManager: Error while saving  "+f.getAbsolutePath(), e);
        }
        finally
        {
            try { writer.close(); } catch (IOException e) {}
            try { if(fos != null) { fos.close(); } } catch (IOException e) {}
        }

        return success;
    }

    /** Deserializes a PSML structure read from the reader using Castor
     *  XML unmarshaller
     *
     * @param reader the reader to load the PSML from
     * @param the loaded portlets structure or null
     */
    protected Portlets load(Reader reader)
        throws IOException, MarshalException, ValidationException, MappingException
    {
        Unmarshaller unmarshaller = new Unmarshaller(this.mapping);
        Portlets portlets = (Portlets)unmarshaller.unmarshal(reader);
        return portlets;
    }

    protected void loadMapping()
        throws InitializationException
    {
        // test the mapping file and create the mapping object

        if (mapFile != null)
        {
            File map = new File(mapFile);
            if (logger.isDebugEnabled())
            {
                logger.debug("PSMLManager: Loading psml mapping file "+mapFile);
            }
            if (map.exists() && map.isFile() && map.canRead())
            {
                try
                {
                    mapping = new Mapping();
                    InputSource is = new InputSource( new FileReader(map) );
                    is.setSystemId( mapFile );
                    mapping.loadMapping( is );
                }
                catch (Exception e)
                {
                    logger.error("PSMLManager: Error in psml mapping creation", e);
                    throw new InitializationException("Error in mapping",e);
                }
            }
            else
            {
                throw new InitializationException("PSML Mapping not found or not a file or unreadable: "+mapFile);
            }
        }
    }

    /** Serializes a PSML structure using the specified writer with Castor
     *  XML marshaller and a Xerces serializer for pretty printing
     *
     * @param writer the writer to use for serialization
     * @param portlets the structure to save
     */
    protected void save(Writer writer, Portlets portlets)
        throws IOException, MarshalException, ValidationException, MappingException
    {
        String encoding = this.defaultEncoding;

        if (portlets != null)
        {
            format.setEncoding(encoding);
            Serializer serializer = new XMLSerializer(writer, format);
            Marshaller marshaller = new Marshaller(serializer.asDocumentHandler());
            marshaller.setMapping(this.mapping);
            marshaller.marshal(portlets);
        }
    }

    /** Tests wether the passed argument is an URL string or a file name
     *  and returns the corresponding file object, using diskcache for
     *  remote URLs
     *
     *  @param fileOrUrl the URL string or file path
     *  @return a File object. This file may not exist on disk.
     */
    protected File getFile(String fileOrUrl)
    {
        File f = null;

        f = new File(fileOrUrl);

        if (f.exists())
        {
            return f;
        }

        return null;
    }

    /** Create a new document.
     *
     * @param profile The description and default value for the new document.
     * @return The newly created document;
     */
    public PSMLDocument createDocument( Profile profile )
    {
        File base = this.rootDir;
        String path = mapLocatorToFile((ProfileLocator)profile);

        if (logger.isDebugEnabled())
        {
            logger.debug("PSMLManager: Create document for profile " + profile +", calculated path: " + path);
        }

        File file = new File(base, path);
        String name = null;

        try
        {
            name = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }

        PSMLDocument template = profile.getDocument();
        PSMLDocument doc = new BasePSMLDocument( name, template.getPortlets() );
        try
        {
            String parent = file.getParent();
            File filePath = new File(parent);
            filePath.mkdirs();
            if (template.getName() != null)
            {
                try
                {
                    File source = new File(template.getName());
                    if (source.exists())
                    {
                        FileCopy.copy( template.getName(), name );
                    }
                }
                catch (Exception e)
                {}
            }
            else
            {
                doc.setName(name);
            }
            saveDocument(doc);
        }
        catch (Exception e)
        {
            logger.error("PSMLManager: Failed to save document: " , e);
        }
        return doc;
    }

    /** Given a ordered list of locators, find the first document matching
     *  a profile locator, starting from the beginning of the list and working
     *  to the end.
     *
     * @param locator The ordered list of profile locators.
     */
    public PSMLDocument getDocument( List locators )
    {
        PSMLDocument doc=null;

        Iterator i = locators.iterator();
        while ((doc==null)&&(i.hasNext()))
        {
            doc=getDocument((ProfileLocator)i.next());
        }

        return doc;
    }

    /** Removes a document.
     *
     * @param locator The description of the profile resource to be removed.
     */
    public void removeDocument( ProfileLocator locator )
    {
        // remove a single document
        String fileName = mapLocatorToFile(locator);

        File base = this.rootDir;
        File file = new File(base, fileName);
        String name = null;

        try
        {
            name = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }


        synchronized (documents)
        {
            documents.remove(name);
        }

        file.delete();

    }

    /** Removes all documents for a given user.
     *
     * @param user The user object.
     */
    public void removeUserDocuments( JetspeedUser user )
    {
        ProfileLocator locator = Profiler.createLocator();
        locator.setUser(user);
        StringBuffer buffer = new StringBuffer();
        buffer.append(PATH_USER);
        String name = user.getUserName();
        if (null != name && name.length() > 0)
        {
            buffer.append(File.separator)
                .append(name);
        }
        else
            return; // don't delete the entire user directories

        String path = buffer.toString();
        File base = this.rootDir;
        File file = new File(base, path);

        try
        {
            name = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }


        synchronized (documents)
        {
            DirectoryUtils.rmdir(name);
            Iterator it = documents.getIterator();
            while (it.hasNext())
            {
                FileCacheEntry entry = (FileCacheEntry)it.next();
                if (null == entry)
                {
                    continue;
                }
                Profile profile = (Profile)entry.getDocument();
                if (null == profile)
                {
                    continue;
                }
                JetspeedUser pUser = profile.getUser();
                if (null != pUser && pUser.getUserName().equals(user.getUserName()))
                {
                    documents.remove(profile.getDocument().getName());
                }
            }
        }

    }

    /** Removes all documents for a given role.
     *
     * @param role The role object.
     */
    public void removeRoleDocuments( Role role )
    {
        ProfileLocator locator = Profiler.createLocator();
        locator.setRole(role);
        StringBuffer buffer = new StringBuffer();
        buffer.append(PATH_ROLE);
        String name = role.getName();
        if (null != name && name.length() > 0)
        {
            buffer.append(File.separator)
                .append(name);
        }
        else
            return; // don't delete the entire role directories

        String path = buffer.toString();
        File base = this.rootDir;
        File file = new File(base, path);

        try
        {
            name = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }


        synchronized (documents)
        {
            DirectoryUtils.rmdir(name);
            Iterator it = documents.getIterator();
            while (it.hasNext())
            {
                FileCacheEntry entry = (FileCacheEntry)it.next();
                if (null == entry)
                {
                    continue;
                }
                Profile profile = (Profile)entry.getDocument();
                if (null == profile)
                {
                    continue;
                }
                Role pRole = profile.getRole();
                if (null != pRole && pRole.getName().equals(role.getName()))
                {
                    documents.remove(profile.getDocument().getName());
                }
            }
        }
    }

    /** Removes all documents for a given group.
     *
     * @param group The group object.
     */
    public void removeGroupDocuments( Group group )
    {
        ProfileLocator locator = Profiler.createLocator();
        locator.setGroup(group);
        StringBuffer buffer = new StringBuffer();
        buffer.append(PATH_GROUP);
        String name = group.getName();
        if (null != name && name.length() > 0)
        {
            buffer.append(File.separator)
                .append(name);
        }
        else
            return; // don't delete the entire group directories

        String path = buffer.toString();
        File base = this.rootDir;
        File file = new File(base, path);

        try
        {
            name = file.getCanonicalPath();
        }
        catch (IOException e)
        {
            logger.error("PSMLManager: unable to resolve file path for "+ file);
        }


        synchronized (documents)
        {
            DirectoryUtils.rmdir(name);
            Iterator it = documents.getIterator();
            while (it.hasNext())
            {
                FileCacheEntry entry = (FileCacheEntry)it.next();
                if (null == entry)
                {
                    continue;
                }
                Profile profile = (Profile)entry.getDocument();
                if (null == profile)
                {
                    continue;
                }
                Group pGroup = profile.getGroup();
                if (null != pGroup && pGroup.getName().equals(group.getName()))
                {
                    documents.remove(profile.getDocument().getName());
                }
            }
        }

    }


    /**
     * Maps a ProfileLocator to a file.
     *
     * @param locator The profile locator describing the PSML resource to be found.
     * @return the String path of the file.
     */
    protected String mapLocatorToFile(ProfileLocator locator)
    {
        StringBuffer path = new StringBuffer();

        // move the base dir is either user or role is specified
        Role role = locator.getRole();
        Group group = locator.getGroup();
        JetspeedUser user = locator.getUser();

        if (user != null)
        {
            path.append(PATH_USER);
            String name = user.getUserName();
            if (null != name && name.length() > 0)
            {
                path.append(File.separator)
                    .append(name);
            }
        }
        else if (group != null)
        {
            path.append(PATH_GROUP);
            String name = group.getName();
            if (null != name && name.length() > 0)
            {
                path.append(File.separator)
                    .append(name);
            }
        }
        else if (null != role)
        {
            path.append(PATH_ROLE);
            String name = role.getName();
            if (null != name && name.length() > 0)
            {
                path.append(File.separator)
                    .append(name);
            }
        }

        // Media
        if (null != locator.getMediaType())
        {
            path.append(File.separator)
                .append(locator.getMediaType());
        }
        // Language
        if (null != locator.getLanguage() && (! locator.getLanguage().equals("-1")))
        {
            path.append(File.separator)
                .append(locator.getLanguage());
        }
        // Country
        if (null != locator.getCountry() && (! locator.getCountry().equals("-1")))
        {
            path.append(File.separator)
                .append(locator.getCountry());
        }
        // Resource Name
        if (null != locator.getName())
        {
            if (!(locator.getName().endsWith(CastorPsmlManagerService.DEFAULT_EXT)))
            {
                path.append(File.separator)
                    .append(locator.getName()).append(CastorPsmlManagerService.DEFAULT_EXT);
            }
            else
            {
                path.append(File.separator)
                    .append(locator.getName());
            }
        }
        else
        {
            path.append(File.separator)
                .append(DEFAULT_RESOURCE);
        }

        return  path.toString();
    }

    protected static int STATE_INIT = 0;
    protected static int STATE_BASE = 1;
    protected static int STATE_NAME = 2;
    protected static int STATE_MEDIA = 3;
    protected static int STATE_LANGUAGE = 4;
    protected static int STATE_COUNTRY = 5;

    /** Query for a collection of profiles given a profile locator criteria.
     *
     * @param locator The profile locator criteria.
     */
    public Iterator query( QueryLocator locator )
    {
        List list = new LinkedList();

        Role role = locator.getRole();
        Group group = locator.getGroup();
        JetspeedUser user = locator.getUser();

        // search thru anonymous directories?
        int qm = locator.getQueryMode();
        if ((qm & QueryLocator.QUERY_USER) == QueryLocator.QUERY_USER)
        {
            Profile profile = createProfile();
            StringBuffer path = new StringBuffer();
            path.append(PATH_USER);
            String name = null;
            int state = STATE_INIT;
            if (null != user)
            {
                name = user.getUserName();
                profile.setUser( user );
                if (null != name)
                {
                    path.append(File.separator).append(name);
                    state = STATE_BASE;
                }
            }
            File base = this.rootDir;
            File file = new File(base, path.toString());
            String absPath = file.getAbsolutePath();
            QueryState qs = new QueryState( QUERY_BY_USER,
                                             profile,
                                             locator,
                                             list,
                                             name,
                                             state);
            subQuery(qs, absPath);
        }
        if ((qm & QueryLocator.QUERY_ROLE) == QueryLocator.QUERY_ROLE)
        {
            Profile profile = createProfile();
            StringBuffer path = new StringBuffer();
            path.append(PATH_ROLE);
            String name = null;
            int state = STATE_INIT;
            if (null != role)
            {
                name = role.getName();
                profile.setRole( role );
                if (null != name)
                {
                    path.append(File.separator).append(name);
                    state = STATE_BASE;
                }
            }
            File base = this.rootDir;
            File file = new File(base, path.toString());
            String absPath = null;

            try
            {
                absPath = file.getCanonicalPath();
            }
            catch (IOException e)
            {
                logger.error("PSMLManager: unable to resolve file path for "+ file);
            }

            QueryState qs = new QueryState( QUERY_BY_ROLE,
                                             profile,
                                             locator,
                                             list,
                                             name,
                                             state);
            subQuery(qs, absPath);
        }
        if ((qm & QueryLocator.QUERY_GROUP) == QueryLocator.QUERY_GROUP)
        {
            Profile profile = createProfile();
            StringBuffer path = new StringBuffer();
            path.append(PATH_GROUP);
            String name = null;
            int state = STATE_INIT;
            if (null != group)
            {
                name = group.getName();
                profile.setGroup( group );
                if (null != name)
                {
                    path.append(File.separator).append(name);
                    state = STATE_BASE;
                }
            }
            File base = this.rootDir;
            File file = new File(base, path.toString());
            String absPath = null;

            try
            {
                absPath = file.getCanonicalPath();
            }
            catch (IOException e)
            {
                logger.error("PSMLManager: unable to resolve file path for "+ file);
            }

            QueryState qs = new QueryState( QUERY_BY_GROUP,
                                             profile,
                                             locator,
                                             list,
                                             name,
                                             state);
            subQuery(qs, absPath);
        }

        return list.iterator();
    }

    /** Create a profile based on import flag.
     *
     */
    protected Profile createProfile()
    {
        if (importFlag)
            return new ImportProfile(this, this.consumer);
        else
            return Profiler.createProfile();
    }

    protected Profile createProfile(ProfileLocator locator)
    {
        if (importFlag)
            return new ImportProfile(this, this.consumer, locator);
        else
            return Profiler.createProfile(locator);
    }

    /** Query for a collection of profiles given a profile locator criteria.
     *  This method should be used when importing or exporting profiles between services.
     *
     * @param locator The profile locator criteria.
     * @return The count of profiles exported.
     */
    public int export(PsmlManagerService consumer, QueryLocator locator)
    {
        importFlag = true;
        Iterator profiles = null;
        int count = 0;
        try
        {
            this.consumer = consumer;
            profiles = query(locator);

            while (profiles.hasNext() )
            {
                Profile profile = (Profile)profiles.next();
                //dumpProfile(profile);
                try
                {
                    consumer.createDocument(profile);
                    count++;
                }
                catch (Exception ex)
                {
                    try
                    {
                        consumer.store(profile);
                        count++;
                    }
                    catch (Exception e)
                    {
                        logger.error("PSMLManager: Failed to export profiles to DB: " + profile, ex );
                    }
                }
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
            logger.error("PSMLManager: Failed to export profiles to DB: " , e );

        }
        finally
        {
            importFlag = false;
        }
        return count;
    }


    /** Query for a collection of profiles given a profile locator criteria.
     *  To specify 'all' - use '*' in the criteria
     *
     * @param locator The profile locator criteria.
     */
    protected void subQuery(QueryState qs, String path)
    {
        File file = new File(path);
        if (file.isFile())
        {
            try
            {
                String filename = file.getName();
                if (!filename.endsWith(this.ext))
                    return;

                Profile clone = (Profile)qs.profile.clone();
                clone.setName(filename);
                qs.list.add( clone );
            }
            catch (Exception e)
            {
                logger.error("PSMLManager: Failed to clone profile: " + path + " : " + e, e);
            }
        }
        else if (file.isDirectory())
        {
            String dirName = file.getName();
            qs.state++;

            // filter out based on name, mediatype, language, country
            if (qs.state == STATE_NAME)
            {
                if (null != qs.name)
                {
                    if (!dirName.equals(qs.name))
                        return;
                }
                try
                {
                    if (QUERY_BY_USER == qs.queryBy)
                    {
                        JetspeedUser user = (JetspeedUser)qs.profile.getUser();
                        if (null == user)
                        {
                            user = JetspeedUserFactory.getInstance();
                            user.setUserName(file.getName());
                            qs.profile.setUser(user);
                            qs.clearName = true;
                        }
                    }
                    else if (QUERY_BY_ROLE == qs.queryBy)
                    {
                        Role role = qs.profile.getRole();
                        if (null == role)
                        {
                            role = JetspeedRoleFactory.getInstance();
                            role.setName(file.getName());
                            qs.profile.setRole(role);
                            qs.clearName = true;
                        }
                    }
                    else if (QUERY_BY_GROUP == qs.queryBy)
                    {
                        Group group = qs.profile.getGroup();
                        if (null == group)
                        {
                            group = JetspeedGroupFactory.getInstance();
                            group.setName(file.getName());
                            qs.profile.setGroup(group);
                            qs.clearName = true;
                        }
                    }
                }
                catch (Exception e)
                {}


            }
            else if (qs.state == STATE_MEDIA)
            {
                String media = qs.locator.getMediaType();
                if (null != media)
                {
                    if (!dirName.equals(media))
                        return;
                }
                else
                {
                    qs.profile.setMediaType(dirName);
                    qs.clearMedia = true;
                }
            }
            else if (qs.state == STATE_LANGUAGE)
            {
                String language = qs.locator.getLanguage();
                if (null != language)
                {
                    if (!dirName.equals(language))
                        return;
                }
                else
                {
                    qs.profile.setLanguage(dirName);
                    qs.clearLanguage = true;
                }
            }
            else if (qs.state == STATE_COUNTRY)
            {
                String country = qs.locator.getCountry();
                if (null != country)
                {
                    if (!dirName.equals(country))
                        return;
                }
                else
                {
                    qs.profile.setCountry(dirName);
                    qs.clearCountry = true;
                }
            }

            if (!path.endsWith(File.separator))
                path += File.separator;

            String files[] = file.list();


            // Process all files recursivly
            for(int ix = 0; files != null && ix < files.length; ix++)
            {
                subQuery(qs, path + files[ix]);
            }

            // clear state
            if (qs.state == STATE_NAME && true == qs.clearName)
            {
                if (QUERY_BY_USER == qs.queryBy)
                    qs.profile.setUser(null);
                else if (QUERY_BY_ROLE == qs.queryBy)
                    qs.profile.setRole(null);
                else if (QUERY_BY_GROUP == qs.queryBy)
                    qs.profile.setGroup(null);
                qs.clearName = false;
            }
            else if (qs.state == STATE_MEDIA && true == qs.clearMedia)
            {
                qs.profile.setMediaType(null);
                qs.clearMedia = false;
            }
            else if (qs.state == STATE_LANGUAGE && true == qs.clearLanguage)
            {
                qs.profile.setLanguage(null);
                qs.clearLanguage = false;
            }
            else if (qs.state == STATE_COUNTRY && true == qs.clearCountry)
            {
                qs.profile.setCountry(null);
                qs.clearCountry = false;
            }

            qs.state--;

        }

    }

     static int QUERY_BY_USER = 0;
     static int QUERY_BY_ROLE = 1;
     static int QUERY_BY_GROUP = 2;

    protected class QueryState
    {

        QueryState( int queryBy,
                    Profile profile,
                    ProfileLocator locator,
                    List list,
                    String name,
                    int state)
        {
            this.queryBy = queryBy;
            this.profile = profile;
            this.locator = locator;
            this.list = list;
            this.name = name;
            this.state = state;
        }

        protected int queryBy;
        protected Profile profile;
        protected ProfileLocator locator;
        protected List list;
        protected String name;
        protected int state;

        protected boolean clearName = false;
        protected boolean clearMedia = false;
        protected boolean clearLanguage = false;
        protected boolean clearCountry = false;

    }

    protected void testCases()
    {
        try
        {
            QueryLocator locator = new QueryLocator( QueryLocator.QUERY_USER );
            Iterator x1 = query( locator );
            dump( x1 );

            QueryLocator locator2 = new QueryLocator( QueryLocator.QUERY_USER );
            locator2.setUser( JetspeedSecurity.getUser("turbine") );
            Iterator x2 = query( locator2 );
            dump( x2 );


            QueryLocator locator4 = new QueryLocator( QueryLocator.QUERY_GROUP );
//            locator4.setGroup( JetspeedSecurity.getGroup("apache") );
            Iterator x4 = query( locator4 );
            dump( x4 );
          }
        catch (Exception e)
        {
            System.out.println( "Exception in Debug:" + e);
        }
    }

    protected void dump( Iterator it )
    {
        System.out.println("===============================================");
        while (it.hasNext() )
        {
            Profile profile = (Profile)it.next();
            dumpProfile(profile);
        }
        System.out.println("===============================================");
    }

    protected void dumpProfile(Profile profile)
    {
        JetspeedUser user = profile.getUser();
        Group group = profile.getGroup();
        Role role = profile.getRole();
        if (profile.getAnonymous() == true)
            System.out.println("ANON USER");
        System.out.println("RESOURCE = " + profile.getName());
        if (null != user)
            System.out.println("USER = " + user.getUserName() );
        if (null != group)
            System.out.println("GROUP = " + group.getName() );
        if (null != role)
            System.out.println("ROLE = " + role.getName() );
        System.out.println("MEDIA TYPE = " + profile.getMediaType());
        System.out.println("LANGUAGE = " + profile.getLanguage());
        System.out.println("COUNTRY = " + profile.getCountry());
        PSMLDocument doc = profile.getDocument();
        if (null == doc)
            System.out.println("Document is null");
        else
        {
            if (null == profile.getName())
                System.out.println("profile name is null");
            else
                System.out.println("Doc.name=" + profile.getName());
        }

        System.out.println("----------------------");
    }

    /**
     * Refresh event, called when the entry is being refreshed from file system.
     *
     * @param entry the entry being refreshed.
     */
    public void refresh(FileCacheEntry entry)
    {
        if (logger.isInfoEnabled())
        {
            logger.info("CastorPsmlManager: Entry is refreshing: " + entry.getFile().getPath());
        }

        Profile profile = (Profile) entry.getDocument();
        String path = null;

        if (profile != null)
        {
            try
            {
                path = entry.getFile().getCanonicalPath();
                profile.setDocument(loadDocument(path));
            }
            catch(java.io.IOException e)
            {
                logger.error("CastorPsmlManager: Failed to refresh document "+path);
            }
        }
    }

    /**
     * Evict event, called when the entry is being evicted out of the cache
     *
     * @param entry the entry being refreshed.
     */
    public void evict(FileCacheEntry entry)
    {
        System.out.println("entry is evicting: " + entry.getFile().getName());
    }

}

