/**
* 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
*/
@SuppressWarnings("unchecked")
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 = "";
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
*/
@SuppressWarnings("unchecked")
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;
}
}