// QueryTransaction.java
// Simple asynchronous transaction class.
// Copyright (c) 1996 by Paul Burchard
// Distributed under the terms of the GNU Library General Public License

////////////////////////////////////////////////////////////////
// Please do not use this code as an example of anything.     //
// It is a nest of workarounds for bugs in AWT and Netscape,  //
// for AWT's horrible design for event and layout management, //
// for Netscape/Sun's severely broken thread implementation,  //
// and for Java's OOP-hostile network loading strategy.       //
////////////////////////////////////////////////////////////////

import java.applet.*;
import java.lang.*;
import java.io.*;
import java.net.*;
import java.util.*;

class QueryTransaction extends Observable implements Runnable {
	static int currentSerial = 0;
	static long timeout = 60000; // one minute
	static long waitpoll = 4000; //!!! wait() is not reliably notify()-able; check status every few seconds

	int mySerial = -1;
	URL scriptURL, queryURL;
	String queryString;
	QueryTransaction prev, next;
	Thread runThread;
	Applet app;
	boolean waiting, complete;
	
	public QueryTransaction(QueryTransaction chainTrans, URL script) {
		if((prev = chainTrans) != null) prev = prev.chain(this);
		scriptURL = script;
	}
	public QueryTransaction(QueryTransaction chainTrans, URL script, String query) {
		if((prev = chainTrans) != null) prev = prev.chain(this);
		scriptURL = script;
		setQuery(query);
	}
	public QueryTransaction(QueryTransaction chainTrans, URL script, Hashtable query) {
		if((prev = chainTrans) != null) prev = prev.chain(this);
		scriptURL = script;
		setQuery(query);
	}
	public final void setQuery(String query) {
		if(query != null) queryString = urlEncodeText(query);
		else queryString = null;
	}
	public final void setQuery(Hashtable query) {
		if(query==null || query.isEmpty()) {
			queryString = null;
		} else {
			StringBuffer queryStringBuf = new StringBuffer();
			String sep = "";
			for(Enumeration i=query.keys(); i.hasMoreElements();) {
				Object k = i.nextElement();
				queryStringBuf.append(sep).append(urlEncodeText(k.toString())).append(
					"=").append(urlEncodeText(query.get(k).toString()));
				if(sep.length() == 0) sep = "&";
			}
			queryString = queryStringBuf.toString();
		}
	}
	public int serial() {
		return mySerial;
	}
	public synchronized QueryTransaction chain(QueryTransaction nextTrans) {
		if(!complete) { next = nextTrans; return this; }
		else return null;
	}
	public synchronized void unchain() {
		QueryTransaction nextTrans;
		synchronized(this) {
			complete = true;
			nextTrans = next;
			prev = next = null;
		}
		if(nextTrans!=null && nextTrans.prev==this) nextTrans.notify();
	}
	public synchronized boolean isComplete() {
		return complete;
	}
	public synchronized boolean isWaiting() {
		return waiting;
	}
	public void start(Observer callback) {
		addObserver(callback);
		start();
	}
	public void start(Applet callback) {
		app = callback;
		start();
	}
	public void start() {
		mySerial = currentSerial++;
		if(countObservers() > 0) {
			// Asynchronous query.
			if(queryString == null) queryURL = scriptURL;
			else {
				try { queryURL = new URL(scriptURL.toExternalForm() + "?" + queryString); }
				catch(MalformedURLException e) { queryURL = null; }
			}
			if(queryURL == null) unchain();
			else {
				runThread = new Thread(this);
				runThread.setPriority(Thread.NORM_PRIORITY+2);
				runThread.start();
			}
		} else if(app != null) {
			// No Observers to receive result; replace designated page instead.
			unchain();
			if(queryString == null) app.getAppletContext().showDocument(scriptURL);
			else {
				try { queryURL = new URL(scriptURL.toExternalForm() + "?" + queryString); }
				catch(MalformedURLException e) { queryURL = null; }
				if(queryURL != null) app.getAppletContext().showDocument(queryURL);
			}
		} else {
			unchain();
		}
	}
	public void run() {
		// Wait for previous transaction (if any) to finish.
		if(queryURL == null) { unchain(); return; }
		synchronized(this) {
			waiting = true;
			long bailout = System.currentTimeMillis() + timeout;
			while(waiting && prev!=null && !prev.isComplete()) {
				try {
					wait(waitpoll);
					if(System.currentTimeMillis()>=bailout && prev!=null && !prev.isComplete()) waiting = false;
				} catch(InterruptedException e) {}
			}
		}
		if(!waiting) {
			unchain();
			setChanged();
			notifyObservers(new RuntimeException("Server not responding, giving up on input line "+serial()));
			return;
		}
		synchronized(this) {
			waiting = false;
			prev = null;
		}
		
		// Perform transaction via URLConnection.
		StringBuffer output = new StringBuffer();
		try {
			BufferedInputStream i = new BufferedInputStream(queryURL.openConnection().getInputStream());
			setChanged();
			for(int b=i.read(); b>=0; b=i.read()) output.append((char)b); //!!! inefficient?
			i.close();
		} catch(Exception e) {}
		notifyObservers(output.toString());
		unchain();
	}
	public static String urlEncodeText(String s) {
		char[] dec = s.toCharArray();
		char[] enc = new char[3*dec.length];
		int i, j;
		for(i=j=0; i<dec.length; i++) {
			char c = dec[i];
			if(Character.isSpace(c)) enc[j++] = '+';
			else if(Character.isDigit(c) || Character.isUpperCase(c) || Character.isLowerCase(c)) enc[j++] = c;
			else {
				int ic = (((int)c) % 256);
				if(ic < 0) ic += 256;
				enc[j++] = '%';
				enc[j++] = Character.forDigit(((ic)/16) % 16, 16);
				enc[j++] = Character.forDigit(ic % 16, 16);
			}
		}
		return(new String(enc, 0, j));
	}
	public static String urlDecodeText(String s) {
		char[] enc = s.toCharArray();
		char[] dec = new char[enc.length];
		int i, j=0, state;
		try for(i=j=state=0; i<enc.length; i++) {
			char c = enc[i];
			if(c == '+') dec[j++] = ' ';
			else if(c == '%') {
				int d1 = Character.digit(enc[++i], 16);
				int d2 = Character.digit(enc[++i], 16);
				if(d1>=0 && d2>=0) dec[j++] = (char)(16*d1 + d2);
			}
			else dec[j++] = c;
		} catch(IndexOutOfBoundsException e) {}
		return(new String(dec, 0, j));
	}
}



