org.frogx.service.core.DefaultMUGService.java Source code

Java tutorial

Introduction

Here is the source code for org.frogx.service.core.DefaultMUGService.java

Source

/**
 * Copyright (C) 2008-2009 Guenther Niess. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.frogx.service.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.frogx.service.api.MUGManager;
import org.frogx.service.api.MUGOccupant;
import org.frogx.service.api.MUGPersistenceProvider;
import org.frogx.service.api.MUGRoom;
import org.frogx.service.api.MUGService;
import org.frogx.service.api.MultiUserGame;
import org.frogx.service.api.exception.ForbiddenException;
import org.frogx.service.api.exception.NotAllowedException;
import org.frogx.service.api.exception.UnsupportedGameException;
import org.frogx.service.core.iq.IQDiscoInfoHandler;
import org.frogx.service.core.iq.IQDiscoItemsHandler;
import org.frogx.service.core.iq.IQSearchHandler;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The first implementation of a  {@see  MUGService} . A Multi-User Gaming service is a XMPP component which  manages game rooms and user sessions.
 * @author  G&uuml;nther Nie&szlig;
 */
public class DefaultMUGService implements MUGService {

    private static final Logger log = LoggerFactory.getLogger(DefaultMUGService.class);

    /**
     * the game service's hostname (subdomain)
     */
    private String serviceName;

    /**
     * the game service's description
     * @uml.property  name="description"
     */
    private String description;

    /**
     * Flag that indicates if MUG service is enabled.
     * @uml.property  name="serviceEnabled"
     */
    private boolean serviceEnabled;

    /**
     * The Multi-User Gaming manager for sending packets, etc.
     * @uml.property  name="mugManager"
     * @uml.associationEnd  
     */
    private MUGManager mugManager = null;

    /**
     * Utility for storing informations
     * @uml.property  name="storage"
     * @uml.associationEnd  
     */
    private MUGPersistenceProvider storage = null;

    /**
     * Additional identities to be added to the disco response for the service.
     */
    private List<Element> extraDiscoIdentities = new ArrayList<Element>();

    /**
     * Additional features to be added to the disco response for the service.
     */
    private List<String> extraDiscoFeatures = new ArrayList<String>();

    /**
     * Handle iq disco info queries.
     * @uml.property  name="iqDiscoInfoHandler"
     * @uml.associationEnd  
     */
    private IQDiscoInfoHandler iqDiscoInfoHandler;

    /**
     * Handle iq disco items queries.
     * @uml.property  name="iqDiscoItemsHandler"
     * @uml.associationEnd  
     */
    private IQDiscoItemsHandler iqDiscoItemsHandler;

    /**
     * Handle iq search queries.
     * @uml.property  name="iqSearchHandler"
     * @uml.associationEnd  
     */
    private IQSearchHandler iqSearchHandler;

    /**
     * A list of user sessions.
     */
    private Map<JID, DefaultMUGSession> sessions = new ConcurrentHashMap<JID, DefaultMUGSession>();

    /**
     * Supported games with disco info namespace.
     */
    private Map<String, MultiUserGame> games = null;

    /**
     * A collection of the categories supported by the games on this service.
     * @uml.property  name="gameCategories"
     */
    private List<String> gameCategories = new ArrayList<String>();

    /**
     * The local game rooms.
     */
    private Map<String, MUGRoom> rooms = null;

    /**
     * The local game rooms sorted by the categories.
     */
    private Map<String, List<MUGRoom>> roomsByCategory = new ConcurrentHashMap<String, List<MUGRoom>>();

    /**
     * Returns the permission policy for creating rooms.
     * @uml.property  name="roomCreationRestricted"
     */
    private boolean roomCreationRestricted = false;

    /**
     * A list of the administrators bare JIDs, these can create and destroy rooms.
     * @uml.property  name="admins"
     */
    private Collection<JID> admins;

    /**
     * The time to elapse (default is 5 minutes) between clearing of idle game sessions.
     */
    private int timeouttask = 300000;

    /**
     * The number of milliseconds (default is two days) a user session
     * must be idle before he/she gets kicked from all the rooms.
     */
    private int sessiontimeout = 172800000;

    /**
     * Timer to monitor game sessions.
     */
    private Timer timer = new Timer("MUG session cleanup");

    /**
     * Task that cleanup idle sessions and maybe kick users from the rooms.
     * @uml.property  name="sessionTimeoutTask"
     * @uml.associationEnd  
     */
    private SessionTimeoutTask sessionTimeoutTask;

    /**
     * Create a new multi user game server.
     *
     * @param subdomain Subdomain portion of the games services (for example, games for games.example.org)
     * @param description Short description of service for disco and such.
     * @param games A map of the supported games with the disco info namespace and implementing class.
     */
    public DefaultMUGService(String subdomain, String description, MUGManager mugManager,
            Map<String, MultiUserGame> games, MUGPersistenceProvider storage) {
        this.serviceName = subdomain;
        this.description = description;
        this.mugManager = mugManager;
        this.games = games;
        this.rooms = new ConcurrentHashMap<String, MUGRoom>();
        this.serviceEnabled = false;
        this.storage = storage;

        iqDiscoInfoHandler = new IQDiscoInfoHandler(this, mugManager);
        iqDiscoItemsHandler = new IQDiscoItemsHandler(this);
        iqSearchHandler = new IQSearchHandler(this, mugManager);

        // initialize game categories
        for (MultiUserGame game : games.values()) {
            if (!gameCategories.contains(game.getCategory().toLowerCase()))
                gameCategories.add(game.getCategory().toLowerCase());
        }
    }

    public void initialize(JID jid, ComponentManager componentManager) throws ComponentException {
        loadPropertys();
    }

    public void loadPropertys() {
        try {
            String value = null;
            value = storage.getServiceProperty(serviceName, "enabled");
            if (value != null && (value.toLowerCase().equals("false") || (value.equals("0")))) {
                serviceEnabled = false;
            }

            value = storage.getServiceProperty(serviceName, "restricted");
            if (value != null && (value.toLowerCase().equals("true") || (value.equals("1")))) {
                roomCreationRestricted = true;
            }

            value = storage.getServiceProperty(serviceName, "timeouttask");
            if (value != null) {
                try {
                    timeouttask = Integer.parseInt(value);
                } catch (NumberFormatException e) {
                    log.error("[MUG] Error while parsing " + serviceName + ".timeouttask.", e);
                }
            }

            value = storage.getServiceProperty(serviceName, "sessiontimeout");
            if (value != null) {
                try {
                    sessiontimeout = Integer.parseInt(value);
                } catch (NumberFormatException e) {
                    log.error("[MUG] Error while parsing " + serviceName + ".usertimeout.", e);
                }
            }

            admins = storage.getServiceAdmins(serviceName);
        } catch (Exception e) {
            if (log != null)
                log.error("[MUG] Error while loading service properties.", e);
        }
    }

    public void start() {
        if (sessionTimeoutTask == null) {
            // Run through the users every 5 minutes after a 5 minutes server startup delay (default
            // values)
            sessionTimeoutTask = new SessionTimeoutTask();
            timer.schedule(sessionTimeoutTask, timeouttask, timeouttask);
        }

        serviceEnabled = true;
    }

    public void shutdown() {
        serviceEnabled = false;

        timer.cancel();
        if (sessionTimeoutTask != null) {
            sessionTimeoutTask = null;
        }
    }

    /**
     * Get the subdomain of the multi user gaming service.
     * 
     * @return the subdomain of the service.
     */
    public String getName() {
        return serviceName;
    }

    /**
     * Get the full domain of the multi user gaming service.
     * 
     * @return the domain of the service.
     */
    public String getDomain() {
        if (!serviceEnabled)
            return null;
        return serviceName + "." + mugManager.getServerName();
    }

    /**
     * Get a collection of the administrators JIDs.
     * @return  A collection of the administrators JIDs.
     * @uml.property  name="admins"
     */
    public Collection<JID> getAdmins() {
        return admins;
    }

    /**
     * Get the JID of the multi user gaming service with the full service domain.
     * 
     * @return the subdomain of the service.
    */
    public JID getAddress() {
        return new JID(null, getDomain(), null, true);
    }

    /**
     * Get a human readable description of the multi user gaming service.
     * @return  the description of the service.
     * @uml.property  name="description"
     */
    public String getDescription() {
        return description;
    }

    /**
     * Get a human readable description of the multi user gaming service.
     * @param desc  the description of the service.
     * @uml.property  name="description"
     */
    public void setDescription(String desc) {
        this.description = desc;
    }

    /**
     * Get true if the the multi user gaming service is started and running.
     * @return  true if the service is running, otherwise false.
     * @uml.property  name="serviceEnabled"
     */
    public boolean isServiceEnabled() {
        return serviceEnabled;
    }

    /**
     * Get the number of active and saved Multi-user Game rooms.
     * 
     * @return the number of rooms.
     */
    public int getNumberRooms() {
        // TODO: Implement this
        return rooms.size();
    }

    /**
     * Get the number of active game rooms.
     * 
     * @return the number of rooms.
     */
    public int getNumberActiveRooms() {
        // TODO: Implement this
        return rooms.size();
    }

    /**
     * Get the number of saved game rooms.
     * 
     * @return the number of rooms.
     */
    public int getNumberSavedRooms() {
        // TODO: Implement this
        return 0;
    }

    /**
     * Retuns the total number of user sessions within the service.
     *
     * @return the number of user sessions on the server.
     */
    public int getNumberUserSessions() {
        return sessions.size();
    }

    /**
     * Processes a packet sent to this Component.
     *
     * @param packet the packet.
     */
    public void processPacket(Packet packet) {
        if (!isServiceEnabled()) {
            return;
        }
        // TODO: Remove debug output
        log.debug("[MUG]: Processing: " + packet.toXML());

        // The MUG service will receive all the packets whose domain matches the domain of the MUG
        // service. This means that, for instance, a disco request should be responded by the
        // service itself instead of relying on the server to handle the request.
        try {
            // Check if the packet is a disco or jabber search request
            if (packet instanceof IQ) {
                if (process((IQ) packet)) {
                    return;
                }
            }
            getGameSession(packet.getFrom()).process(packet);
        } catch (Exception e) {
            log.error(mugManager.getLocaleUtil().getLocalizedString("admin.error"), e);
        }

    }

    private DefaultMUGSession getGameSession(JID realJID) {
        DefaultMUGSession session = sessions.get(realJID);
        if (session == null) {
            session = new DefaultMUGSession(this, mugManager, realJID);
            sessions.put(realJID, session);
        }
        return session;
    }

    /**
     * Returns true if the IQ packet was processed. This method should only process disco packets
     * as well as jabber:iq:search packets sent to the MUG service.
     *
     * @param iq the IQ packet to process.
     * @return true if the IQ packet was processed.
     */
    private boolean process(IQ iq) {
        Element childElement = iq.getChildElement();
        String namespace = null;
        IQ reply = null;
        // Ignore IQs of type ERROR
        if (IQ.Type.error == iq.getType()) {
            return false;
        }
        if (iq.getTo().getResource() != null) {
            // Ignore here IQ packets sent to room occupants
            // these are handled in MUGRooms
            return false;
        }
        if (childElement != null) {
            namespace = childElement.getNamespaceURI();
        }
        if ("jabber:iq:search".equals(namespace)) {
            reply = iqSearchHandler.handleIQ(iq);
        } else if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
            reply = iqDiscoInfoHandler.handleIQ(iq);
        } else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
            reply = iqDiscoItemsHandler.handleIQ(iq);
        } else {
            return false;
        }
        try {
            // TODO: Remove debug output
            log.debug("[MUG]: Sending: " + reply.toXML());

            mugManager.sendPacket(this, reply);
        } catch (Exception e) {
            log.error("[MUG] Error while sending an IQ stanza.", e);
        }
        return true;
    }

    /**
     * Register a Multi-User Game
     *
     * @param game is the MultiUserGame which will be registered.
     */
    public void registerMultiUserGame(String namespace, MultiUserGame game) {
        if (!gameCategories.contains(game.getCategory().toLowerCase()))
            gameCategories.add(game.getCategory().toLowerCase());
        games.put(namespace, game);
    }

    /**
     * Unregister a Multi-User Game
     *
     * @param namespace The namespace of the MultiUserGame which will be unregistered.
     */
    public void unregisterMultiUserGame(String namespace) {
        MultiUserGame game = games.get(namespace);
        for (MUGRoom room : getGameRooms()) {
            if (room.getGame().getNamespace().equals(namespace))
                try {
                    removeGameRoom(room.getName());
                } catch (ForbiddenException e) {
                    log.error("[MUG] Can't remove room: ", e);
                }
        }
        games.remove(namespace);

        // remove the game category if it isn't supported anymore
        if (game != null) {
            boolean removeCategory = true;
            String category = game.getCategory().toLowerCase();
            for (MultiUserGame g : games.values()) {
                if (g.getCategory().toLowerCase().equals(category))
                    removeCategory = false;
            }
            if (removeCategory)
                gameCategories.remove(category);
        }
    }

    /**
     * Get the supported game classes.
     * 
     * @return the namespaces and classes that are supported.
     */
    public Map<String, MultiUserGame> getSupportedGames() {
        return games;
    }

    /**
     * Get a collection of the supported game categories.
     * @return  a collection of the game categories that are supported.
     * @uml.property  name="gameCategories"
     */
    public Collection<String> getGameCategories() {
        return gameCategories;
    }

    /**
     * Obtains a game room by name. If the game room does not exists then null will be returned.
     * 
     * @param roomName Name of the room to get.
     * @return The game room for the given name or null if the room does not exists.
     */
    public MUGRoom getGameRoom(String roomName) {
        return rooms.get(roomName);
    }

    /**
     * @return
     * @uml.property  name="roomCreationRestricted"
     */
    public boolean isRoomCreationRestricted() {
        return roomCreationRestricted;
    }

    public MUGRoom getGameRoom(String roomName, String gameNamespace, JID userJID)
            throws NotAllowedException, UnsupportedGameException {
        MUGRoom room = getGameRoom(roomName);
        if (room == null) {
            // Check permissions
            if (isRoomCreationRestricted() && !(admins.contains(userJID.toBareJID()))) {
                throw new NotAllowedException();
            }

            // Check Game Support
            MultiUserGame game = games.get(gameNamespace);
            if (game == null)
                throw new UnsupportedGameException();

            // Create Room
            room = new DefaultMUGRoom(this, mugManager, roomName, game, userJID);

            // Add to the room to the list which is sorted by the game categories
            String category = game.getCategory().toLowerCase();
            if (!roomsByCategory.containsKey(category))
                roomsByCategory.put(category, new ArrayList<MUGRoom>());

            roomsByCategory.get(category).add(room);
        }
        rooms.put(roomName, room);
        return room;
    }

    public Collection<MUGRoom> getGameRooms() {
        return rooms.values();
    }

    public boolean hasRoom(String roomName) {
        if (rooms == null || roomName == null)
            return false;
        return rooms.containsKey(roomName);
    }

    public void addExtraFeature(String feature) {
        if (!extraDiscoFeatures.contains(feature))
            extraDiscoFeatures.add(feature);
    }

    public void removeExtraFeature(String feature) {
        extraDiscoFeatures.remove(feature);
    }

    public List<String> getExtraFeatures() {
        return extraDiscoFeatures;
    }

    /**
     * Adds an extra Disco identity to the list of identities returned for the conference service.
     * @param category Category for identity.  e.g. conference
     * @param name Descriptive name for identity.  e.g. Public Chatrooms
     * @param type Type for identity.  e.g. text 
     */
    public void addExtraIdentity(String category, String name, String type) {
        Element identity = DocumentHelper.createElement("identity");
        identity.addAttribute("category", category);
        identity.addAttribute("name", name);
        identity.addAttribute("type", type);
        extraDiscoIdentities.add(identity);
    }

    /**
     * Removes an extra Disco identity from the list of identities returned for the conference service.
     * @param name Name of identity to remove.
     */
    public void removeExtraIdentity(String name) {
        for (Element elem : extraDiscoIdentities) {
            if (name.equals(elem.attribute("name").getStringValue())) {
                extraDiscoFeatures.remove(elem);
                break;
            }
        }
    }

    public List<Element> getExtraIdentities() {
        return extraDiscoIdentities;
    }

    public void removeGameRoom(String roomName) {
        MUGRoom room = rooms.get(roomName);

        if (room == null) {
            // No room found
            return;
        }

        String category = room.getGame().getCategory().toLowerCase();
        room.destroy();
        rooms.remove(roomName);
        if (roomsByCategory.containsKey(category))
            roomsByCategory.get(category).remove(room);
    }

    public Collection<MUGRoom> getGameRoomsByCategory(String category) {
        return roomsByCategory.containsKey(category.toLowerCase()) ? roomsByCategory.get(category.toLowerCase())
                : new ArrayList<MUGRoom>();
    }

    /**
     * Probes the presence of any user who's last packet was sent more than 5 minute ago.
     */
    private class SessionTimeoutTask extends TimerTask {
        /**
         * Remove any user session that has been idle for longer than the session timeout.
         */
        public void run() {
            checkForTimedOutSessions();
        }
    }

    private void checkForTimedOutSessions() {
        final long deadline = System.currentTimeMillis() - sessiontimeout;
        for (DefaultMUGSession session : sessions.values()) {
            try {
                // If user is not present in any room then remove the session
                if (!session.isParticipant()) {
                    removeSession(session.getAddress());
                    continue;
                }
                // Do nothing if this feature is disabled (i.e sessiontimeout equals -1)
                if (sessiontimeout < 1) {
                    continue;
                }
                if (session.getLastPacketTime() < deadline) {
                    removeSession(session.getAddress());
                }
            } catch (Throwable e) {
                log.error(mugManager.getLocaleUtil().getLocalizedString("admin.error"), e);
            }
        }
    }

    private void removeSession(JID jabberID) {
        DefaultMUGSession session = sessions.remove(jabberID);
        if (session != null) {
            for (MUGOccupant occupant : session.getOccupants()) {
                try {
                    occupant.getGameRoom().leave(occupant);
                } catch (Exception e) {
                    log.error("Can't leave game room: " + jabberID, e);
                }
            }
        }
    }
}