/*
 * $Id: HtmlExtractor.java,v 1.22 2007/10/30 13:48:17 marbobo Exp $
 *
 * Copyright (c) Siemens AG 2008 
 *           (c) Universität Paderborn 2008 
 * Licensed under EUPL 1.1. 
 *
 * $Log: HtmlExtractor.java,v $
 * Revision 1.22  2007/10/30 13:48:17  marbobo
 * + removed unnecessary statements
 *
 * Revision 1.21  2006/06/06 20:36:02  marbobo
 * + Mögliche NPE abgefangen
 *
 * Revision 1.20  2005/07/28 13:22:42  lawern
 * Abbruch des Renderings, wenn Speicher voll ist.
 *
 * Revision 1.19  2005/06/30 13:51:19  marbobo
 * + layout angepasst
 *
 * Revision 1.18  2005/04/01 00:12:10  marbobo
 * + UTF-8 Parsing vervollständigt und korrigiert
 *
 * Revision 1.17  2005/03/30 08:48:04  lawern
 * Fehler abgefangen.
 *
 * Revision 1.16  2005/03/24 12:57:04  lawern
 * Umstellung des TDF-Formats auf Version 1.1 in UTF-8-Kodierung.
 *
 * Revision 1.15  2004/11/30 11:57:23  lawern
 * *** empty log message ***
 *
 * Revision 1.14  2004/11/10 15:02:43  koppy
 * Projektweite Überarbeitung von Error-Logs, Stacktraces,..
 *
 * Revision 1.13  2004/07/26 11:44:48  lawern
 * Bugfixes
 *
 * Revision 1.12  2004/07/14 06:36:52  lawern
 * Fehler abgefangen
 *
 * Revision 1.11  2004/06/27 17:12:47  marbobo
 * + Quellcode maintenance
 *
 * Revision 1.10  2003/12/24 10:12:01  bigmac
 * Projektweite Import-Optimierung
 *
 * Revision 1.9  2003/09/22 07:35:41  lawern
 * Codierung von < und > in TDF-Dokumenten erweitert.
 *
 * Revision 1.8  2003/06/30 07:17:01  lawern
 * Dokumenttitel werden jetzt geholt.
 *
 * Revision 1.7  2003/06/26 15:15:22  lawern
 * XLS und PPT eingefügt und Konverter überarbeitet
 *
 * Revision 1.6  2003/06/06 10:57:22  lawern
 * Anpassungen an Retriever
 *
 * Revision 1.5  2003/04/01 08:08:44  lawern
 * PJA-Bibliothek wegen Problemen mit Tomcat ertstmal wieder entfernt
 *
 * Revision 1.4  2003/03/27 15:23:40  lawern
 * Optimierung
 *
 * Revision 1.3  2003/03/20 09:11:34  lawern
 * Konverter und Analyse überarbeitet
 *
 * Revision 1.2  2003/03/13 16:14:51  lawern
 * getTdfArray um Versionsnummer erweitert.
 *
 * Revision 1.1  2003/03/07 11:55:48  lawern
 * initiale Version
 *
 */
package vkc.converter;

import vkc.analyse.TextStyle;
import vkc.analyse.TextStyles;

import vkc.misc.VKCLogManager;

import vkc.util.VKCUtils;

//PJA-Bibliothek von http://www.eteks.com/pja/en/
//import com.eteks.awt.PJAImage;
import java.awt.AWTEvent;
import java.awt.Container;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

import java.util.ArrayList;

import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.LabelView;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;

/**
 * Diese Klasse extrahiert aus einem Html-Dokument den Text und gewichtet
 * diesen auf Basis der verwendeten typographischen Styles (Schriftgröße,
 * Kursivschrift usw.). Es wirkt sich sicherlich zeitsparend aus, wenn man
 * Objekte dieses Typs mehrfach verwendet.
 *
 * @author lawern
 */
public final class HtmlExtractor {
	public static final boolean debug = false;

	private static final int maxWaitMillis = 10000;

	private EditorPane editorPane = null;

	private View lastSpanView;

	private String lastSpanTags;

	private String lastElement;

	private int lastRowPosition;

	private int lastRowHeight;

	private boolean rendered;

	PagePropertyListener pageController = new PagePropertyListener();

	private ArrayList links = new ArrayList();

	private TextStyles elements;

	private String documentTitle;

	private String userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)";

	public HtmlExtractor() {
		//if (editorPane == null)
		build();

		//Link-Liste löschen
		links.clear();

		//Style-Statistik neu anlegen
		elements = new TextStyles();

		documentTitle = null;
	}

	/**
	 * Initialisiert den HtmlExtractor für ein neues Html-Dokument. Eine
	 * bestehende  Statistik bleibt erhalten.
	 */
	private void init() {
		lastSpanView = null;
		lastSpanTags = null;
		lastElement = "";

		lastRowPosition = -1;
		lastRowHeight = 0;

		rendered = false;
	}

	/**
	 * Methode zum initialisieren einer unsichtbaren EditorPane
	 */
	private void build() {
		//Editor Pane in der Größe eines fullscreen IE anlegen
		editorPane = new EditorPane();
		editorPane.setSize(1004, 606);

		try {
			//eigenen Basis-StyleSheet laden
			//            HTMLEditorKit editorKit = (HTMLEditorKit) editorPane.getEditorKitForContentType(
			//                    "text/html");
			EditorKit editorKit = new EditorKit();

			StyleSheet ss = new StyleSheet() {
				public Style getRule(String rule) {
					//System.out.println("getRule: "+rule);
					return super.getRule(rule.toLowerCase());
				}

				public AttributeSet getDeclaration(String decl) {
					//System.out.println("getDeclaration: "+decl);
					return super.getDeclaration(decl);
				}
			};

			try {
				InputStreamReader reader = new InputStreamReader(HtmlExtractor.class.getResourceAsStream("HTMLExtractor.css"));
				ss.loadRules(reader, HtmlExtractor.class.getResource("HTMLExtractor.css"));
			} catch (IOException e) {
				VKCLogManager.errorLog(this, "2", "Basic StyleSheet HTMLExtractor.css doesn't exist.", e);
			}

			editorKit.setStyleSheet(ss);
			editorPane.setEditorKit(editorKit);
		} catch (Throwable ioe) {
			if (debug) {
				ioe.printStackTrace();
			}

			return;
		}
	}

	/**
	 * Diese Methode rendert die übergebene URL auf eine unsichtbare
	 * EditorPane. Sie kann mehrfach hintereinander mit verschiedenen
	 * Dokumenten aufgerufen werden die in ein Super-TDF-Dokument sollen.
	 *
	 * @param file Zu rendernde Datei oder URL.
	 */
	public void render(String file) {
		//HtmlExtractor für ein neues Html-Dokument initialisieren
		init();

		try {
			if (debug) {
				System.out.println("Dokument wird geladen ...");
			}

			//Seite laden
			editorPane.setPage(file);
		} catch (Throwable ioe) {
			//Seite kann nicht geladen werden.
			VKCLogManager.errorLog(this, "1", "unable to open: " + file, ioe);

			return;
		}

		try {
			editorPane.render();
		} catch (Exception e) {
			editorPane = null;
			VKCLogManager.errorLog(this, "4", "Rendering Problem!", e);
		}

		if (editorPane != null) {
			if (debug) {
				System.out.println("Dokument fertig gerendert!");
			}

			HTMLDocument doc = (HTMLDocument) editorPane.getDocument();

			//Titel holen
			if (documentTitle == null) {
				documentTitle = (String) doc.getProperty(Document.TitleProperty);
			}

			//Links aus dem Dokument holen
			extractLinks(doc);

			rendered = true;
		} else {
			rendered = false;
		}
	}

	/**
	 * Gibt eine Liste zurueck, der alle HREFs einer Seite enthaelt.
	 *
	 * @return Link-Vektor
	 */
	public ArrayList getLinks() {
		if (rendered) {
			return links;
		}

		return null;
	}

	/**
	 * Liefert eine Liste mit Objekten vom Typ TextStyle zurück, welche das
	 * gerenderte HTML-Dokument beschreiben.
	 *
	 * @return Liste mit Objekten vom Typ TextStyle
	 */
	public TextStyles getHtmlElements() {
		if (rendered) {
			return elements;
		}

		return null;
	}

	/**
	 * Extrahiert aus dem uebergebenen HTMLDocument die Links und schreibt sie
	 * in die Link-Liste.
	 *
	 * @param doc HTMLDocument aus dem die Links extrahiert werden sollen.
	 */
	private void extractLinks(HTMLDocument doc) {
		String url = null;

		HTMLDocument.Iterator x = doc.getIterator(HTML.Tag.A);

		// Loop through the Iterator
		while (x.isValid()) {
			// Create an AttributeSet that contains all of the attributes of the anchor
			AttributeSet as = x.getAttributes();

			// Not all achors have hrefs in them; only process the ones that do
			if (as.isDefined(HTML.Attribute.HREF)) {
				// Process the value of the href
				try {
					url = (String) as.getAttribute(HTML.Attribute.HREF);

					//Links absolut machen
					url = (new URL(doc.getBase(), url)).toString();
					links.add(url);
				} catch (java.net.MalformedURLException mue) {
				}
			}

			// Loop to the next anchor
			x.next();
		}
	}

	/**
	 * Rekursive Methode zum analysieren eines uebergebenen View. In jeder
	 * Rekursion wird der  tags-String um das Tag der aktuellen View
	 * erweitert. Ergebnisse werden im Elements- Vector in Form von
	 * TextStyle-Objekten abgespeichert.
	 *
	 * @param view View Zu analysierender View
	 * @param tags Tag-Historie des aktuellen View
	 */
	private void analyseView(View view, String tags) {
		Rectangle rect = null;
		int x;
		int y;
		int height;

		boolean separated = false;
		boolean newLine = false;

		Element currentElement = (view.getElement());
		String name = currentElement.getName();

		//Beenden, falls noframes-Bereich. Darf nicht geparst werden, da der dortige HTML-Code
		//nicht gerendert wird. Wuerde zu Fehlern fuehren.
		if (name.equals("noframes")) {
			return;
		}

		String newTags = tags + "," + name;
		int childViews = view.getViewCount();

		//Text holen
		String text = "";

		try {
			text = currentElement.getDocument().getText(
					currentElement.getStartOffset(),
					Math.min(currentElement.getEndOffset(), 
					currentElement.getDocument().getLength()) - currentElement.getStartOffset());

			//ASCII #160 durch Space ersetzen
			text = text.replace((char) 160, ' ').trim();
		} catch (BadLocationException ble) {
		}

		//(Manchmal treten gleiche Elemente mehrfach auf, warum?)
		if ((childViews == 0) && (!lastElement.equals(currentElement.toString()))) {
			lastElement = currentElement.toString();

			//Workarround fuer falsch interpretiertes <span>-Tag
			if (name.equals("span")) {
				lastSpanView = view;
				lastSpanTags = tags;
			}

			AttributeSet attributes = view.getAttributes();
			AttributeSet spanAttributes;

			//Wenn ein <span>-Tag das aktuelle Blatt umschliesst
			//dann ist das <span>-Tag faelschlicherweise ein Nachbar des aktuellen Blattes.
			//In diesem Fall werden die Attribute von dort geholt.
			if ((lastSpanView != null) && (lastSpanTags.equals(tags))) {
				spanAttributes = lastSpanView.getAttributes();
			} else {
				spanAttributes = attributes;
			}

			//Attribute holen, wenn lokal gesetzt, lokales Attribut verwenden, ansonsten nachsehen
			//ob <span>-Tag existiert und das Attribut dort gesetzt ist.
			int fontSize = -1;
			boolean bold = false;
			boolean italic = false;
			boolean underlined = false;

			try {
				//Font-Groesse ermitteln
				if (spanAttributes.isDefined(StyleConstants.CharacterConstants.Size)) {
					fontSize = StyleConstants.getFontSize(spanAttributes);
				} else {
					fontSize = StyleConstants.getFontSize(attributes);
				}

				//fett-gedruckt?
				if (spanAttributes.isDefined(StyleConstants.CharacterConstants.Bold)) {
					bold = StyleConstants.isBold(spanAttributes);
				} else {
					bold = StyleConstants.isBold(attributes);
				}

				//kursiv-gedruckt?
				if (spanAttributes.isDefined(StyleConstants.CharacterConstants.Italic)) {
					italic = StyleConstants.isItalic(spanAttributes);
				} else {
					italic = StyleConstants.isItalic(attributes);
				}

				//unterstrichen-gedruckt?
				if (spanAttributes.isDefined(StyleConstants.CharacterConstants.Underline)) {
					underlined = StyleConstants.isUnderline(spanAttributes);
				} else {
					underlined = StyleConstants.isUnderline(attributes);
				}
			} catch (Exception e) {
			}

			//Element in elements-Liste eintragen
			if (text.length() > 0) {
				//Position und Hoehe des aktuellen Elements bestimmen
				try {
					rect = editorPane.modelToView(currentElement.getStartOffset()).getBounds();
					x = rect.x;
					y = rect.y;
					height = rect.height;
				} catch (Exception e) {
					x = -1;
					y = -1;
					height = -1;
				}

				//neue Zeile?
				if (y != lastRowPosition) {
					newLine = true;

					//neuer Absatz? Wenn Abstand groesser als 1.2-facher Zeilenabstand!
					if ((lastRowPosition + (lastRowHeight * 1.2)) < y) {
						separated = true;
					} else {
						separated = false;
					}

					lastRowPosition = y;
					lastRowHeight = height;
				} else {
					if (y != -1) {
						newLine = false;
					} else {
						newLine = true;
						separated = true;
					}
				}

				elements.add(new TextStyle(fontSize, bold, italic, underlined, text, x, y, height, newLine, separated));

				//Debug-Ausgabe
				if (debug) {
					System.out.println(newTags + ",x:" + x + ",y:" + y
							+ ",height:" + height + ",size:" + fontSize
							+ ",bold:" + bold + ",text:" + text);
				}

				//aufräumen
				currentElement = null;
				name = null;
				attributes = null;
				spanAttributes = null;
				rect = null;
			}
		} else {
			//aufräumen
			currentElement = null;
			name = null;

			//Soehne bearbeiten
			for (int i = 0; i < childViews; i++) {
				if (view.getView(i) != null) {
					analyseView(view.getView(i), newTags);
				}
			}
		}
	}

	/**
	 * Startet die Analyse einer bereits gerenderten Html-Seite. Die Ergebnisse
	 * finden sich danach im Elements-Vector.
	 */
	public void analyse() {
		//try{Thread.sleep(10000);} catch (InterruptedException e){}
		//System.out.println(editorPane);
		if (editorPane != null) {
			View rootView = editorPane.getUI().getRootView(editorPane);
			analyseView(rootView, "html");

			//System.out.println("Elements:");
			//analyseElement(rootView.getElement(), "html");
			//Sind weitere Komponenten in der JEditorPane?
			if (editorPane.getComponentCount() > 0) {
				analyseComponent(editorPane, "frame");
			}
		}
	}

	/**
	 * Besteht eine Html-Seite aus Frames wird diese Methode aufgerufen um
	 * diese zu analysieren.
	 *
	 * @param component auf Frames zu analysierende Komponente
	 * @param tag Tag-Historie der aktuellen Komponente
	 */
	void analyseComponent(JComponent component, String tag) {
		//System.out.println("analyseComponent: " + tag +
		//    "component.getComponentCount():" + component.getComponentCount());
		try {
			for (int i = 0; i < component.getComponentCount(); i++) {
				Container container = (Container) component.getComponent(i);

				//System.out.println(container.getComponentCount());
				//System.out.println(container.getComponent(0).getClass().getName());
				String componentName = container.getComponent(0).getClass().getName();

				if (componentName.equals("javax.swing.JScrollPane")) {
					//Frame-Behandlung
					JScrollPane scrollPane = (JScrollPane) container.getComponent(0);
					JViewport viewport = (JViewport) scrollPane.getComponent(0);
					JEditorPane tmpEditorPane = (JEditorPane) viewport.getComponent(0);
					View frameRootView = tmpEditorPane.getUI().getRootView(tmpEditorPane);

					//Frame analysieren
					if (frameRootView != null) {
						analyseView(frameRootView, tag);
						extractLinks((HTMLDocument) tmpEditorPane.getDocument());
					}

					//enthaelt das Frame weitere Frames die analysiert werden muessen?
					if (tmpEditorPane.getComponentCount() > 0) {
						analyseComponent(tmpEditorPane, tag + "," + "frame");
					}
				} else {
					//Andere Komponenten analysieren
					if (debug) {
						System.out.println("(2) Komponente nicht behandelt: " + componentName);
					}
				}
			}
		} catch (Exception e) {
			VKCLogManager.errorLog(this, "5", "Unknown error", e);
		}
	}

	/**
	 * Schreibt eine Datei mit Gewichtungsmarken in ein byte[]
	 *
	 * @return Array, welches die Datei enthält
	 */
	public byte[] getTdfArray() {
		ByteArrayOutputStream array = new ByteArrayOutputStream();
		PrintStream out;
		
		try {
			out = new PrintStream(array, false, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}

		if (rendered) {
			TextStyle actStyle = null;

			out.println("<V:1.1>");

			String docTitle = VKCUtils.tdfEncode(documentTitle);
			out.print("<T:\u200E" + docTitle + "\u200E>");

			String text;

			for (int i = 0; i < elements.size(); i++) {
				actStyle = (TextStyle) elements.get(i);

				//neue Zeile?
				if (actStyle.newLine) {
					out.println("");

					//neuer Absatz?
					if (actStyle.separated) {
						out.println("<P>");
					}
				}

				text = VKCUtils.tdfEncode(actStyle.text);
				out.print("<S:" + actStyle.weight + ":\u200E" + text + "\u200E>");
			}
		}

		return array.toByteArray();
	}

	/**
	 * Setzt das User-Agent-Property im Http-Header
	 *
	 * @param agent String der als User-Agent-Property im Http-Header gesendet
	 *        wird.
	 */
	public void setUserAgent(String agent) {
		userAgent = agent;
	}

	/**
	 * Gibt den Titel des aktuell gerenderten Dokuments zurück
	 *
	 * @return Titel des Dokuments
	 */
	public String getTitle() {
		if (rendered) {
			return documentTitle;
		}

		return null;
	}

	public static void main(String[] args) {
		VKCLogManager.init("c:/temp/test_", ".log");
		VKCLogManager.setEnableLogging(true);
		VKCLogManager.setLogToConsole(true);

		HtmlExtractor extractor = new HtmlExtractor();
		extractor.render(args[0]);
		extractor.analyse();

		System.exit(0);

		//org.jutils.runtime.ThreadLister.main(new String[1]);
	}

	/**
	 * JEditorPane, die es erlaubt unsichtbar zu rendern.
	 *
	 * @author lawern
	 */
	final class EditorPane extends JEditorPane {
		Object monitor = new Object();

		public EditorPane() {
			//super();
			disableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK);
			setEditable(false);
			setEnabled(false);
			setLayout(null); // layout is managed by View hierarchy
			setDoubleBuffered(false);
			updateUI();
		}

		protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
			super.firePropertyChange(propertyName, oldValue, newValue);

			//System.out.println("Property: " + propertyName);
			//wenn die Seite fertig geladen ist kommt ein page-Property, dann Renderer anstossen
			if (propertyName.equals("page")) {
				synchronized (monitor) {
					monitor.notify();

					if (debug) {
						System.out.println("Seite vollständig geladen");
					}
				}
			}
		}

		void setPageController(JComponent component) {
			Container container;
			String componentName;

			for (int i = 0; i < component.getComponentCount(); i++) {
				container = (Container) component.getComponent(i);
				componentName = container.getComponent(0).getClass().getName();

				if (componentName.equals("javax.swing.JScrollPane")) {
					//Frame-Behandlung
					JScrollPane scrollPane = (JScrollPane) container.getComponent(0);
					JViewport viewport = (JViewport) scrollPane.getComponent(0);

					try {
						JEditorPane tmpEditorPane = (JEditorPane) viewport.getComponent(0);
						tmpEditorPane.addPropertyChangeListener(pageController);
						pageController.inc();

						//enthaelt das Frame weitere Frames die kontrolliert werden muessen?
						if (tmpEditorPane.getComponentCount() > 0) {
							setPageController(tmpEditorPane);
						}
					} catch (java.lang.ClassCastException cce) {
						if (debug) {
							System.out.println("Unerwartete Komponente nicht behandelt: " + componentName);
						}
					}
				} else {
					//Andere Komponenten behandeln
					if (debug) {
						System.out.println("Komponente nicht behandelt: " + componentName);
					}
				}
			}
		}

		public void render() {
			synchronized (monitor) {
				try {
					//maximal maxWaitMillis warten bis Seite vollstaendig geladen
					monitor.wait(maxWaitMillis);
					setPageController(this);
					pageController.waitForRenderer();

					View view = this.getUI().getRootView(this);

					BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
					Graphics2D g = image.createGraphics();

					Rectangle rect = new Rectangle();

					rect.setRect(0, 0, 1, 1);
					g.setClip(0, 0, 1, 1);

					if (debug) {
						System.out.println("Rendering des Dokuments ...");
					}

					//Graphics g2 = new PJAImage(1, 1).getGraphics();
					try {
						//hier erfolgt das Rendern des Dokuments
						//view.paint(g2, rect);
						view.paint(g, rect);
					} catch (OutOfMemoryError oome) {
						VKCLogManager.errorLog(this, "3", "Document too large!", oome);
					} catch (Exception e) {
						VKCLogManager.errorLog(this, "6", "Rendering error!", e);
					}
				} catch (java.lang.InterruptedException ie) {
					if (debug) {
						System.out.println("Zeitüberschreitung beim Rendering");
					}
				}
			}
		}

		/**
		 * Fetches a stream for the given URL, which is about to  be loaded by
		 * the <code>setPage</code> method.  By default, this simply opens the
		 * URL and returns the stream.  This can be reimplemented to do useful
		 * things like fetch the stream from a cache, monitor the progress of
		 * the stream, etc.
		 *
		 * <p>
		 * This method is expected to have the the side effect of establishing
		 * the content type, and therefore setting the appropriate
		 * <code>EditorKit</code> to use for loading the stream.
		 * </p>
		 *
		 * <p>
		 * If this the stream was an http connection, redirects will be
		 * followed and the resulting URL will be set as the
		 * <code>Document.StreamDescriptionProperty</code> so that relative
		 * URL's can be properly resolved.
		 * </p>
		 *
		 * @param page the URL of the page
		 */
		protected InputStream getStream(URL page) throws IOException {
			if (debug) {
				System.out.println("getStream: " + page.toString());
			}

			URLConnection conn = page.openConnection();

			if (conn instanceof HttpURLConnection) {
				HttpURLConnection hconn = (HttpURLConnection) conn;
				hconn.setRequestProperty("User-Agent", userAgent);
				hconn.setInstanceFollowRedirects(false);

				int response = hconn.getResponseCode();
				boolean redirect = ((response >= 300) && (response <= 399));

				/*
				 * In the case of a redirect, we want to actually change the URL
				 * that was input to the new, redirected URL
				 */
				if (redirect) {
					String loc = conn.getHeaderField("Location");

					if (loc.startsWith("http", 0)) {
						page = new URL(loc);
					} else {
						page = new URL(page, loc);
					}

					return getStream(page);
				}
			}

			InputStream in = conn.getInputStream();

			//Hier muss ein Close abgefangen werden, welches im JDK versehentlich zuviel gemacht wird.
			return new VKCInputStream(in);
		}
	}

	/**
	 * Eigener InputStream, der einen JDK-Bug abfängt. Wird nur benoetigt so
	 * lange der Close-Bug im JDK 1.4.0_01 exisiert!
	 *
	 * @author lawern
	 */
	final class VKCInputStream extends InputStream {
		InputStream is;

		VKCInputStream(InputStream _is) {
			is = _is;
		}

		public int available() throws IOException {
			return is.available();
		}

		public void close() throws IOException {
			try {
				is.close();
			} catch (NullPointerException npe) {
				//Es kann passieren, dass das JDK das Close einmal zuviel macht. Die daraus
				//resultierende Exception wird hier abgefangen.
				//JDC-BUG:4533243 "Closing a keep alive stream gives NullPointerException"
				if (HtmlExtractor.debug) {
					System.out.println("Das JDK macht noch immer ein Close zuviel!");
				}
			}
		}

		public void mark(int readlimit) {
			is.mark(readlimit);
		}

		public boolean markSupported() {
			return is.markSupported();
		}

		public int read() throws IOException {
			return is.read();
		}

		public int read(byte[] b) throws IOException {
			return is.read(b);
		}

		public int read(byte[] b, int off, int len) throws IOException {
			return is.read(b, off, len);
		}

		public void reset() throws IOException {
			is.reset();
		}

		public long skip(long n) throws IOException {
			return is.skip(n);
		}
	}

	/**
	 * Dient zum ueberpruefen ob alle HTML-Komponenten schon gerendert sind.
	 *
	 * @author lawern
	 */
	final class PagePropertyListener implements PropertyChangeListener {
		private int counter = 0;

		private Object monitor = new Object();

		/**
		 * Vermindert immer beim Eintreffen des page-Properties den Zaehler der
		 * noch nicht gerenderten Komponenten.
		 */
		public void propertyChange(PropertyChangeEvent evt) {
			//System.out.println("PropertyChangeEvent: "+evt.getPropertyName());
			if (evt.getPropertyName().equals("page")) {
				counter--;

				if (counter == 0) {
					synchronized (monitor) {
						monitor.notify();
					}
				}
			}
		}

		/**
		 * Erhoeht die Anzahl der zu kontrollierenden Komponenten um 1.
		 */
		public void inc() {
			counter++;
		}

		/**
		 * Wartet bis der Zaehler der noch zu Rendernden Komponeten auf 0 ist.
		 */
		public void waitForRenderer() {
			synchronized (monitor) {
				if (counter > 0) {
					try {
						monitor.wait(maxWaitMillis);
					} catch (java.lang.InterruptedException ie) {
					}
				}
			}
		}
	}

	/**
	 * Diese Klasse erzeugt einen HTMLEditorKit, der keine Bilder anzeigt.
	 *
	 * @author lawern
	 */
	class EditorKit extends HTMLEditorKit {
		public ViewFactory getViewFactory() {
			return new HTMLViewFactory();
		}

		/**
		 * Erzeugt eine HTMLFactory, die bei einem IMG-Tag anstatt eines
		 * ImageView eine LabelView liefert.
		 *
		 * @author lawern
		 */
		class HTMLViewFactory extends HTMLEditorKit.HTMLFactory {
			Runtime rTime = Runtime.getRuntime();

			boolean full = false;

			public View create(Element elem) {
				if (!full && !memoryFull()) {
					Object o = elem.getAttributes().getAttribute(StyleConstants.NameAttribute);

					if (o instanceof HTML.Tag) {
						HTML.Tag kind = (HTML.Tag) o;

						if (kind == HTML.Tag.IMG) {
							return new LabelView(elem);
						}

						return super.create(elem);
					}

					return super.create(elem);
				}

				//Abbrechen, wenn Speicher voll ist.
				full = true;

				return null;
			}

			/**
			 * Liefert <code>true</code> zurück, wenn der Speicher voll ist.
			 */
			private boolean memoryFull() {
				long freeMemory = rTime.freeMemory();

				if (freeMemory < 10000000) {
					//Gibts evtl. noch nicht allokierten Speicher?
					freeMemory = rTime.maxMemory() - rTime.totalMemory() + freeMemory;

					if (freeMemory < 10000000) {
						//versuchen ob der GarbageCollector noch etwas erreichen kann.
						rTime.gc();
						freeMemory = rTime.maxMemory() - rTime.totalMemory() + rTime.freeMemory();

						if (freeMemory < 10000000) {
							VKCLogManager.errorLog(this, "7", "To low memory for full rendering!");
							
							return true;
						}
					}
				}

				return false;
			}
		}
	}
}