/*
Copyright (c) 1997, Jay F Kominek

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL JAY
F KOMINEK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

In other words, this software is under a license identical to that of X11.
*/
import java.net.*;
import java.io.*;
import java.util.*;

/**
 * An object for accessing dictionaries via the DICT protocol.
 * @author Jay Kominek <a href=mailto:jkominek@xtn.net>jkominek@xtn.net</a>
 * @version 0.99q of April 26, 1997 RFC Draft
 * @see Definition
 */
public class NetDictionary {
 /**
  * The socket to the DICT server.
  */
 Socket socket;
 BufferedReader br;
 DataOutputStream dos;
 /**
  * Are we connected?
  */
 boolean connected = false;

 /**
  * String representation of the last error we got, for easy display.
  */
 String lastError;

 /**
  * The banner that we got when we connected, we'll need that to generate
  * the proper secret for using AUTH.
  * @see auth
  */
 String serverBanner = new String("");

 /* Constructors */

 NetDictionary() {
  connected = false;
 }

 /**
  * @param server The DICT server to connect this object to.
  */
 NetDictionary(String server) {
  connect(server);
 }

 /**
  * @param server The DICT server to connect this object to.
  * @param port The port of the DICT server to connet to.
 NetDictionary(String server, int port) {
  connect(server,port);
 }

 /* Network-layer methods / internal use methods */

 /**
  * Connect this object to a DICT server via TCP and setup data streams.
  * @param server The DICT server to connect to.
  * @return If the object is now connected to a server or not.
  */
 public boolean connect(String server) {
  return connect(server,2628);
 }

 /**
  * Connect this object to a DICT server via TCP and setup data streams.
  * @param server The DICT server to connect to.
  * @param port The port to connect to the server on.
  * @return If the object is now connected to a server or not.
  */
 public boolean connect(String server, int port) {
  if(connected)
   close();
  try {
   socket = new Socket(server,port);
   br  = new BufferedReader(new InputStreamReader((socket.getInputStream())));
   dos = new DataOutputStream(socket.getOutputStream());
   serverBanner = getLine();
   if(serverBanner.startsWith("220")) {
    connected = true;
   } else {
    connected = false;
    socket.close();
    lastError = serverBanner.substring(serverBanner.indexOf(' ')+1);
   }
  } catch (IOException e) {
   lastError = "IO Exception";
   connected = false;
  }
  return connected;
 }

 /**
  * My very own line-reading function. The one in Java 1.0.2 is deprecated
  * in 1.1, and the one in 1.1 doesn't exist in 1.0.2, so i wrote this.
  * @return The string that it read, short of the \r\n
  */
 String getLine() {
  String buffer = new String("");

// this is a bunch of crap
/*  byte temp[] = new byte[1];

  try {
   dis.read(temp);
   while(!buffer.endsWith("\n")) {
    String moo = new String(temp,0);
    buffer = buffer + moo;
    dis.read(temp);
   }
  } catch (IOException e) {
   lastError = "IO Exception";
  }
  buffer = buffer.substring(0,buffer.length()-1);

  if(buffer.indexOf('\r')>0) {
   System.out.println("Hah! Spied a \\r at "+buffer.indexOf('\r')+
                      "total string length was "+buffer.length());
   buffer = buffer.substring(0,buffer.length()-1);
  } */
// end of the bunch of crap

  try {
   buffer = br.readLine();
  } catch(IOException e) {
   ;
  }

  return buffer;
 }

 /**
  * This reads lines until it gets to a line with only a '.' on it.
  * @return A multi-line string.
  */
 String getTextual() {
  String buffer = new String(""), temp = new String("");

  temp = getLine();
  while(!temp.equals(".")) {
   buffer = buffer + temp + "\r\n";
   temp = getLine();
  }

  return buffer;
 }

 /**
  * A function to write a line out to the server, and append the \r\n, for
  * me, as I'm a lazy bum.
  * @param line The line to send to the server.
  */
 void writeLine(String line) {
  try {
   dos.writeBytes(line+"\r\n");
  } catch (IOException e) {
   lastError = "IO Exception";
  }
 }

 /**
  * Needed a tokenizer that understood the way \ and "" worked, well.
  * @param args A string which needs to be parsed into tokens.
  * @return A vector of strings, which are the tokens.
  */
 Vector argify(String args) {
  Vector results = new Vector(3,1);
  int pointer = 0;
  String temp = new String("");
  boolean inQuotes = false;

  for(pointer = 0 ; pointer < args.length() ; pointer++) {
   switch(args.charAt(pointer)) {
    case '\\':
     pointer++;
     temp = temp + String.valueOf(args.charAt(pointer));
     break;
    case '"':
     if(inQuotes)
      inQuotes = false;
     else
      inQuotes = true;
     break;
    case ' ':
     if(!inQuotes) {
      results.addElement(temp);
      temp = "";
     } else
      temp = temp + String.valueOf(args.charAt(pointer));
     break;
    default:
     temp = temp + String.valueOf(args.charAt(pointer));
     break;
   }
  }
  if(temp.length()>0)
   results.addElement(temp);

  return results;
 }

 /* protocol related functions */

 /**
  * In case, for some reason, the user would like to see the server's
  * banner, we let them get it here.
  * @return The banner given to us by the server.
  */
 public String getBanner() {
  return serverBanner;
 }

 /**
  * @return A textual version of the last error.
  */
 public String lastError() {
  return lastError;
 }

 /**
  * Disconnect from the server.
  */
 public void close() {
  quit();
 }

 /**
  * Send QUIT to the server and close the connection.
  */
 public void quit() {
  writeLine("QUIT");
  try {
   socket.close();
  } catch (IOException e) {
   ;
  }
  connected = false;
 }

 /**
  * Request the definition of a word from the server.
  * @param word The word to have the server look up.
  * @return A vector of Definitions
  */
 public Vector define(String word) {
  return define(word,"!");
 }

 /**
  * Request the definition of a word from the server.
  * @param word The word to have the server look up.
  * @param database The specific database to have the server look in.
  * @return A vector of Definitions
  */
 public Vector define(String word, String database) {
  Vector results = new Vector(1,1);
  String temp, body = new String(""), db, dbname, temp2;
  Enumeration enum;
  int moo, cow;

  writeLine("DEFINE "+database+" \""+word+"\"");

  temp = getLine();

  if(temp.startsWith("550")) {
   lastError = "Invalid database \""+database+"\"";
   return results;
  } else if(temp.startsWith("552")) {
   lastError = "No match for word \""+word+"\"";
   return results;
  }

  temp = getLine();
  while(!temp.startsWith("250")) {
   System.out.println(temp);
   enum = argify(temp.substring(temp.indexOf(' ')+1)).elements();
   body = getTextual();
   results.addElement(new Definition((String)enum.nextElement(),
				(String)enum.nextElement(),
				(String)enum.nextElement(),body));
   temp = getLine();
  }

  return results;
 }

 /**
  * Have the server try to find matches for a word.
  * @return A vector of definitions, with the actual definition field blank.
  * @param word The word to attempt to match.
  */
 public Vector match(String word) {
  return match(word,".","!");
 }

 /**
  * Have the server try to find matches for a word.
  * @return A vector of definitions, with the actual definition field blank.
  * @param word The word to attempt to match.
  * @param strategy The strategy to use when trying to match the word.
  */
 public Vector match(String word, String strategy) {
  return match(word,strategy,"*");
 }

 /**
  * Have the server try to find matches for a word.
  * @return A vector of definitions, with the actual definition field blank.
  * @param word The word to attempt to match.
  * @param strategy The strategy to use when trying to match the word.
  * @param database The database to search in.
  */
 public Vector match(String word, String strategy, String database) {
  Vector results = new Vector(1,1);
  String temp;

  writeLine("MATCH \""+database+"\" "+strategy+" \""+word+"\"");

  temp = getLine();

  if(temp.startsWith("550")) {
   lastError = "Invalid database \""+database+"\"";
   return results;
  } else if(temp.startsWith("551")) {
   lastError = "Invalid strategy \""+strategy+"\"";
   return results;
  } else if(temp.startsWith("552")) {
   lastError = "No matches for \""+word+"\"";
   return results;
  }

  temp = getLine();
  while(!temp.equals(".")) {
   Enumeration e = argify(temp).elements();
   temp = (String)e.nextElement();
   results.addElement(new Definition((String)e.nextElement(),
					temp,"",""));
   temp = getLine();
  }

   // get the silly trailing 250 response code
   temp = getLine();

  return results;
 }

 /**
  * Send a client string to the server, a nice thing to do.
  * @param clientinfo The client string to send to the server.
  * @return Did the server accept the client line.
  */
 public boolean client(String clientinfo) {
  String result;
  writeLine("CLIENT "+clientinfo);
  result = getLine();
  if(result.startsWith("250"))
   return true;
  return false;
 }

 /**
  * Attempt to authenticate with the server.
  * @return This will always return false, as I don't have an MD5
  * function, yet.
  * @param username The username to send to the server.
  * @param secret The password, without banner included.
  */
 public boolean auth(String username, String secret) {
  /* this function can't go anywhere until i get a working MD5
     hash function, as the secret is the server banner catenated
     with the password and MD5'd. hence, it always returns false. */
  return false;
 }

 /**
  * Get a listing of all the databases on the server.
  * @return A vector of definitions, with the word blank, and only
  * the two database fields filled in.
  */
 public Vector showdb() {
  Vector results = new Vector(1,1);
  String temp;

  writeLine("SHOW DB");

  temp = getLine();

  if(temp.startsWith("554")) {
   lastError = "No databases present";
   return results;
  }

  temp = getLine();
  while(!temp.equals(".")) {
   Enumeration e = argify(temp).elements();
   results.addElement(new Definition("",(String)e.nextElement(),
			(String)e.nextElement(),""));
   temp = getLine();
  }

  // this should return a code 250, though i don't see what good
  // checking it would do...
  temp = getLine();

  return results;
 }

 /**
  * Get a listing of all the search strategies supported by this server.
  * @return A vector of Definitions which contain the supported strategies
  */
 public Vector showstrat() {
  Vector results = new Vector(1,1);
  String temp;

  writeLine("SHOW STRAT");

  temp = getLine();

  if(temp.startsWith("555")) {
   lastError = "No strategies present";
   return results;
  }

  temp = getLine();
  while(!temp.equals(".")) {
   Enumeration e = argify(temp).elements();
   results.addElement(new Definition("strat",(String)e.nextElement(),
					(String)e.nextElement(),""));
   temp = getLine();
  }

  temp = getLine();

  return results;
 }

 /**
  * Get information about a database stored on the server.
  * @return A pre-formatted string from the server.
  */
 public String showinfo(String database) {
  String temp = new String("");

  writeLine("SHOW INFO "+database);

  temp = getLine();

  if(temp.startsWith("550")) {
   lastError = "No such database "+database;
   return new String("");
  }

  temp = getTextual();

  return temp;
 }

 /**
  * Get information about the server provided by the adminstrator
  * @return The multi-line response given by the server.
  */
 public String showserver() {
  String result;
  writeLine("SHOW SERVER");

  result = getTextual();

  return result;
 }

 /**
  * Optional server status, timing information, that kind of thing.
  * @return A freeform string.
  */
 public String status() {
  String temp = new String("");
  writeLine("STATUS");

  temp = getLine();
  temp = temp.substring(temp.indexOf(' ')+1);

  return temp; 
}

 /* methods overridden from Object */

 /**
  * The hashcode function bases its result on the hashcode of the banner.
  */
 public int hashCode() {
  int temp;
  temp = getBanner().hashCode();
  return temp;
 }

 /**
  * Just check to see if we have identical hashcodes and stuff
  */
 public boolean equals(Object someObject) {
  if(someObject.hashCode()==hashCode())
   return true;
  return false;
 }
}
