import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.rmi.RemoteException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.ServiceException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.axis.encoding.XMLType;
import org.apache.axis.message.RPCParam;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

public class SOAPing {    
	private static final String version = "Version 1.00, 2004-04-07";
	private static final String ns = "http://jeckle.de/SOAPing";

	public static void main(String argv[]) {
		if ( argv.length == 0 ) {
				System.out.println("Usage: java SOAPing [-dnrtvVC] [-c count] [-i interval] [-l preload] [-s packetSize] [-p pattern] [-f #Threads] [-w deadline] [-a actionURI] [-x XML Output File] [-ttl timeout] [-o XML Statistics File] [-oc Comment] [-ns namespace] URI");
				System.exit(0);
		} //if
		
		int deadline=0;
		int preload=0;
		String pattern = new String("X");
		Config config = Config.getInstance();
		String usrComment = "";
				
		for (int i=0; i<argv.length; i++) {
			if ( argv[i].compareTo("-c")==0 ) {
				config.noPackets = Integer.parseInt(argv[i+1]);
			} //if
			if ( argv[i].compareTo("-s")==0 ) {
				config.packetSize = Long.parseLong(argv[i+1]);
			} //if
			if ( argv[i].compareTo("-p") == 0) {
				System.out.println( "pattern="+argv[i+1] );
				pattern = (new Character((char) (Integer.decode(argv[i+1])).intValue())).toString();
			} //if
			if ( argv[i].compareTo("-q") == 0) {
				config.quiet = true;
			} //if
			if ( argv[i].compareTo("-f") == 0) {
				config.noThreads = Integer.parseInt(argv[i+1]);
			} //if
			if ( argv[i].compareTo("-v") == 0) {
				config.verbose = true;
			} //if
			if ( argv[i].compareTo("-l") == 0 ) {
				preload = Integer.parseInt(argv[i+1]);
			} //if
			if ( argv[i].compareTo("-V") == 0 ) {
				System.out.println( version );
			} //if
			if ( argv[i].compareTo("-w") == 0) {
				deadline = Integer.parseInt(argv[i+1]);
			} //if
			if ( argv[i].compareTo("-i") == 0) {
				config.sleepTime = Integer.parseInt(argv[i+1]);
			} //if
			if ( argv[i].compareTo("-a") == 0) {
				config.actionHeader = argv[i+1];
			} //if
			if ( argv[i].compareTo("-x") == 0) {
				if (config.verbose) 
					System.out.println("XML output file="+argv[i+1]);
				try {
					config.outputWriter = new OutputStreamWriter(new FileOutputStream( argv[i+1] ));
				} catch (FileNotFoundException fnfe) {
					System.out.println("terminating due to a FileNotFoundException\nCannot create XML output file");
					System.exit(1);
				} //catch()
				try {
					config.outputWriter.write("<?xml version='1.0' encoding='UTF-8'?>\n<SOAPing xmlns='"+ns+"'>\n<Values>\n");
				} catch(IOException ioe) {
					System.out.println("terminating due to an IOException\nCannot write XML output file");
				} //catch()
			} //if
			
			if ( argv[i].compareTo("-oc") == 0) {
				usrComment = argv[i+1];
			} //if
			
			if ( argv[i].compareTo("-t") == 0) {
				config.timestamp = true;
			} //if
			if ( argv[i].compareTo("-n") == 0) {
				config.numbered = true;
			} //if
			if ( argv[i].compareTo("-d") == 0) {
				config.demo = true;
			} //if
			if ( argv[i].compareTo("-r") == 0) {
				config.rewrite = true;
			} //if
			if ( argv[i].compareTo("-ttl") == 0) {
				config.ttl = Integer.parseInt( argv[i+1] );
			} //if
			if (argv[i].compareTo("-o") == 0) {
				config.historyFile = argv[i+1];
			} //if
			if (argv[i].compareTo("-ns") == 0) {
				config.namespace = argv[i+1];
			} //if
			if (argv[i].compareTo("-C") == 0) {
				config.checkResult = true;
			} //if
		} //for
		
		config.endpoint = argv[argv.length-1];
		config.service = new Service();
		
		//create call's payload
		StringBuffer buf = new StringBuffer();
		for (int i=0; i<config.packetSize; i++) {
			buf.append( pattern );
		} //for
		config.packet = buf.toString();

		if (config.verbose) 
			System.out.println("creating "+config.noThreads+" threads ...");
		Thread[] threads = new Thread[config.noThreads];
		for(int i=0; i<config.noThreads; i++) { 
			threads[i] = new Thread(new SendPing(config) );
			if (deadline > 0)
				threads[i].setDaemon(true);
		} //for
		if (config.verbose) 
			System.out.println("... finished");
			
		if (config.verbose)
			System.out.println("sending "+preload+" packets preload ...");
		Config preloadConfig = (Config) config.clone();
		preloadConfig.noPackets = preload;
		Thread preT = new Thread(new SendPing(preloadConfig),"preload-Thread" );
		preT.start();
		
		try {
			preT.join();
		} catch(InterruptedException ie) {
			System.out.println("preloading failed");
			System.exit(1);
		} //catch
		
		if (config.verbose)
			System.out.println("... done");

		if (!config.quiet) {
			try {
				InetAddress adr = InetAddress.getLocalHost();
				System.out.println("PING "+config.endpoint+" from "+adr.getHostAddress()+": "+config.packetSize+" bytes of data.\n");
			} catch(java.net.UnknownHostException uhe) {
				System.out.println("unknown host");
				System.exit(1);
			} //catch()
		} //if
		for(int i=0; i<config.noThreads; i++) 
			threads[i].start();
		if (config.verbose) 
			System.out.println("all threads running now");

		if (deadline > 0 ) {
			try {
				Thread.sleep(deadline);
			} catch (InterruptedException ie) {
				System.out.println("interrupted exception while waiting");	
				System.exit(1);
			} //catch
			for (int i=0; i<config.noThreads; i++) 
				threads[i].interrupt();
				
		} else {
			try {
				for(int i=0; i<config.noThreads; i++) 
					threads[i].join();
			} catch (InterruptedException ie) {
				System.out.println("terminating with an interrupted exception");
				System.exit(1);
			} //catch()
		} //else

		int sum=0;
		long minTime = Long.MAX_VALUE;
		long maxTime = 0;
		long sumTime =0;
		long value;
		double tmp;
		float cps=0;
		if (config.verbose)
			System.out.println("collected "+config.time.size()+" values");
			
		for (int i=0; i<config.time.size(); i++) {
			value = ((Long) config.time.get(i)).longValue();
			sumTime += value;
			if (config.outputWriter != null) { 
				try {
					config.outputWriter.write( "<Value>"+value+"</Value>\n" );
				} catch(IOException ioe) {
					System.out.println("terminating due to an IOException\nCannot write XML output file");
					System.exit(1);
				} //catch()
			} //if
		}//for
		double avg = (double) sumTime /  (double) config.noPackets;

		for (int i=0; i<config.time.size(); i++) {
			value = ((Long) config.time.get(i)).longValue();
			tmp = value-avg;
			sum+=java.lang.Math.pow(tmp,2);
			if (value < minTime)
				minTime = value;
			if (value > maxTime)
				maxTime = value;
		}//for
		assert (minTime <= maxTime);
		float mdev = sum/config.getNoSent();
		mdev = (float) java.lang.Math.sqrt(mdev);
		System.out.println("\n--- "+config.endpoint+" ping statistics ---\n"+config.getNoSent()+" packets transmitted, "+config.getNoReceived()+" received, "+ (config.getNoSent() - config.getNoReceived())+"% loss, time "+sumTime+" ms");
		System.out.println("rtt min/avg/max/mdev = "+minTime+"/"+avg+"/"+maxTime+"/"+mdev+" ms");
		cps = (float) (1000*(1/(double)(sumTime/config.noPackets*config.noThreads)));
		System.out.println("calls/sec. "+cps);
		if (config.outputWriter != null) {
			try {
				config.outputWriter.write("</Values>");
				config.outputWriter.write("<Statistics>\n<MinTime>"+minTime+"</MinTime>\n<MaxTime>"+maxTime+"</MaxTime>\n<AvgTime>"+avg+"</AvgTime>\n<MDev>"+mdev+"</MDev>\n<CPS>"+cps+"</CPS>\n</Statistics>\n");
				config.outputWriter.write("</SOAPing>");
				config.outputWriter.close();
			} catch(IOException io) {
				System.out.println("Error closing XML output file");
				System.exit(1);
			} //catch()
		} //if

		if (config.historyFile != null) {
			DocumentBuilderFactory factory = null;
			DocumentBuilder builder = null;
			Document document = null;
			try {
				factory = DocumentBuilderFactory.newInstance();
				factory.setNamespaceAware(true);
	     		builder = factory.newDocumentBuilder();
	     	} catch(ParserConfigurationException pce) {
	     		System.out.println("terminating with a ParserConfigurationException");
	     		System.exit(1);
	     	} //catch

			if ( !(new File(config.historyFile)).exists() ) {
				if (config.verbose)
					System.out.println("Creating result file from scratch");
				document = builder.newDocument();				
				Element e = (Element) document.appendChild(document.createElementNS(ns,"SOAPing"));
				e.setAttribute("xmlns",ns);
				} else {
				if (config.verbose) 
					System.out.println("reading XML history file: " +config.historyFile);
				try {
	      		document = builder.parse( config.historyFile );
	
					if (config.verbose)
						System.out.println("file contains "+document.getElementsByTagNameNS(ns,"Test").getLength()+" test records");
				} catch(IOException ioe) {
					System.out.println("terminating with an IOException"); 
					System.exit(1);
				} catch(SAXException se) {
					System.out.println("terminating with a SAXException");
					System.exit(1);
				}//catch
			} //else
				
			try {
				Element test = document.createElementNS(ns,"Test");
				Element parameters = document.createElementNS(ns,"Parameters");
				Element results = document.createElementNS(ns,"Results");
				Element tmpEl;
							
				//parameters
				tmpEl = document.createElementNS(ns,"SOAPingVersion");
				tmpEl.appendChild( document.createTextNode( version ));
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"PacketSize");
				tmpEl.appendChild( document.createTextNode( (new Long(config.packetSize)).toString()) );
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"PacketsToSend");
				tmpEl.appendChild( document.createTextNode( (new Integer(config.noPackets)).toString()) );
				parameters.appendChild(tmpEl);

				tmpEl = document.createElementNS(ns,"Pattern");
				tmpEl.appendChild( document.createTextNode( pattern ));
				parameters.appendChild(tmpEl);

				tmpEl = document.createElementNS(ns,"NoThreads");
				tmpEl.appendChild( document.createTextNode( (new Integer(config.noThreads)).toString()) );
				parameters.appendChild(tmpEl);

				tmpEl = document.createElementNS(ns,"PreloadPackets");
				tmpEl.appendChild( document.createTextNode( (new Integer(preload)).toString()));
				parameters.appendChild(tmpEl);

				tmpEl = document.createElementNS(ns,"Deadline");
				if (deadline == 0) {
					tmpEl.appendChild( document.createTextNode( "not specified" ));
				} else {
					tmpEl.appendChild( document.createTextNode( (new Integer(deadline)).toString()));
				} //else
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"SleepTime");
				tmpEl.appendChild( document.createTextNode( (new Integer(config.sleepTime)).toString()));
				parameters.appendChild(tmpEl);

				tmpEl = document.createElementNS(ns,"SOAPActionURI");
				if (config.actionHeader != null) 
					tmpEl.appendChild( document.createTextNode( config.actionHeader ));
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"DemoMode");
				if (config.demo) {
					tmpEl.appendChild( document.createTextNode( "true" ));
				} else {
					tmpEl.appendChild( document.createTextNode( "false" ));
				} //if
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"TTL");
				if (config.ttl != 0) {
					tmpEl.appendChild( document.createTextNode( (new Integer(config.ttl)).toString()) );
				} else {
					tmpEl.appendChild( document.createTextNode( "default" ));
				} //if
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"SOAPEndpoint");
				tmpEl.appendChild( document.createTextNode( argv[argv.length-1] ));
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"Namespace");
				if (config.namespace != null)
					tmpEl.appendChild( document.createTextNode( config.namespace ));
				parameters.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"Comment");
				tmpEl.appendChild( document.createTextNode( usrComment ));
				parameters.appendChild(tmpEl);
				
				//results
				tmpEl = document.createElementNS(ns,"Measurement");
				tmpEl.appendChild( document.createTextNode( ISODate.getDate()));				
				results.appendChild(tmpEl);				
				
				tmpEl = document.createElementNS(ns,"NoSent");
				tmpEl.appendChild( document.createTextNode( (new Integer(config.getNoSent())).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"NoReceived");
				tmpEl.appendChild( document.createTextNode( (new Integer(config.getNoReceived())).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"MinTime");
				tmpEl.appendChild( document.createTextNode( (new Long(minTime)).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"AvgTime");
				tmpEl.appendChild( document.createTextNode( (new Double(avg)).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"MaxTime");
				tmpEl.appendChild( document.createTextNode( (new Long(maxTime)).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"MDev");
				tmpEl.appendChild( document.createTextNode( (new Float(mdev)).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"TotalTime");
				tmpEl.appendChild( document.createTextNode( (new Long(sumTime)).toString()) );
				results.appendChild(tmpEl);
				
				tmpEl = document.createElementNS(ns,"CallsPerSecond");
				tmpEl.appendChild( document.createTextNode( (new Float(cps).toString() )));
				results.appendChild(tmpEl);				
				
				test.appendChild(parameters);
				test.appendChild(results);
				document.getDocumentElement().appendChild(test); //add new
																 // result to
																 // tree
				
				//write document
				TransformerFactory transFactory = TransformerFactory.newInstance();
      		Transformer myTransformer = transFactory.newTransformer();
      		DOMSource src = new DOMSource( document );
      		StreamResult res = new StreamResult( config.historyFile );
      		myTransformer.transform( src, res );
				
				if (config.verbose)
					System.out.println("successfully wrote XML history file");

      	} catch (TransformerConfigurationException tce) {
      		System.out.println("terminating with a TransformerConfigurationException");
      		System.exit(1);
      	} catch (TransformerException te) {
      		System.out.println("terminating with a TransformerException");
      		System.exit(1);
      	} //catch()
		} //if
	} //main()
} //class SOAPing
// --------------------------------------------------------
class Config {
	public Vector time;
	private int noSent=0;
	private int noReceived=0;
	public long packetSize = 64;
	public int noPackets = 1;
	public int sleepTime=0;
	public boolean quiet=false;
	public boolean verbose=false;
	public boolean timestamp=false;
	public boolean numbered = false;
	public String packet;
	public Service service;
	public Call call;
	public String endpoint;
	public String actionHeader = null;
	public OutputStreamWriter outputWriter=null;
	public boolean demo=false;
	private static Config instance=null;
	public int noThreads=1;
	public boolean rewrite=false;
	public int ttl=0;
	public String historyFile=null;
	public String namespace=null;
	public boolean checkResult = false;
	
	public synchronized void incNoSent() {
		noSent++;
	} //incNoSent()
	public synchronized int getNoSent() {
		return noSent;
	} //getNoSet()
	public synchronized void incNoReceived() {
		noReceived++;
	} //incNoReceived()
	public synchronized int getNoReceived() {
		return noReceived;
	} //getNoReceived()
	

	public static Config getInstance() {
		if (instance == null) {
			instance = new Config();
		} //if
		return instance;
	} //factory
	
	public Config() {
		this.time = new Vector();
	} //constructor
	
	public Object clone() {
		Config c = new Config();
		c.packetSize=this.packetSize;
		c.noPackets=this.noPackets;
		c.sleepTime=this.sleepTime;
		c.quiet=this.quiet;
		c.verbose=this.verbose;
		c.packet=this.packet;
		c.service=this.service;
		c.call=this.call;
		c.endpoint=this.endpoint;
		c.actionHeader=this.actionHeader;
		c.timestamp=this.timestamp;
		c.outputWriter=this.outputWriter;
		c.numbered=this.numbered;
		c.demo=this.demo;
		c.noThreads=this.noThreads;
		c.rewrite=this.rewrite;
		c.ttl=this.ttl;
		c.historyFile=this.historyFile;
		c.namespace=this.namespace;
		c.checkResult=this.checkResult;
		return c;
	} //clone()
} //class Config
// --------------------------------------------------------
class SendPing implements Runnable {
	private Config config;
	private Object[] param;
	private long timeStart;
	private long timeEnd;
	private long consumedTime;
	private Call call = null;
		
	public SendPing(Config c) {
		config = c;

		//configure call
		Service service = config.service;
      try {
      	call = (Call) service.createCall();
		} catch (ServiceException se) {
			System.out.println("terminating with a service exception");
			System.exit(1);
		} //catch
		call.setTargetEndpointAddress( config.endpoint );
		call.setOperationName( "ping" );
		call.addParameter( "packet", XMLType.XSD_STRING, ParameterMode.IN );
		call.setReturnType(XMLType.XSD_STRING);
		if (config.verbose) {
			if (config.ttl != 0) {
				System.out.println("changing timeout from "+call.getTimeout()+" to "+config.ttl);
				call.setTimeout( new Integer(config.ttl) );
			} else {
				System.out.println("using default timeout "+call.getTimeout());
			} //if
		}
		if (config.actionHeader != null)
			call.setSOAPActionURI(config.actionHeader);

		if (config.namespace == null) {
			param = new Object[] {config.packet};	
	   } else {
	   	param = new Object[] { new RPCParam(config.namespace, "packet", config.packet) };
		} //if
	} //constructor
	
	public void run() {
		Object res = null;
      try {								
			for(int i=0; i<config.noPackets; i++) {
				if (!config.demo) {
					if (config.namespace == null) {
						timeStart = System.currentTimeMillis();
						call.invoke( param );
						timeEnd = System.currentTimeMillis();
					} else {
						timeStart = System.currentTimeMillis();
						call.invoke( config.namespace, "ping", param);
						timeEnd = System.currentTimeMillis();
					} //if
					//called with return checking is enabled
					if (config.checkResult) {
						if (config.namespace == null) {
						 	res = call.invoke( param );
							timeEnd = System.currentTimeMillis();
						} else {
							timeStart = System.currentTimeMillis();
							res = call.invoke( config.namespace, "ping", param);
							timeEnd = System.currentTimeMillis();
						} //if
						if (res == null || ((String) res).compareTo(config.packet) != 0) {
							System.out.println("terminating since the received data packet differs from the sent one");
							System.exit(1);
						} else {
							if (config.verbose)
								System.out.println("received packet in sync with sent one");
						} //else
					}//if
				} //if
				
				config.incNoSent();
				config.incNoReceived(); //only a workaround until we implement
										// asynchronous calls
				assert (timeEnd >= timeStart);
				consumedTime = timeEnd-timeStart;
				
				if(Thread.currentThread().getName().compareTo("preload-Thread") != 0)
					config.time.add( new Long(consumedTime) );
				
				if (!config.quiet) {
					synchronized (config) {
						System.out.print( "("+Thread.currentThread().getName()+") "+config.packetSize+" bytes from "+config.endpoint+": time="+consumedTime+" msec" );
						if (config.timestamp)
							System.out.print(" ("+new Date()+")");
						if (config.numbered)
							System.out.print(" ("+(i+1)+"/"+config.noPackets+")");
						if (config.rewrite) {
							System.out.print("     \r");
						} else {
							System.out.println();
						} //else
					} //synchronized
				} //if
				try {
					Thread.sleep(config.sleepTime);
				} catch(InterruptedException ie) {
					System.out.println("("+Thread.currentThread().getName()+") terminating with an interrupted exception");
				} //catch()
			} //for
		}  catch(RemoteException re) {
			System.out.println("terminating with a remote exception\n"+re.getMessage());
			System.exit(1);
		} //catch()
	} //run
} //class SendPing
//-----------------------------------------------------------
class ISODate {
   public static String getDate() {
      Calendar myCal = Calendar.getInstance(Locale.GERMANY);
      String result = new String();
      result += myCal.get(Calendar.YEAR);
      result +="-";

      if (1+ (int) myCal.get(Calendar.MONTH) < 10)
         result += "0"+ (1+ (int) myCal.get(Calendar.MONTH));
      else
         result += (1+ (int) myCal.get(Calendar.MONTH));

      result +="-";
      if ((int) myCal.get(Calendar.DATE) < 10)
         result += "0"+ myCal.get(Calendar.DATE);
      else
         result += myCal.get(Calendar.DATE);
      
      result += "T";
      int tmp = myCal.get(Calendar.HOUR_OF_DAY);
      if (tmp < 10) 
      	result += "0";
      result += tmp;
		result += ":";
      tmp = myCal.get(Calendar.MINUTE);   
      if (tmp < 10)
      	result += "0";
      result += tmp;
		result += ":";
      tmp = myCal.get(Calendar.SECOND);
      if (tmp < 10)
      	result += "0";
      result += tmp;
      
      tmp = myCal.get(Calendar.ZONE_OFFSET)/3600000;
      if ( tmp >0) 
      	result += "+";
      else
      	result += "-";
		if (tmp < 10 )
			result += "0";
		result += tmp;
		result += ":00";
		
      return result;
	} //readDate()
} //class ISODate
	