Package org.jdesktop.wonderland.modules.avatarbase.client.imi

Source Code of org.jdesktop.wonderland.modules.avatarbase.client.imi.ImiAvatarConfigManager$ImiAvatarConfigHolder

/**
* Project Wonderland
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied
* this code.
*/
package org.jdesktop.wonderland.modules.avatarbase.client.imi;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.bind.JAXBException;
import org.jdesktop.wonderland.client.comms.SessionStatusListener;
import org.jdesktop.wonderland.client.comms.WonderlandSession;
import org.jdesktop.wonderland.client.comms.WonderlandSession.Status;
import org.jdesktop.wonderland.client.login.ServerSessionManager;
import org.jdesktop.wonderland.common.ThreadManager;
import org.jdesktop.wonderland.modules.avatarbase.client.AvatarSessionLoader;
import org.jdesktop.wonderland.modules.avatarbase.client.jme.cellrenderer.WlAvatarCharacter;
import org.jdesktop.wonderland.modules.avatarbase.client.registry.AvatarRegistry;
import org.jdesktop.wonderland.modules.contentrepo.common.ContentCollection;
import org.jdesktop.wonderland.modules.contentrepo.common.ContentNode;
import org.jdesktop.wonderland.modules.contentrepo.common.ContentNode.Type;
import org.jdesktop.wonderland.modules.contentrepo.common.ContentRepositoryException;
import org.jdesktop.wonderland.modules.contentrepo.common.ContentResource;

/**
* Manages the various IMI avatar configurations a user may have.
*
* The local avatar (ie on local disk) are the ones presented to the user
* for selection.
*
* Each avatar file is versioned in the filename using the _[number] postfix
*
* When the user connects to a server the system will ensure that the latest
* versions of avatars are uploaded to the server. It will also download any
* new files from the server.
*
* @author paulby
* @author Jordan Slott <jslott@dev.java.net>
*/
public class ImiAvatarConfigManager {

    private static final Logger logger = Logger.getLogger(ImiAvatarConfigManager.class.getName());

    // A map of current server sessions and that the threads that manage the
    // communcations with each.
    private final Map<ServerSessionManager, ServerSyncThread> avatarConfigServers = new HashMap();

    // A map of local avatar names and the poiners to their configurations that
    // are stored locally on the user's repository.
    private final Map<String, ImiAvatar> localAvatars = new HashMap();

    // Returns the base directory (content collection) that stores all avatar
    // related configuration information on the user's local server.
    private ContentCollection imiCollection = null;

    /**
     * Default constructor
     */
    private ImiAvatarConfigManager() {
        // Fetch the IMI base content collection for configuration, which is
        // the imi/ directory beneath the avatar base.
        AvatarRegistry registry = AvatarRegistry.getAvatarRegistry();
        ContentCollection bc = registry.getAvatarCollection();
        try {
            imiCollection = (ContentCollection) bc.getChild("imi");
            if (imiCollection == null) {
                imiCollection = (ContentCollection) bc.createChild("imi", Type.COLLECTION);
            }
            logger.info("Using local IMI avatar collection " +
                    imiCollection.getPath());
        } catch (ContentRepositoryException excp) {
            logger.log(Level.WARNING, "Unable to fetch imi/ collection", excp);
            return;
        }

        // Initialize the set of local avatars. We register them with the system
        // but a SYNC should be called before these avatars can be relied upon
        // so that we know we have the most up-to-date state. The SYNC will
        // correct any avatars that we registered with the system here.
        try {
            List<ContentNode> avatarList = imiCollection.getChildren();
            for (ContentNode node : avatarList) {
                if (node instanceof ContentResource) {
                    ImiAvatar avatar = new ImiAvatar((ContentResource) node);
                    localAvatars.put(avatar.getName(), avatar);
                }
            }
        } catch (ContentRepositoryException excp) {
            logger.log(Level.WARNING, "Unable to fetch local avatars from " +
                    "collection " + imiCollection.getPath(), excp);
            return;
        }
    }

    /**
     * Singleton to hold instance of AvatarConfigManager. This holder class is
     * loader on the first execution of AvatarConfigManager.getAvatarConfigManager().
     */
    private static class ImiAvatarConfigHolder {
        private final static ImiAvatarConfigManager manager = new ImiAvatarConfigManager();
    }

    /**
     * Returns a single instance of this class
     * <p>
     * @return Single instance of this class.
     */
    public static final ImiAvatarConfigManager getImiAvatarConfigManager() {
        return ImiAvatarConfigHolder.manager;
    }

    /**
     * Registers the known local avatars with the avatar registry.
     */
    public void registerAvatars() {
        AvatarRegistry registry = AvatarRegistry.getAvatarRegistry();
        synchronized (localAvatars) {
            for (String avatarName : localAvatars.keySet()) {
                ImiAvatar avatar = localAvatars.get(avatarName);
                registry.registerAvatar(avatar, false);
            }
        }
    }

    /**
     * Unregisters the known local avatars with the avatar registry.
     */
    public void unregisterAvatars() {
        AvatarRegistry registry = AvatarRegistry.getAvatarRegistry();
        synchronized (localAvatars) {
            for (String avatarName : localAvatars.keySet()) {
                ImiAvatar avatar = localAvatars.get(avatarName);
                registry.unregisterAvatar(avatar);
            }
        }
    }

    /**
     * Returns the URL representing the configuration file for the given avatar
     * on the given server.
     *
     * @param session The current server session
     * @param avatar The avatar
     */
    public URL getAvatarURL(ServerSessionManager session, ImiAvatar avatar)
            throws InterruptedException {

        // First fetch the thread for the given session and then ask it for
        // the URL.
        ServerSyncThread t = null;
        synchronized (avatarConfigServers) {
            t = avatarConfigServers.get(session);
        }
        return t.getAvatarServerURL(avatar);
    }

    /**
     * Returns true if the given server is managed and at least the initial
     * synchronization has been performed.
     *
     * @param session The server session to check
     * @return True if the server session is ready
     */
    public boolean isServerManaged(ServerSessionManager session) {
        synchronized (avatarConfigServers) {
            return avatarConfigServers.containsKey(session);
        }
    }

    /**
     * Adds a new server session for this manager to track and performs an
     * initial synchronization with the avatar configurations found on that
     * server. This method blocks until the initial synchronization is
     * complete, or has been interrupted.
     *
     * @param session The new session to add
     * @throw InterruptedException If the initialization has been interrupted
     */
    public void addServerAndSync(ServerSessionManager session) throws InterruptedException {
        // We do not wish for multiple calls to this method to happen at once
        // for the same server, but we do not want to synchronize this whole
        // method across 'avatarConfigServers' because that would block activity
        // on other servers. So we just synchronize on 'this' which means only
        // a single addServerAndSync() call can be active at once.
        synchronized (this) {
            logger.warning("Adding server " + session.getServerURL());
           
            // First check to see if the session already exists in the map. If
            // so then just return
            synchronized (avatarConfigServers) {
                if (avatarConfigServers.containsKey(session) == true) {
                    logger.info("Server " + session.getServerURL() +
                            " is already present in the manager.");
                    return;
                }
            }

            // Go ahead and synchronize the avatar's local configuration with
            // the server's. Wait for this to complete so we know we are in
            // a 'ready' state.
            ServerSyncThread t = null;
            try {
                logger.info("Starting sychronization with server " +
                        session.getServerURL());

                t = new ServerSyncThread(session);
                t.scheduleSync(true);

                logger.info("Sychronization with server " +
                        session.getServerURL() + " is done.");
            } catch (ContentRepositoryException excp) {
                logger.log(Level.WARNING, "Unable to create sync thread for " +
                        "server " + session.getServerURL(), excp);
                return;
            }

            // Finally, add this server to the list of servers indicating that
            // it is ready.
            synchronized (avatarConfigServers) {
                avatarConfigServers.put(session, t);
                logger.info("Added server " + session.getServerURL() +
                        " to map of managed sessions.");
            }
        }
    }

    /**
     * Removes the session from being managed. If it is not being managed, this
     * method does nothing.
     *
     * @param session The session to remove.
     */
    public void removeServer(ServerSessionManager session) {
        // Remove the server session. If we find a thread, then stop it and
        // remove it from the map.
        logger.warning("Removing server " + session.getServerURL());
        synchronized (avatarConfigServers) {
            ServerSyncThread t = avatarConfigServers.remove(session);
            logger.warning("Stopping thread " + t);
            if (t != null) {
                t.setConnected(false);
            }
        }
    }

    /**
     * Given the avatar, removes the avatar and from all of the server's we
     * are currently connected to.
     *
     * @param avatar The avatar to remove
     */
    public void deleteAvatar(ImiAvatar avatar) {
        // First remove the avatar from the local list, synchronized around the
        // local this so other threads don't update this list at the same time.
        // Also, iterator through all of the servers we know about and tell
        // them to remove the avatar too.
        synchronized (localAvatars) {
            localAvatars.remove(avatar.getName());

            // Remove the avatar from the system
            AvatarRegistry registry = AvatarRegistry.getAvatarRegistry();
            registry.unregisterAvatar(avatar);

            try {
                // Remove from the user's local repository.
                String fileName = avatar.getResource().getName();
                imiCollection.removeChild(fileName);

                // Schedule asynchronous jobs to remove the avatar configuration
                // file from all of the server's we are connected to.
                // XXX This is not quite correct as it will not remove older
                // versions of a file
                synchronized (avatarConfigServers) {
                    for (ServerSyncThread c : avatarConfigServers.values()) {
                        try {
                            c.scheduleDelete(avatar, false);
                        } catch (InterruptedException excp) {
                            logger.log(Level.WARNING, "Attempt to delete the" +
                                    " avatar " + avatar.getName() + " from " +
                                    "the server " + c.toString() + " was " +
                                    "interrupted.", excp);
                        }
                    }
                }
            } catch (ContentRepositoryException excp) {
                logger.log(Level.WARNING, "Unable to remove avatar", excp);
            }
        }
    }

    /**
     * Save the given avatar. This method saves the configuration to the user's
     * repository on the local machine and synchronizing with all of the
     * server's we are curently connected to. Upon success, the avatar version
     * is increment (if not new) and a pointer to its local configuration file
     * is updated.
     *
     * @param avatar The avatar to save
     * @throw ContentRepositoryException Upon error writing files
     * @throw IOException Upon error writing data
     * @throw JAXBException Upon error serializing to XML
     */
    public void saveAvatar(ImiAvatar avatar)
            throws ContentRepositoryException, IOException, JAXBException {

        logger.info("Saving avatar with name " + avatar.getName() + " to server.");

        // First check to see if an avatar already exists with the same name.
        // We need this to know whether to update an existing one or not.
        String avatarName = avatar.getName();
        ImiAvatar existingAvatar = localAvatars.get(avatarName);
       
        // Delete the existing avatar locally if there is one. We need to do
        // this before touching the 'avatar' object because they may be one
        // in the same.
        if (existingAvatar != null) {
            ContentResource resource = existingAvatar.getResource();
            ContentCollection parent = resource.getParent();
            try {
                parent.removeChild(resource.getName());
            } catch (ContentRepositoryException excp) {
                logger.log(Level.WARNING, "Unable to remove local avatar " +
                        resource.getPath(), excp);
            }
        }

        // For each server we are connected to, delete the old file from the
        // server
        synchronized (avatarConfigServers) {
            logger.info("Attempting to delete avatar to server, number=" +
                avatarConfigServers.size());

            for (ServerSyncThread t : avatarConfigServers.values()) {
                // If there is an old version, we ask the server to delete the
                // file. We wait for its completion. We need to do this first
                // to get rid of the old avatar before we upload the new one.
                if (existingAvatar != null) {
                    try {
                        logger.info("Schedule delete of existing avatar " +
                                "named " + avatarName + " from server.");
                        t.scheduleDelete(existingAvatar, true);
                    }
                    catch (InterruptedException excp) {
                        logger.log(Level.WARNING, "Attempt to delete the" +
                                " avatar " + avatar.getName() + " from " +
                                "the server " + t.toString() + " was " +
                                "interrupted.", excp);
                    }
                }
            }
        }

        if (existingAvatar != null) {
            logger.info("Already have an avatar named " + avatarName +
                    " with version " + existingAvatar.getVersion());

            // If we already have an avatar, check it's version number,
            // increment it and assign the new version number to this file.
            int version = existingAvatar.getVersion();
            avatar.setVersion(version);
            avatar.incrementVersion();
        }

        // Generate the avatar character from the avatar configuration. We will
        // use this to write out its parameters
        WonderlandCharacterParams params = avatar.getAvatarParams(false);
        WlAvatarCharacter character = ImiAvatar.getAvatarCharacter(params);

        // Create a file to hold the avatar configuration locally if it does
        // not yet exist.
        String fileName = avatar.getFilename();
        ContentResource file = (ContentResource) imiCollection.createChild(fileName, Type.RESOURCE);
        if (file == null) {
            file = (ContentResource) imiCollection.createChild(fileName, Type.RESOURCE);
        }

        logger.info("Writing avatar to resource " + file.getPath());

        // Write out the avatar configuration to a byte avatar.
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        character.saveConfiguration(out);
        out.close();

        // Then write out the XML to the local repository. Update the avatar
        // to point to this local avatar resource
        file.put(out.toByteArray());
        avatar.setResource(file);

        // Update the map of local avatars with the new avatar. We always do
        // this no matter whether the avatar is new or whether we are updating
        // an existing avatar.
        synchronized (localAvatars) {
            logger.info("Put avatar named " + avatarName + " in list of avatars");
            localAvatars.put(avatarName, avatar);
        }
       
        // For each server we are connected to, upload the new file.
        synchronized (avatarConfigServers) {
            logger.info("Attempting to upload avatar to server, number=" +
                avatarConfigServers.size());

            for (ServerSyncThread t : avatarConfigServers.values()) {
                // We ask the server to upload the file. We need to wait for
                // it to complete before we tell other clients to use it.
                try {
                    logger.info("Schedule upload of avatar named " + avatarName +
                            " to server " + t.toString());
                    t.scheduleUpload(avatar, true);
                } catch (InterruptedException excp) {
                    logger.log(Level.WARNING, "Attempt to upload the avatar " +
                            avatar.getName() + " to the server " + t.toString() +
                            " was interrupted.", excp);
                }
            }
        }

        // If the avatar is new, tell the system.
        if (existingAvatar == null) {
            logger.info("Registering avater named " + avatarName + " in the system");
            AvatarRegistry registry = AvatarRegistry.getAvatarRegistry();
            registry.registerAvatar(avatar, false);
        }
    }

    /**
     * The ServerSyncThread maintains synchronization of avatar information with
     * a particular server session. It runs its own thread to serialize updates
     * with the server (sync, add, remove, etc).
     */
    private class ServerSyncThread extends Thread {

        // The server we are currently collected to
        private ServerSessionManager manager = null;

        // A queue of jobs to execute on the thread.
        private LinkedBlockingQueue<Job> jobQueue = new LinkedBlockingQueue();

        // The collection where all IMI avatar information is kept on the server
        private ContentCollection serverCollection = null;

        // A map of avatar names to pointers to their configuration files on the
        // server
        private Map<String, ImiServerAvatar> serverAvatars = new HashMap();
       
        // True if we are connected to the server, false if we have disconnected
        private boolean isConnected = true;

        public ServerSyncThread(final ServerSessionManager manager)
                throws ContentRepositoryException {

            super(ThreadManager.getThreadGroup(), "AvatarServerSyncThread");
            this.manager = manager;

            // Fetch the base directory in which all IMI avatar configuration info
            // is found on the server.
            serverCollection = getBaseServerCollection(manager);

            // Finally, start the thread off
            this.start();
        }

        /**
         * Sets whether the session is connect or not. If the session has been
         * disconnected, then this should be set to false. Once the thread has
         * been put into the not connected state, the thread is effectively dead.
         *
         * @param isConnected True if the session is connected, false if not.
         */
        public void setConnected(boolean isConnected) {
            this.isConnected = isConnected;
        }

        /**
         * Returns the URL corresponding to the configuration file on the server
         * of given avatar.
         *
         * @param avatar The local avatar
         * @return The URL of the avatar configuration file on the server
         * @throw InterruptedException If the job has been interrupted
         */
        public URL getAvatarServerURL(ImiAvatar avatar) throws InterruptedException {
            Job job = Job.newGetURLJob(avatar);
            jobQueue.add(job);
            job.waitForJob();
            return job.url;
        }

        /**
         * Schedules an asynchronous job to synchronous the IMI avatar
         * configuration between the client and server. The caller may block
         * until the job is complete, by giving isWait of true.
         *
         * @param isWait True to block until the job has completed
         * @throw InterruptedException If the job has been interrupted
         */
        public void scheduleSync(boolean isWait) throws InterruptedException {
            Job job = Job.newSyncJob();
            jobQueue.add(job);
            if (isWait == true) {
                job.waitForJob();
            }
        }

        /**
         * Schedules an asynchronous job to delete an avatar from the server.
         * The caller may block until the job is complete, by giving isWait of
         * true.
         *
         * @param isWait True to block until the job has completed
         * @throw InterruptedException If the job has been interrupted
         */
        public void scheduleDelete(ImiAvatar avatar, boolean isWait) throws InterruptedException {
            Job job = Job.newDeleteJob(avatar);
            jobQueue.add(job);
            if (isWait == true) {
                job.waitForJob();
            }
        }

        /**
         * Schedules an asynchronous job to upload an avatar to the server.
         * The caller may block until the job is complete, by giving isWait of
         * true.
         *
         * @param isWait True to block until the job has completed
         * @throw InterruptedException If the job has been interrupted
         */
        public void scheduleUpload(ImiAvatar avatar, boolean isWait) throws InterruptedException {
            Job job = Job.newUploadJob(avatar);
            jobQueue.add(job);
            if (isWait == true) {
                job.waitForJob();
            }
        }

        /**
         * Returns the collection at the root of all of the avatar configuration
         * information.
         *
         * @return A ContentCollection of avatar configuration info on the server
         * @throw ContentRepositoryException Upon error finding the collection
         */
        private ContentCollection getBaseServerCollection(ServerSessionManager session)
                throws ContentRepositoryException {

            // Fetch the avatars/imi directory, creating each if necessary
            ContentCollection dir = AvatarSessionLoader.getBaseServerCollection(session);
            ContentCollection imiDir = (ContentCollection) dir.getChild("imi");
            if (imiDir == null) {
                imiDir = (ContentCollection) dir.createChild("imi", Type.COLLECTION);
            }
            return imiDir;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void run() {
            while (isConnected == true) {
                // Fetch the next job. If we are interrupted in doing so, then
                // simply log an error and continue.
                Job job = null;
                try {
                    job = jobQueue.take();
                } catch (InterruptedException excp) {
                    logger.log(Level.WARNING, "Attempt to fetch the next job" +
                            " in queue was interrupted for server " +
                            manager.getServerURL(), excp);
                    continue;
                }

                // Dispatch to the implementation based upon the job type. When
                // done indicate we are done.
                switch (job.jobType) {
                    case SYNC:
                        syncImpl();
                        break;
                    case DELETE:
                        deleteImpl(job.avatar);
                        break;
                    case UPLOAD:
                        uploadFileImpl(job.avatar);
                        break;
                    case GETURL:
                        job.url = getURLImpl(job.avatar);
                        break;
                }
                job.setJobDone();
            }
        }

        /**
         * Synchronous implementation of removing an avatar from the server
         * given the avatar to remove.
         *
         * @param avatar The avatar to remove from the server
         */
        private void deleteImpl(ImiAvatar avatar) {
            // Fetch the name of the avatar and see if an entry exists on the
            // server. If so, then remove it from the server and the map.
            String avatarName = avatar.getName();
            ImiServerAvatar serverAvatar = serverAvatars.get(avatarName);
            if (serverAvatar != null) {
                ContentResource resource = serverAvatar.resource;
                ContentCollection parent = resource.getParent();
                try {
                    parent.removeChild(resource.getName());
                } catch (ContentRepositoryException excp) {
                    logger.log(Level.WARNING, "Unable to delete avatar from" +
                            " server " + avatar, excp);
                }
                serverAvatars.remove(avatarName);
            }
        }

        /**
         * Synchronous implementation of fetching the URL of the server
         * configuration file for the given avatar. Returns the URL.
         */
        private URL getURLImpl(ImiAvatar avatar) {

            // Fetch the server version using the name of the avatar. If it
            // does not exist, log an error and return null
            String avatarName = avatar.getName();
            ImiServerAvatar serverAvatar = serverAvatars.get(avatarName);
            if (serverAvatar == null) {
                logger.severe("No record of avatar " + avatarName +
                        " on server " + manager.getServerURL());
                return null;
            }

            // Otherwise, return its URL or null upon error
            try {
                return serverAvatar.resource.getURL();
            } catch (ContentRepositoryException excp) {
                logger.log(Level.WARNING, "Unable to fetch URL for server" +
                        " avatar " + avatarName + " on server " +
                        manager.getServerURL(), excp);
                return null;
            }
        }

        /**
         * Synchronous implementation of synchronizing the client and server
         * IMI avatar configuration information.
         */
        private void syncImpl() {
            logger.info("Beginning sychronization of server " +
                    manager.getServerURL());

            // Keep arrays of configuration files that need to be uploaded to
            // the server and downloaded from the server.
            List<ImiAvatar> uploadList = new ArrayList();
            List<ImiServerAvatar> downloadList = new ArrayList();

            // Look through the list of avatars on the server. If there was
            // a previous entry in the map (perhaps from a previous SYNC), then
            // remove the old entry. The 'serverAvatars' map contains all of
            // the avatars on the server.
            try {
                List<ContentNode> avatarList = serverCollection.getChildren();
                for (ContentNode node : avatarList) {
                    if (node instanceof ContentResource) {
                        ContentResource resource = (ContentResource)node;
                        ImiServerAvatar serverAvatar = new ImiServerAvatar(resource);
                        String avatarName = serverAvatar.avatarName;

                        logger.info("Looking at server avatar named " +
                                avatarName + " with version " + serverAvatar.version);

                        // Check to see if the server avatar already exists in
                        // the known list of server avatars.
                        ImiServerAvatar previous = serverAvatars.put(avatarName, serverAvatar);
                        if (previous != null && previous.version > serverAvatar.version) {

                            logger.info("Found a previous version named " +
                                    avatarName + " with version " + previous.version);

                            // If we somehow find a more recent avatar in our
                            // list, then we remove the one we just found on
                            // the server. (The most recent one will be added
                            // back to the server later).
                            serverAvatars.put(previous.avatarName, previous);
                            String fileName = serverAvatar.getFilename();
                            serverCollection.removeChild(fileName);
                        }
                    }
                }

                // Make a copy of the map of server avatars. This will serve
                // as a list of all avatars that need to be downloaded from
                // the server.
                Map<String, ImiServerAvatar> tmpServerAvatars = new HashMap(serverAvatars);

                // Loop through all of the avatars we know about locally. See
                // if one by the same name exists on the server. If so, and
                // the server version needs to be updated, then do so. If the
                // local copy needs to be updated then do so.
                synchronized (localAvatars) {

                    logger.info("Taking a look at all of our local avatars");

                    for (ImiAvatar avatar : localAvatars.values()) {
                        String avatarName = avatar.getName();
                        ImiServerAvatar serverVersion = tmpServerAvatars.get(avatarName);

                        logger.info("Looking at local avatar named " +
                                avatarName + " server version " + serverVersion);

                        // If the local avatar is not on the server, or if the
                        // version of the server is older than locally, then
                        // mark this avatar for upload.
                        if (serverVersion == null ||
                                serverVersion.version < avatar.getVersion()) {

                            logger.info("Server version for avatar named " +
                                    avatarName + " does not exist or is older" +
                                    " than version " + avatar.getVersion());

                            uploadList.add(avatar);
                            tmpServerAvatars.remove(avatarName);
                        }
                        else if (serverVersion.version > avatar.getVersion()) {
                            logger.info("Server version for avatar named " +
                                    avatarName + " is more recent than local " +
                                    "with server version " + serverVersion.version +
                                    " and local version " + avatar.getVersion());

                            // Otherwise, if the server has a more recent copy
                            // of the avatar, then mark this avatar for
                            // download.
                            downloadList.add(serverVersion);
                            tmpServerAvatars.remove(avatarName);
                        }
                        else if (serverVersion.version == avatar.getVersion()) {
                            logger.info("Server version for avatar named " +
                                    avatarName + " is same as local version " +
                                    avatar.getVersion());
                           
                            // If the two versions are the same, we just want
                            // to do nothing with it.
                            // XXX Why do we need to re-add it to the server
                            // avatar list? It should already be there.
                            tmpServerAvatars.remove(avatarName);
                            serverAvatars.put(avatarName, serverVersion);
                        }
                    }
                }

                // Avatars left in the serverAvatars map are only on the server,
                // and not on the client (so the previous code block did not
                // see them), so add them to the download list.
                for (ImiServerAvatar serverAvatar : tmpServerAvatars.values()) {
                    logger.info("Adding Server avatar to download list " +
                            serverAvatar.avatarName + " version " +
                            serverAvatar.version);

                    downloadList.add(serverAvatar);
                }


                // For all of the avatar configuration files that we wish to
                // upload, do so synchronously.
                logger.info("Doing upload of local avatars, number to upload " +
                        uploadList.size());

                for (ImiAvatar avatar : uploadList) {
                    logger.info("Uploading Local avatar to server " +
                            avatar.getName() + " version " + avatar.getVersion());

                    uploadFileImpl(avatar);
                }

                // Keep a list around of all of the avatars we have just
                // downloaded, to be added to the set of local avatars later.
                List<ImiAvatar> newAvatarList = new ArrayList();

                // For all of the avatar configuration files that we wish to
                // download, do so synchronously.
                logger.info("Doing download of local avatars to the server," +
                        " number to download " + downloadList.size());

                for (ImiServerAvatar serverAvatar : downloadList) {
                    try {
                        logger.info("Downloading server avatar named " +
                                serverAvatar.avatarName + " to file name " +
                                serverAvatar.resource.getName());

                        // Create an entry for the configuration file locally.
                        // Write the contents of the server version to this file.
                        String fileName = serverAvatar.resource.getName();
                        ContentResource localFile = (ContentResource) imiCollection.createChild(fileName, Type.RESOURCE);
                        InputStream is = serverAvatar.resource.getURL().openStream();
                        localFile.put(new BufferedInputStream(is));

                        // Create a new entry to put on the local list. These
                        // will be added below.
                        ImiAvatar newAvatar = new ImiAvatar(localFile);
                        newAvatarList.add(newAvatar);

                        logger.info("Local avatar created in resource " +
                                localFile.getPath());

                    } catch (IOException excp) {
                        logger.log(Level.WARNING, "Error downloading server " +
                                "avater named " + serverAvatar.avatarName, excp);
                    }
                }

                // For all of the new avatars we just downloaded, upload the
                // list of local avatars. We also fire an event to indicate
                // that a new avatar has been added.
                logger.info("Adding new local avatars to system, " +
                        "number of avatars " + newAvatarList.size());

                synchronized (localAvatars) {
                    for (ImiAvatar newAvatar : newAvatarList) {
                        logger.info("Adding new local avatar to system " +
                                "named " + newAvatar.getName());

                        localAvatars.put(newAvatar.getName(), newAvatar);
                        AvatarRegistry registry = AvatarRegistry.getAvatarRegistry();
                        registry.registerAvatar(newAvatar, false);
                    }
                }
            } catch (ContentRepositoryException excp) {
                logger.log(Level.WARNING, "Error synchronizing with server " +
                        manager.getServerURL(), excp);
            }
        }

        /**
         * Synchronous implementation of uploading an avatar to the server given
         * the avatar.
         *
         * @param avatar The avatar to upload to the server
         */
        private void uploadFileImpl(ImiAvatar avatar) {
            // Fetch the avatar we wish to upload and the resource that corresponds
            // to the local configuration file.
            ContentResource resource = avatar.getResource();
            String fileName = resource.getName();

            try {
                // Create a resource on the server with the same as the local resource.
                // Assume the file does not yet exist on the server. Then go ahead
                // and upload the local file to the server.
                ContentResource file = (ContentResource) serverCollection.createChild(fileName, Type.RESOURCE);
                InputStream is = resource.getURL().openStream();
                file.put(new BufferedInputStream(is));

                // Add an entry to the map of avatars on the server.
                ImiServerAvatar serverAvatar = new ImiServerAvatar(file);
                serverAvatars.put(serverAvatar.avatarName, serverAvatar);
            } catch (java.lang.Exception excp) {
                logger.log(Level.WARNING, "Unable to upload avatar config", excp);
            }
        }

        @Override
        public String toString() {
            return manager.getServerURL();
        }
    }

    /**
     * A static inner class that represents an avatar configuration on the
     * server
     */
    private static class ImiServerAvatar {
        // The content repository resource pointing to the remote config file
        // on the server.
        public ContentResource resource = null;
       
        // The version number of the configuration file
        public int version = 0;

        // The name of the configuration file, stripped of the version number.
        public String avatarName = null;

        private static final String EXTENSION = ".xml";

        /**
         * Constructor, takes the content resource
         * @param resource
         */
        public ImiServerAvatar(ContentResource resource) {
            this.resource = resource;
            String name = resource.getName();
            version = getAvatarVersion(name);
            int i = name.lastIndexOf('_');
            if (i == -1) {
                avatarName = name;
            } else {
                avatarName = name.substring(0, i);
            }
        }

        /**
         * Return the file name corresponding to the avatar's configuration file.
         *
         * @return The configuration file name
         */
        public String getFilename() {
            return avatarName + "_" + version + EXTENSION;
        }

        /**
         * Returns the version number embedded in the configuration file name.
         */
        private int getAvatarVersion(String filename) {
            // The version number is found between the final underscore and
            // the file extension.
            int underscore = filename.lastIndexOf('_');
            int ext = filename.lastIndexOf('.');

            if (underscore == -1 || ext == -1) {
                return -1;
            }
            String verStr = filename.substring(underscore + 1, ext);
            try {
                return Integer.parseInt(verStr);
            } catch (NumberFormatException e) {
                return -1;
            }
        }
    }

    /**
     * A static inner class that represents a 'job' submitted to the 'server
     * sync' thread. Threads may block on the completion of a thread by
     * invoking waitForJob().
     */
    private static class Job {

        public enum JobType {
            SYNC, DELETE, UPLOAD, GETURL
        };

        public JobType jobType;
        public ImiAvatar avatar = null;
        public Semaphore jobDone = null;
        public URL url = null;

        private Job(JobType jobType, ImiAvatar avatar) {
            this.jobType = jobType;
            this.avatar = avatar;
            this.jobDone = new Semaphore(0);
            this.url = null;
        }

        public static Job newSyncJob() {
            return new Job(JobType.SYNC, null);
        }

        public static Job newDeleteJob(ImiAvatar avatar) {
            return new Job(JobType.DELETE, avatar);
        }

        public static Job newUploadJob(ImiAvatar avatar) {
            return new Job(JobType.UPLOAD, avatar);
        }

        public static Job newGetURLJob(ImiAvatar avatar) {
            return new Job(JobType.GETURL, avatar);
        }

        /**
         * Waits for this job to be done.
         * @throw InterruptedException If the job has been interrupted
         */
        public void waitForJob() throws InterruptedException {
            jobDone.acquire();
        }

        /**
         * Used by jobs to indicate they have completed.
         */
        public void setJobDone() {
            jobDone.release();
        }
    }
}
TOP

Related Classes of org.jdesktop.wonderland.modules.avatarbase.client.imi.ImiAvatarConfigManager$ImiAvatarConfigHolder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.