Java tutorial
/** * DefaultMUGRoom - A Multi-User Gaming room. * Some parts are inspired by the LocalMUCRoom of the Openfire XMPP * server. * * Copyright (C) 2004-2008 Jive Software. All rights reserved. * 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.forms.FormField; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import org.xmpp.packet.Presence; import org.xmpp.packet.Presence.Type; import org.dom4j.DocumentFactory; import org.dom4j.Element; import org.frogx.service.api.MUGManager; import org.frogx.service.api.MUGMatch; import org.frogx.service.api.MUGOccupant; import org.frogx.service.api.MUGRoom; import org.frogx.service.api.MUGService; import org.frogx.service.api.MultiUserGame; import org.frogx.service.api.exception.CannotBeInvitedException; import org.frogx.service.api.exception.ConflictException; import org.frogx.service.api.exception.ForbiddenException; import org.frogx.service.api.exception.GameConfigurationException; import org.frogx.service.api.exception.NotFoundException; import org.frogx.service.api.exception.RequiredPlayerException; import org.frogx.service.api.exception.RoomLockedException; import org.frogx.service.api.exception.ServiceUnavailableException; import org.frogx.service.api.exception.UnauthorizedException; import org.frogx.service.api.exception.UnsupportedGameException; import org.frogx.service.api.exception.UserAlreadyExistsException; import org.frogx.service.api.util.LocaleUtil; import org.frogx.service.core.iq.IQOwnerHandler; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * The first implementation of a {@see MUGRoom} . A game room manages its occupants, sends game moves, invitations, presence information,... * @author Günther Nieß */ public class DefaultMUGRoom implements MUGRoom { private static final Logger log = LoggerFactory.getLogger(DefaultMUGRoom.class); /** * This represents the privacy type of a {@see MUGRoom} . * @author Günther Nieß */ public enum Anonymity { /** * @uml.property name="nonAnonymous" * @uml.associationEnd */ nonAnonymous, /** * @uml.property name="semiAnonymous" * @uml.associationEnd */ semiAnonymous, /** * @uml.property name="fullyAnonymous" * @uml.associationEnd */ fullyAnonymous; } /** * The service hosting the room. * @uml.property name="mugService" * @uml.associationEnd */ private MUGService mugService; /** * The ComponentManager provides a logging utility, localized Strings and allows to send XML stanzas. * @uml.property name="mugManager" * @uml.associationEnd */ private MUGManager mugManager; /** * @uml.property name="locale" * @uml.associationEnd */ private LocaleUtil locale; /** * The game which can be played in this room * @uml.property name="game" * @uml.associationEnd */ MultiUserGame game; /** * The running match represents the game logic and game state. * @uml.property name="match" * @uml.associationEnd */ MUGMatch match; /** * The name of the room which is used in the JID address of the room. * @uml.property name="name" */ private String name; /** * The natural language name of the room. * @uml.property name="naturalLanguageName" */ private String naturalLanguageName; /** * Description of the room. The owner can change the description using the room configuration form. * @uml.property name="description" */ private String description; /** * A public room means that the room is searchable and visible. This means that the room can be located using disco or search requests. * @uml.property name="publicRoom" */ private boolean publicRoom; /** * Moderated rooms enables the owner to kick users, revoke roles and save and reload the match. * @uml.property name="moderated" */ private boolean moderated; /** * In a member-only room a user cannot enter without being on the member list. This can be done by inviting the user to a room. * @uml.property name="membersOnly" */ private boolean membersOnly; /** * A List of bare JIDs, that are game room's members. */ private List<String> members = new ArrayList<String>(); /** * The privacy type of this room. This describes who is able to see the occupants real JIDs. * @uml.property name="anonymity" * @uml.associationEnd */ private Anonymity anonymity; /** * Some rooms may restrict the occupants that are able to send invitations. * Sending an invitation in a members-only room adds the invitee to the members list. */ private boolean canOccupantsInvite; /** * This describes if the room occupants are public available via a disco items query. */ private boolean canDiscoverOccupants; /** * The password that every occupant should provide in order to enter the room. * @uml.property name="password" */ private String password = null; /** * The max. number of occupants who are able to join the room. * @uml.property name="maxOccupants" */ private int maxOccupants; /** * The occupants of the room accessible by the occupants nickname. * @uml.property name="occupants" */ private Map<String, MUGOccupant> occupants = new ConcurrentHashMap<String, MUGOccupant>(); /** * A list of the occupants nicknames who want to start the match. */ private List<String> startMatch = new ArrayList<String>(); /** * The bare JID of the room owner. * @uml.property name="owner" */ private String owner; /** * The IQ handler for the owner namespace. It helps to configure the room. * @uml.property name="iqOwnerHandler" * @uml.associationEnd */ private IQOwnerHandler iqOwnerHandler; /** * Create a game room. * * @param service The {@see MUGService} which hosts this room. * @param componentManager A {@see ComponentManager} provides a utility for sending * packages and logging. * @param roomName The room name which is used in the {@see JID} address of the room. * @param game A {@see MultiUserGame} which can be played within the room. * @param creator The {@see JID} of the user who is creating this room. */ public DefaultMUGRoom(MUGService service, MUGManager mugManager, String roomName, MultiUserGame game, JID creator) { this.mugService = service; this.mugManager = mugManager; this.locale = mugManager.getLocaleUtil(); this.name = roomName; this.naturalLanguageName = roomName; this.description = roomName; this.game = game; this.owner = creator.toBareJID(); this.iqOwnerHandler = new IQOwnerHandler(service, mugManager, this); loadDefaultValues(); this.match = game.createMatch(this); log.debug(locale.getLocalizedString("mug.room.debug.create") + roomName); } public void destroy() { if (match != null) game.destroyMatch(this); match = null; game = null; name = null; description = null; password = null; if (occupants != null) occupants.clear(); mugManager = null; } /** * @return * @uml.property name="name" */ public String getName() { return name; } public MUGService getMUGService() { return mugService; } public JID getJID() { return new JID(getName(), getMUGService().getAddress().getDomain(), null); } /** * @return * @uml.property name="game" */ public MultiUserGame getGame() { return game; } /** * @return * @uml.property name="match" */ public MUGMatch getMatch() { return match; } /** * @return * @uml.property name="occupants" */ public Collection<MUGOccupant> getOccupants() { return Collections.unmodifiableCollection(occupants.values()); } /** * @return * @uml.property name="naturalLanguageName" */ public String getNaturalLanguageName() { return naturalLanguageName; } /** * @return * @uml.property name="description" */ public String getDescription() { return description; } public boolean isLocked() { return match.getStatus() == MUGMatch.Status.created; } public boolean isPasswordProtected() { return password != null && password.trim().length() > 0; } /** * @return * @uml.property name="password" */ public String getPassword() { return password; } /** * @return * @uml.property name="membersOnly" */ public boolean isMembersOnly() { return membersOnly; } /** * @return * @uml.property name="moderated" */ public boolean isModerated() { return moderated; } /** * @return * @uml.property name="publicRoom" */ public boolean isPublicRoom() { return publicRoom; } public boolean isNonAnonymous() { return (anonymity == Anonymity.nonAnonymous); } public boolean isSemiAnonymous() { return (anonymity == Anonymity.semiAnonymous); } public boolean isFullyAnonymous() { return (anonymity == Anonymity.fullyAnonymous); } public boolean canOccupantsInvite() { return canOccupantsInvite; } public boolean canDiscoverOccupants() { return (!isLocked() && canDiscoverOccupants); } /** * @return * @uml.property name="owner" */ public JID getOwner() { return new JID(owner); } /** * @return * @uml.property name="maxOccupants" */ public int getMaxOccupants() { return maxOccupants; } public int getOccupantsCount() { return occupants.size(); } public int getPlayersCount() { return match.getPlayers().size(); } public Collection<String> getExtraFeatures() { // We don't have additional disco features return null; } public Collection<FormField> getExtraExtendedDiscoFields() { // We don't have additional extended disco fields return null; } public String getUID() { return getJID().toString(); } /** * Load the default room configuration. */ protected void loadDefaultValues() { //TODO: Read a adjustable default room configuration from DB canDiscoverOccupants = true; canOccupantsInvite = false; maxOccupants = 10; membersOnly = false; moderated = false; publicRoom = true; anonymity = Anonymity.semiAnonymous; } protected Presence getPresence() { Presence presence = new Presence(); presence.setFrom(getJID()); Element game = presence.addChildElement("game", MUGService.mugNS); Element statusElement = game.addElement("status"); statusElement.addText(match.getStatus().name()); if (match == null || match.getState() == null) { if (match == null) log.warn("[MUG] No match in room " + getJID() + "!"); if (match.getState() == null) log.debug("[MUG] No game state available in room " + getJID()); } else game.add(match.getState().createCopy()); return presence; } protected void resetStartMatch() { startMatch.clear(); } protected void sendBroadcastPacket(Packet packet, MUGOccupant sender) throws ComponentException { packet.setFrom(sender.getRoomAddress()); for (MUGOccupant recipient : occupants.values()) { recipient.send(packet); } } private void onlyOwner(JID user) throws ForbiddenException { // Check Permissions: only owners, admins or the MUG service are ok if (!mugService.getAdmins().contains(user.toBareJID()) && !owner.equals(user.toBareJID()) && !getJID().equals(user)) throw new ForbiddenException(); } /** * Set a human readable name of this room. * @param name the human readable name of this room. * @uml.property name="naturalLanguageName" */ public void setNaturalLanguageName(String name) { naturalLanguageName = name; } public void setAllowInvites(boolean canOccupantsInvite) { this.canOccupantsInvite = canOccupantsInvite; } /** * @param membersOnly * @uml.property name="membersOnly" */ public void setMembersOnly(boolean membersOnly) { this.membersOnly = membersOnly; } public void setMUGService(MUGService service) { this.mugService = service; } /** * @param password * @uml.property name="password" */ public void setPassword(String password) { this.password = password; } /** * @param publicRoom * @uml.property name="publicRoom" */ public void setPublicRoom(boolean publicRoom) { this.publicRoom = publicRoom; } /** * @param moderated * @uml.property name="moderated" */ public void setModerated(boolean moderated) { this.moderated = moderated; } /** * @param description * @uml.property name="description" */ public void setDescription(String description) { this.description = description; } /** * @param maxOccupants * @uml.property name="maxOccupants" */ public void setMaxOccupants(int maxOccupants) { this.maxOccupants = maxOccupants; } /** * Set the privacy type of this room. * @param anonymity describes who can see the real JID of occupants. * @uml.property name="anonymity" */ public void setAnonymity(Anonymity anonymity) { this.anonymity = anonymity; } public void addMember(JID newMember, MUGOccupant occupant) throws ForbiddenException { onlyOwner(occupant.getUserAddress()); String bareJID = newMember.toBareJID(); if (!members.contains(bareJID)) members.add(bareJID); } public void broadcastPresence(MUGOccupant occupant) { Presence presence = occupant.getPresence().createCopy(); for (MUGOccupant otherOccupant : occupants.values()) { if (otherOccupant.equals(occupant)) continue; try { // In semi-anonymous rooms we must add the real JID for room owners if ((anonymity == Anonymity.semiAnonymous) && MUGOccupant.Affiliation.owner.equals(otherOccupant.getAffiliation())) { Presence extPresence = presence.createCopy(); Element frag = extPresence.getChildElement("game", MUGService.mugNS); frag.element("item").addAttribute("jid", occupant.getUserAddress().toBareJID()); otherOccupant.send(extPresence); } else { otherOccupant.send(presence); } } catch (ComponentException e) { log.error(locale.getLocalizedString("mug.room.error.presence") + otherOccupant.getUserAddress(), e); } } } public void broadcastRoomPresence() { Packet roomPresence = getPresence(); for (MUGOccupant recipient : occupants.values()) { try { recipient.send(roomPresence); } catch (ComponentException e) { log.error(locale.getLocalizedString("mug.room.error.presence") + recipient.getUserAddress(), e); } } } public void broadcastTurn(Collection<Element> moves, MUGOccupant sender) throws ComponentException { Element rawMessage = DocumentFactory.getInstance().createDocument().addElement("message"); Element turn = rawMessage.addElement("turn", MUGService.mugNS + "#user"); if (moves != null) { for (Element move : moves) { move.setParent(null); turn.add(move); } } Message message = new Message(rawMessage, false); sendBroadcastPacket(message, sender); } public MUGOccupant changeNickname(String oldNick, String newNick, Presence newPresence) throws NotFoundException, ConflictException { MUGOccupant occupant = occupants.get(oldNick.toLowerCase()); if (occupant == null) throw new NotFoundException(); if (occupants.containsKey(newNick.toLowerCase())) throw new ConflictException(); // Refresh start counter if (startMatch.contains(oldNick.toLowerCase())) { startMatch.remove(oldNick.toLowerCase()); startMatch.add(newNick.toLowerCase()); } // Submit changing occupants.remove(oldNick.toLowerCase()); occupants.put(newNick.toLowerCase(), occupant); occupant.changeNickname(newNick); // Update presence if (newPresence != null) occupant.setPresence(newPresence); // Inform the occupants about the change Presence presence = occupant.getPresence().createCopy(); presence.setFrom(new JID(getName(), mugService.getDomain(), oldNick)); Element gameElement = presence.getChildElement("game", MUGService.mugNS); Element item = gameElement.element("item"); item.addAttribute("nick", newNick); for (MUGOccupant otherOccupant : occupants.values()) { // In semi-anonymous rooms we must add the real JID for room owners if ((anonymity == Anonymity.semiAnonymous) && MUGOccupant.Affiliation.owner.equals(otherOccupant.getAffiliation())) { gameElement.element("item").addAttribute("jid", occupant.getUserAddress().toBareJID()); } try { otherOccupant.send(presence); } catch (ComponentException e) { log.error(locale.getLocalizedString("mug.room.error.presence") + otherOccupant.getUserAddress(), e); } } return occupant; } public void handleOwnerIQ(IQ iq, MUGOccupant occupant) throws ForbiddenException, IllegalArgumentException, GameConfigurationException, UnsupportedGameException { iqOwnerHandler.handleIQ(iq, occupant); } public void invite(JID recipient, String reason, MUGOccupant invitor) throws ForbiddenException, CannotBeInvitedException { if (canOccupantsInvite() || MUGOccupant.Affiliation.owner.equals(invitor.getAffiliation())) { Message message = new Message(); message.setFrom(getJID()); message.setTo(recipient); Element gameElement = message.addChildElement("game", MUGService.mugNS + "#user"); Element invite = gameElement.addElement("invited"); invite.addAttribute("var", game.getNamespace()); if (invitor.getUserAddress() != null) { invite.addAttribute("from", invitor.getUserAddress().toBareJID()); } if (reason != null && reason.length() > 0) { invite.addElement("reason").setText(reason); } if (isPasswordProtected()) { gameElement.addElement("password").setText(getPassword()); } try { // TODO: Remove debug output log.debug("[MUG]: Sending: " + message.toXML()); mugManager.sendPacket(mugService, message); } catch (Exception e) { log.error(locale.getLocalizedString("mug.room.error.invite"), e); throw new CannotBeInvitedException(); } } else { throw new ForbiddenException(); } } public MUGOccupant join(String nick, String passwd, JID fullJID, Presence presence) throws ServiceUnavailableException, RoomLockedException, UserAlreadyExistsException, UnauthorizedException, ForbiddenException, ComponentException { DefaultMUGOccupant occupant = null; boolean isOwner = true; // Check permission try { onlyOwner(fullJID); } catch (ForbiddenException e) { isOwner = false; } // Check capacity if (getMaxOccupants() > 0 && getOccupantsCount() >= getMaxOccupants() && !isOwner) throw new ServiceUnavailableException(); // Check if the room is locked if (isLocked()) { if (!isOwner) { throw new RoomLockedException(); } } // Check if the nickname is already used in the room if (occupants.containsKey(nick.toLowerCase())) { if (occupants.get(nick.toLowerCase()).getUserAddress().toBareJID().equals(fullJID.toBareJID())) { //TODO: Nickname exists in room, and belongs to this user, maybe kick the previous instance. // The new instance will "take over" the previous role or handle two user instances. // Participants in the room shouldn't notice anything has occurred. throw new UserAlreadyExistsException(); } else throw new UserAlreadyExistsException(); } // Check password if (isPasswordProtected()) { if (password == null || !password.equals(getPassword())) { throw new UnauthorizedException(); } } // Set affiliation MUGOccupant.Affiliation affiliation; if (isOwner) { affiliation = MUGOccupant.Affiliation.owner; } else if (members.contains(fullJID.toBareJID())) { affiliation = MUGOccupant.Affiliation.member; } else if (isMembersOnly()) { throw new ForbiddenException(); } else { affiliation = MUGOccupant.Affiliation.none; } // Add the new occupant occupant = new DefaultMUGOccupant(this, fullJID, nick, affiliation, presence, mugManager); occupants.put(nick.toLowerCase(), occupant); // Send presence of the room itself (room and match information) occupant.send(getPresence()); // Send presence of existing occupants to new occupant for (MUGOccupant otherOccupant : occupants.values()) { if (otherOccupant.equals(occupant)) continue; // In semi-anonymous rooms we must add the real JID for room owners if ((anonymity == Anonymity.semiAnonymous) && isOwner) { Presence pres = otherOccupant.getPresence().createCopy(); Element frag = pres.getChildElement("game", MUGService.mugNS); frag.element("item").addAttribute("jid", otherOccupant.getUserAddress().toBareJID()); occupant.send(pres); } else { occupant.send(otherOccupant.getPresence()); } } // Broadcast the presence of the new occupant broadcastPresence(occupant); // Confirm and welcome the new occupant by his presence in the room occupant.send(occupant.getPresence()); return occupant; } public void sendPrivatePacket(Packet packet, MUGOccupant sender) throws NotFoundException, ComponentException { String nick = packet.getTo().getResource(); MUGOccupant occupant = occupants.get(nick.toLowerCase()); if (occupant != null) { packet.setFrom(sender.getRoomAddress()); occupant.send(packet); } else { throw new NotFoundException(); } } public void sendInvitationRejection(JID recipient, String reason, JID sender) throws ComponentException { Message message = new Message(); message.setFrom(getJID()); message.setTo(recipient); Element frag = message.addChildElement("game", MUGService.mugNS + "#user"); frag.addElement("declined").addAttribute("from", sender.toBareJID()); if (reason != null && reason.length() > 0) { frag.element("declined").addElement("reason").setText(reason); } // TODO: Remove debug output log.debug("[MUG]: Sending: " + message.toXML()); mugManager.sendPacket(mugService, message); } public void sendTurn(Collection<Element> moves, MUGOccupant sender, MUGOccupant recipient) throws ComponentException { Element rawMessage = DocumentFactory.getInstance().createDocument().addElement("message"); Element turn = rawMessage.addElement("turn", MUGService.mugNS + "#user"); if (moves != null) { for (Element move : moves) { move.setParent(null); turn.add(move); } } Message message = new Message(rawMessage, false); message.setFrom(sender.getRoomAddress()); recipient.send(message); } public boolean startMatch(MUGOccupant occupant) throws RequiredPlayerException, GameConfigurationException, ComponentException { //TODO: Make this robust against changing roles or nicknames boolean started = false; if (occupant.hasRole()) { if (!startMatch.contains(occupant.getNickname().toLowerCase())) { startMatch.add(occupant.getNickname().toLowerCase()); } // If all player sent a start, try to start. if (match != null && match.getPlayers() != null && match.getPlayers().size() == startMatch.size()) { resetStartMatch(); match.start(); started = true; } // Reflect Start Message to other players Message startMessage = new Message(); startMessage.setFrom(occupant.getRoomAddress()); startMessage.addChildElement("start", MUGService.mugNS + "#user"); for (MUGOccupant player : match.getPlayers()) { if (player == occupant) continue; player.send(startMessage); } // If the game has started, reflect the room state if (started) broadcastRoomPresence(); } else throw new ForbiddenException(); return started; } public void leave(MUGOccupant occupant) { boolean hasRole = occupant.hasRole(); MUGMatch.Status matchStateBefore = match.getStatus(); if (occupant.getPresence().getType() != Type.unavailable) occupant.setPresence(new Presence(Type.unavailable)); // leave the match and inform the occupants about changes match.leave(occupant); broadcastPresence(occupant); try { occupant.send(occupant.getPresence()); } catch (ComponentException e) { log.error(locale.getLocalizedString("mug.room.error.leave"), e); } // remove the occupant if (occupants.containsKey(occupant.getNickname().toLowerCase())) occupants.remove(occupant.getNickname().toLowerCase()); occupant.destroy(); occupant = null; // inform occupants about the changing match status if (!matchStateBefore.equals(match.getStatus())) broadcastRoomPresence(); // reset start counter if (hasRole) resetStartMatch(); // if he was the last, remove the room if (occupants.size() == 0) { mugService.removeGameRoom(name); } } }