/** * com.planet_ink.coffee_mud.core.intermud.net.Interactive * Copyright (c) 1996 George Reese * 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. * * The Imaginary interactive implementation * of interactive connections. */ package com.planet_ink.coffee_mud.core.intermud.net; import com.planet_ink.coffee_mud.core.intermud.server.ServerUser; import com.planet_ink.coffee_mud.core.intermud.server.Server; import com.planet_ink.coffee_mud.core.*; import java.io.*; import java.net.Socket; import java.util.Date; import java.util.Vector; /** * This class provides an implementation of the Imaginary server * interactive module. It is responsible for handling the login * of an individual user and processing its input as directed * by the server.<BR> * Created: 27 September 1996<BR> * Last modified: 27 September 1996 * @author George Reese (borg@imaginary.com) * @version 1.0 */ public abstract class Interactive implements ServerUser { /** * Given a user name, this method will build a unique * key. This unique key has nothing to do with the * unique object id. The idea behind a key name is to * ensure that you do not end up with a user named * Descartes and a user named Des Cartes. It removes * all non-alphabetic characters and makes the string * lower case. * @exception InvalidNameException thrown if the name produces an unuseable key * @param nom the visual name to create a key from */ static public String createKeyName(String nom) throws InvalidNameException { StringBuffer buff = new StringBuffer(nom.toLowerCase()); String key = new String(); int i; for(i=0; i<buff.length(); i++) { char c = buff.charAt(i); if( c >= 'a' && c <= 'z' ) { key = key + c; } else if( c != '\'' && c != '-' && c != ' ' ) { throw new InvalidNameException(c + " is an invalid character for names."); } } if( key.length() < 3 ) { throw new InvalidNameException("Your name must have at least three alphabetic characters."); } else if( key.length() > 29 ) { throw new InvalidNameException("Your name is too long."); } return key; } /** * Given a user name, this method will find the Interactive * instance associated with that user name. If no such user is * currently logged in, this method will return null. * @param nom the name of the desired user * @return the Interactive object for the specified name or null if no such user exists */ static public Interactive findUser(String nom) { ServerUser[] users = Server.getInteractives(); int i; try { nom = createKeyName(nom); } catch( InvalidNameException e ) { return null; } for(i=0; i<users.length; i++) { Interactive user = (Interactive)users[i]; if( user.getKeyName().equals(nom) ) { return user; } } return null; } private InteractiveBody body; private Date current_login_time; private boolean destructed; private String display_name; private String email; private InputThread input_thread; private String key_name; private Date last_command_time; private String last_login_site; private Date last_login_time; private String object_id; private PrintStream output_stream; private String password; private String real_name; private Vector redirect; private Socket socket; /** * Constructs a new interactive object and initializes * its data. */ public Interactive() { super(); destructed = false; input_thread = null; object_id = null; output_stream = null; redirect = new Vector(); } /** * Implementation of the ServerUser connect method. * A mudlib will want to display a welcome screen * and ask for a user name by extending this method. * Here, the login time is set. */ public synchronized void connect() { current_login_time = new Date(); last_command_time = new Date(); } /** * Stops any running I/O threads for this interactive, closes the * user socket, and marks the object for destruction according to * the requirements of the ServerObject interface. * @see com.planet_ink.coffee_mud.core.intermud.server.ServerObject#getDestructed */ public synchronized void destruct() { output_stream.flush(); if( input_thread != null ) { input_thread.stop(); input_thread = null; } try { if(socket!=null) socket.close(); } catch( java.io.IOException e ) { Log.errOut("IMInteractive",e); } destructed = true; } /** * Called whenever a command is pulled off the incoming * command stack. If there is an instance of the Input * class to which input is supposed to be redirected, * then the command is sent there. Otherwise it is sent * to the parser. Muds wishing to implement their own * parser system should * @param cmd the command to be executed * @see #processInput */ protected synchronized void input(String cmd) { Input ob = null; if( redirect.size() > 0 ) { ob = (Input)redirect.elementAt(0); redirect.removeElementAt(0); } if( ob != null ) { ob.input(this, cmd); } else if( body != null ) { body.executeCommand(cmd); } if( redirect.size() < 1 ) { sendMessage(getPrompt(), true); } } /** * This method is triggered by the input thread when it detects * that the user has lost their link. It will tell the body object * that the link is lost, then destruct itself. */ protected void loseLink() { socket = null; if( body != null ) { body.loseLink(); } destruct(); } /** * Does event handling for the user object. Each * server cycle, the server triggers this method. If * the user has periodic events which occur to it, * the event processor will flag that the event() method * should be called. * @see com.planet_ink.coffee_mud.core.intermud.server.ServerObject#processEvent */ public void processEvent() { } /** * The server triggers this method once each server cycle to see * if the user has any input waiting to be processed. This method * checks the input queue. If there is input waiting, it updates * the last command time and calls the input() method with the * waiting command. Otherwise it simply returns. * @see #input * @see com.planet_ink.coffee_mud.core.intermud.server.ServerUser#processInput * @see com.planet_ink.coffee_mud.core.intermud.server.ServerThread#run */ public synchronized final void processInput() { if( input_thread != null ) { String msg = input_thread.nextMessage(); if( msg != null ) { last_command_time = new java.util.Date(); input(msg); } } } /** * Redirects user input to the input object passed to it. * This will create a LIFO chain of input redirection. For * example, if I have my input currently redirected to a * mud created editor, then I wish to get help from inside * the editor, my next input will be directed to the help * prompt. If I enter something at that point with no further * input redirection, my next input will then go back to the * editor. * @param ob the instance of com.planet_ink.coffee_mud.core.intermud.net.Input to which input will be redirected * @see com.planet_ink.coffee_mud.core.intermud.net.Input * @see #input */ public synchronized final void redirectInput(Input ob) { redirect.addElement(ob); } /** * Sends a message across to the client with a newline appended * to the message. * @param msg the message to send to the client machine */ public final void sendMessage(String msg) { if( socket == null ) { return; } sendMessage(msg, false); } /** * Sends a message across to the client. It will append * nowrap is true, no newline will be appended. * @param msg the message to send to the client * @param nowrap if true, no newline is attached */ public final void sendMessage(String msg, boolean nowrap) { if( !nowrap ) { msg += "\n"; } output_stream.print(msg); output_stream.flush(); } /** * Validates a user password against a random string. * @return true if the two passwords match */ public final boolean validatePassword(String other) { return other.equals(password); } /** * Provides the address from which this user is connected. * @return the host name for this user's current site */ public final String getAddressName() { if(CMProps.getVar(CMProps.SYSTEM_MUDDOMAIN).length()>0) return CMProps.getVar(CMProps.SYSTEM_MUDDOMAIN).toLowerCase(); return socket.getInetAddress().getHostName(); } /** * Provides the body to which this user is connected. * @return the body to which this user is connected, or null if no body exists */ public final InteractiveBody getBody() { return body; } /** * Sets the body to which this interactive connection * is connected. Any mudlib using this system for * interactive management must implement the InteractiveBody * interface for any body to be used by a user. * @param ob the body to which this interactive is being connected * @see com.planet_ink.coffee_mud.core.intermud.net.InteractiveBody */ public void setBody(InteractiveBody ob) { body = ob; } /** * Provides the time at which the user logged in for this session * @return the time of login for the current session */ public final Date getCurrentLoginTime() { return current_login_time; } /** * Tells whether or not the user is marked for destruction. * @return true if the user is marked for destruction */ public boolean getDestructed() { return destructed; } /** * Provides the user's name as they wish it to appear * with mixed capitalization, spaces, hyphens, etc. * @return the user's display name */ public String getDisplayName() { return display_name; } /** * Sets the user's display name. Prevents the operation * if the display name is not a permutation of the key * name. * @param str the new display name */ public final void setDisplayName(String str) { try { if( !getKeyName().equals(Interactive.createKeyName(str)) ) { return; } display_name = str; } catch( InvalidNameException e ) { return; } } /** * Provides the user's email address * @return the email address for this user */ public final String getEmail() { return email; } /** * Sets the user's email address * @param str the new email address */ public final void setEmail(String str) { email = str; } /** * Provides the number of seconds which have elapsed since the user * last entered a command. * @return the idle time in seconds */ public final int getIdle() { return (int)(((new Date()).getTime() - last_command_time.getTime())/1000); } /** * Provides the key name for this user. The key name is a * play on the user name to create a unique identifier for this * user that will always work. For example, the following * command should work for a user:<BR> * <PRE> * tell descartes hi! * tell deScartes hi! * tell des cartes hi! * </PRE> * The key name thus creates a common denomenator to which a name * can be reduced for comparison. * @see #createKeyName */ public final String getKeyName() { return key_name; } /** * Sets the key name during user creation. This prevents resetting * of the key name. * @param str the key name being set * @see #getKeyName */ protected void setKeyName(String str) { if( key_name != null ) { return; } key_name = str; } /** * Provides the name of the site from which the user logged in * at their last login. * @return the last login site */ public final String getLastLoginSite() { return last_login_site; } /** * Sets the last login site. Used by a subclass * during login. * @param site the last login site */ public void setLastLoginSite(String site) { if( last_login_site != null ) { return; } last_login_site = site; } /** * Provides the time of the user's last login. * @return the last login time */ public final Date getLastLoginTime() { return last_login_time; } /** * Used by the login process to set the last login * time. * @param time the time the user last logged in */ public void setLastLoginTime(Date time) { if( last_login_time != null ) { return; } last_login_time = time; } /** * Gives the user object's object id. * @return the object id * @see com.planet_ink.coffee_mud.core.intermud.server.ServerObject#getObjectId */ public final String getObjectId() { return object_id; } /** * Allows the server to set the object id. * @param id the object id assigned to this object * @see com.planet_ink.coffee_mud.core.intermud.server.ServerObject#setObjectId */ public final void setObjectId(String id) { if( object_id != null ) { return; } object_id = id; } /** * Allows a subclass to get the password. * @return the user's password */ protected String getPassword() { return password; } /** * Sets the user's password. * @param pass the new password */ protected void setPassword(String pass) { password = pass; } /** * Provides the user's command prompt. * @return the command prompt */ public String getPrompt() { return "> "; } /** * Provides the user's real name, or null if they never entered * a real name. * @return the user's real name or null */ public final String getRealName() { return real_name; } /** * Sets the user's real name. * @param nom the real name for the user */ public void setRealName(String nom) { real_name = nom; } /** * Called by the server before connect() is called to assign * the socket for this Interactive to it. * @param s the socket for this connection * @see com.planet_ink.coffee_mud.core.intermud.server.ServerUser#setSocket */ public final void setSocket(Socket s) throws java.io.IOException { socket = s; input_thread = new InputThread(socket, this); output_stream = new PrintStream(s.getOutputStream()); } } /** * The InputThread class handles asynchronous user input and queues * it up to be picked up by the user synchronously. In English, * the user can be entering information at any point in time * while the server is running. You want, however, that a command * be executed in a specific order. This class therefore stuffs commands * into a queue when they arrive. When the user is ready, it pulls a * single command off to be executed.<BR> * Created: 27 September 1996 * Last modified 27 September 1996 * @author George Reese (borg@imaginary.com) * @version 1.0 * @see com.planet_ink.coffee_mud.core.intermud.net.Interactive */ class InputThread implements Runnable { private Vector input_buffer; private BufferedReader stream; private boolean destructed; private Thread thread; private Interactive user; /** * Constructs and starts the thread which accepts user * input. As a user enters a command, the command is * added to a input_buffer. During each server cycle, the * Interactive object for this thread pulls off one * command and executes it. * @exception java.io.IOException thrown if no input stream can be created * @param s the socket connected to the user's machine * @param u the Interactive attached to this thread */ public InputThread(Socket s, Interactive u) throws java.io.IOException { destructed = false; user = u; input_buffer = new Vector(10); stream = new java.io.BufferedReader(new java.io.InputStreamReader(s.getInputStream())); thread = new Thread(this); thread.setDaemon(true); thread.start(); } /** * As long as the user is connected, this thread accepts * input from the user machine. If the user drops link, * this will call loseLink() in the interactive object. * @see com.planet_ink.coffee_mud.core.intermud.net.Interactive#loseLink */ public void run() { while( !destructed ) { String msg; try { msg = stream.readLine(); } catch( java.io.IOException e ) { synchronized( user ) { user.loseLink(); } return; } synchronized( this ) { input_buffer.addElement(msg); } try { Thread.sleep(10); } catch( InterruptedException e ) { } } } /** * The interactive object for this input thread will * call stop if the interactive is destructed for * any reason. */ public void stop() { CMLib.killThread(thread,500,1); destructed = true; } protected synchronized String nextMessage() { String msg; synchronized( input_buffer ) { if( input_buffer.size() > 0 ) { msg = (String)input_buffer.elementAt(0); input_buffer.removeElementAt(0); } else { msg = null; } } return msg; } }