//Gruppe 01 (Übung 4)
//Aufgabe 4-3

package uebung04.aufgabe03;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashSet;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLEditorKit.ParserCallback;
import javax.swing.text.html.parser.ParserDelegator;

/**
* Sucht in einer HTML-Ressource nach eingebetten Objekten und summiert die Dateigrößen auf,
* wobei mehrfach verlinkte Objekte nur einmal gezählt werden.
* Bei Framesets werden sämtliche Frames ausgewertet, in mehreren Frames dargestellte Seiten
* werden nur einfach gezählt.
* 
* @author Tilman Walther
* @version 2.1
*/
public class SiteSize extends ParserCallback {

	private URL				url;							// die URL der HTML-Ressource
	private URLConnection	connection;						// Verbindung zur HTML-Ressource
	private HashSet			objects = null;					// speichert URLs der bereits gezählten Objekten
	private long			size = 0;						// die Größe aller gezählten Objekte
	
	private ParserDelegator	parser = new ParserDelegator();	// parst die Ressource und ruft handleSimpleTag() auf
		
	/**
	 * Parses the resource behind the given URL and measures the size.
	 * If the URL points to a non-HTML resource the size will be zero.
	 * @param url the URL for the resource to parse
	 * @return the size of the website including all embedded objects
	 * @throws IOException if an I/O error occurs
	 */
	public long getSize(URL url) throws IOException {
		
		/* Für Testzwecke: Durchleitungsproxy einstellen /
		
		java.util.Properties props = System.getProperties();
		props.put("proxySet", "true");
		props.put("proxyHost", "127.0.0.1");
		props.put("proxyPort", "10000");
		
		/* --------------------------------------------- */
		
		System.out.println("\nparsing "+url);
		
		this.url = url;
		connection = url.openConnection();
		
		// Daten vom Server im "identity"-Format anfordern
		connection.addRequestProperty("Accept-Encoding", "identity");
		
		// TODO für Testzwecke:
		//connection.addRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12");
		
		connection.connect();
		
		//System.out.println("Transfer-Encoding: "+connection.getHeaderField("transfer-encoding")+"\n");
		
		// falls objects nicht bereits durch den privaten Konstruktor
		// inititalisiert wurde, neues HashSet erzeugen
		if (objects == null) objects = new HashSet();
		
		// auf HTML-Inhalt prüfen
		if (connection.getContentType().startsWith("text/html")) {
			// Größe der Seite zur Gesamtgröße addieren
			addSize(url);
			
			// eingebettete Objekte mit Parser finden/addieren
			BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
			parser.parse(br, this, true);
		}
		
		// Gesamtgröße zurückgeben
		return size;
	}
	
	/**
	 * This private method is used for recursive processing of framesets.
	 * In order not to count linked objects twice, the HashSet 'objects' is passed.
	 * @param url the URL for the actual frame
	 * @param objects the HashSet containing all objects that have already been counted
	 * @return the size if the subframe
	 * @throws IOException if an I/O error occurs
	 */
	private long getSize(URL url, HashSet objects) throws IOException {
		this.objects = objects;
		return getSize(url);
	}
	
	/**
	 * Overrides javax.swing.text.html.HTMLEditorKit.ParserCallback.handleSimpleTag() which is called by the ParserDelegator
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleSimpleTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet, int)
	 */
	public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
		
		String objLink = null;
		
		try {
			if (t == HTML.Tag.FRAME) {
				// leider trifft man öfter auf dummy-frames (Dreamweaver!), deswegen
				// fehlt ab und zu die Quelle
				if (a.getAttribute(HTML.Attribute.SRC) != null) {
					// absuluten Pfad zur Ressource nachvollziehen, um mehrfache Zählung zu vermeiden
					URL frameUrl = generateURL(a.getAttribute(HTML.Attribute.SRC).toString());
					
					// Frame von neuem SiteSize-Objekt parsen lassen
					SiteSize frameSize = new SiteSize();
					size += frameSize.getSize(frameUrl, objects);
				}
			} else {
				if (t == HTML.Tag.APPLET) {
					// Applets können direkt via CODE-Attribut oder als Archiv verlinkt sein
					if (a.getAttribute(HTML.Attribute.ARCHIVE) != null) {
						objLink = a.getAttribute(HTML.Attribute.ARCHIVE).toString();
					} else {
						objLink = a.getAttribute(HTML.Attribute.CODE).toString();
					}
				} else if (t == HTML.Tag.IMG) {
					objLink = a.getAttribute(HTML.Attribute.SRC).toString();
				} else if (t == HTML.Tag.OBJECT) {
					objLink = a.getAttribute(HTML.Attribute.DATA).toString();
				} else if (t == HTML.Tag.LINK) {
					objLink = a.getAttribute(HTML.Attribute.HREF).toString();
				}
				
				if (objLink != null) {
					try {
						// absuluten Pfad zur Ressource nachvollziehen, um mehrfache Zählung zu vermeiden
						addSize(generateURL(objLink));
					} catch (MalformedURLException mue) {
						System.out.println("MalformedURLException on position " + pos + ": '" + objLink + "'");
					}
				}
			}	
		} catch (IOException ioe) {
			System.out.println("IOException (" + pos + "): Could not connect to linked object. ("+objLink+")");
		}
	}
	
	/**
	 * Overrides javax.swing.text.html.HTMLEditorKit.ParserCallback.handleStartTag() which is called by the ParserDelegator.
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleStartTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet, int)
	 */
	public void handleStartTag(Tag t, MutableAttributeSet a, int pos) {
		// Da APPLET- und OBJECT-Tags auch als StartTags vorkommen können, wird das der Aufruf
		// in diesem Fall an handleSimpleTag() weitergegeben
		if ((t == HTML.Tag.APPLET) || (t == HTML.Tag.OBJECT)) handleSimpleTag(t, a, pos);
	}
	
	/**
	 * Adds the size of the resource that the given URLConnection points to if it has not already been counted
	 * @param connection an URLConnection to a resource
	 * @throws IOException if an I/O error occurs
	 */
	private void addSize(URL url) throws IOException {
		
		URLConnection resConn = url.openConnection();
		
		// TODO für Testzwecke:
		//resConn.addRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; de-DE; rv:1.7) Gecko/20040803 Firefox/0.9.3");
		
		resConn.connect();
		
		String resUrlStr = url.toString();
		
		if (!objects.contains(resUrlStr)) {
			objects.add(resUrlStr);
			int resLength = resConn.getContentLength();
			
			if (resLength >= 0) {
				size += resLength;
			}
			else {
				// da der Content-Length Header nicht übermittelt wurde, muss zur
				// Messung die Ressource übertragen werden
								
				int bytesRead;
				byte[] buffer = new byte[1024];
				BufferedInputStream input = new BufferedInputStream(resConn.getInputStream());
				
				resLength = 0;
				
				while ((bytesRead = input.read(buffer)) >= 0) {
					resLength += bytesRead;
				}
				
				size += resLength;
			}
			
			System.out.println("added '" + resUrlStr + "' (" + resLength + " bytes)");
		}
	}
	
	/**
	 * Generates the URL for a (relative) link extracted from a tag attribute
	 * @param link a resource link from a tag attribute
	 * @return the URL to the resource
	 * @throws MalformedURLException
	 */
	// TODO wahrscheinlich lässt sich diese Methode (zumindest teilw.) durch URI.relativize() ersetzen
	private URL generateURL(String link) throws MalformedURLException {
		
		if (link.startsWith("//")) link = "http:" + link;
		
		String resUrlStr = null;

		if (link.indexOf("://") > 0) {
			resUrlStr = new String(link);
		} else if (link.startsWith("/")) {
			resUrlStr = new String(url.getProtocol() + "://" + url.getAuthority() + link);
		} else {
			String path = url.getPath();
			if (path.length() > 0) path = path.substring(0, path.lastIndexOf("/"));
			resUrlStr= new String(url.getProtocol() + "://" + url.getAuthority() + path + "/" + link);
		}
		
		int i, j;
		while ( ((i = resUrlStr.indexOf("../")) > -1) && ((j = resUrlStr.indexOf("/", i+3)) > -1) ){
			resUrlStr = resUrlStr.substring(0,i) + resUrlStr.substring(j+1);
		}
		
		//new URL(URLEncoder.encode(resUrlStr, "UTF-8"));
		
		return new URL(resUrlStr);
	}
	
	public static void main(String[] args) {
		try {
			String urlString;
			if (args.length == 0) {
				// URL von der Kommandozeile lesen
				BufferedReader din = new BufferedReader(new InputStreamReader(System.in));
				System.out.print("URL: ");
				urlString = din.readLine();
			} else {
				urlString = args[0];
			}
			
			// URL erzeugen
			URL url = new URL(urlString);
			
			// SiteSize-Objekt erzeugen
			SiteSize siteSize = new SiteSize();
			
			// Brutto-Größe messen und ausgeben
			System.out.println("\nSize of content: " + siteSize.getSize(url) + " bytes");
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}