/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util.gui;
import java.awt.Rectangle;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.concurrent.Executor;
import javax.swing.JEditorPane;
import javax.swing.UIManager;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.ChangedCharSetException;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.html.HTMLDocument;
/**
* A modified JEditorPane; it makes possible async loading for html documents without using setPage(URL) (in most cases the content will be provided via
* InputStream)
*
* @author Paul
*/
public class FixedJEditorPane extends JEditorPane
{
private final Executor exe;
public static final String CHARSET_DIRECTIVE = "charset"; //$NON-NLS-1$
public FixedJEditorPane(Executor exe)
{
super();
this.exe = exe;
}
/**
* a FilterInputStream used by the loader
*/
private PageStream loading;
@Override
public void read(InputStream in, Object desc) throws IOException
{
if (desc == null)
{
read(in);
}
else
{
EditorKit kit = this.getEditorKit();
if (desc instanceof HTMLDocument && kit instanceof FixedHTMLEditorKit)
{
HTMLDocument hdoc = (HTMLDocument)desc;
Document doc = getDocument();
try
{
doc.remove(0, doc.getLength());
}
catch (BadLocationException e)
{
throw new RuntimeException("Can't set document:", e); //$NON-NLS-1$
}
setDocument(hdoc);
super.read(in, hdoc);
/** I don't need it now. I just keep it for further reviews */
// firePropertyChange("document", doc, hdoc);
}
else
{
String charset = (String)getClientProperty(CHARSET_DIRECTIVE);
Reader r = (charset != null) ? new InputStreamReader(in, charset) : new InputStreamReader(in);
super.read(r, desc);
}
}
}
/**
* the method does not override the <code>super.read</code> method... it's only used by the async read method
*
* @param in
* @param doc
* @throws IOException
*/
private void read(InputStream in, Document doc) throws IOException
{
try
{
EditorKit kit = this.getEditorKit();
putClientProperty(CHARSET_DIRECTIVE, "UTF-8");
String charset = (String)getClientProperty(CHARSET_DIRECTIVE);
Reader reader = (charset != null) ? new InputStreamReader(in, charset) : new InputStreamReader(in);
doc.putProperty("IgnoreCharsetDirective", new Boolean(true));
kit.read(reader, doc, 0);
}
catch (BadLocationException e)
{
throw new IOException(e.getMessage());
}
catch (ChangedCharSetException ccse)
{
String charSetSpec = ccse.getCharSetSpec();
if (ccse.keyEqualsCharSet())
{
putClientProperty(CHARSET_DIRECTIVE, charSetSpec);
}
else
{
setCharsetFromContentTypeParameters(charSetSpec);
}
in.reset();
try
{
doc.remove(0, doc.getLength());
}
catch (BadLocationException e)
{
}
doc.putProperty("IgnoreCharsetDirective", new Boolean(true)); //$NON-NLS-1$
read(in, doc);
}
}
private void setCharsetFromContentTypeParameters(String paramlist)
{
String charset = null;
try
{
int semi = paramlist.indexOf(';');
if (semi > -1 && semi < paramlist.length() - 1)
{
paramlist = paramlist.substring(semi + 1);
}
if (paramlist.length() > 0)
{
// try to find charset directive from header
HeaderParser hdrParser = new HeaderParser(paramlist);
charset = hdrParser.findValue(CHARSET_DIRECTIVE);
if (charset != null)
{
putClientProperty(CHARSET_DIRECTIVE, charset);
}
}
}
catch (Exception e)
{
}
}
/**
* makes an asynchronous read; the method is "derived" from <code>JEditorPane.setPage</code>
*
* @param in
*/
public void read(InputStream in) throws IOException
{
scrollRectToVisible(new Rectangle(0, 0, 1, 1));
if (getEditorKit() != null)
{
Document doc = getEditorKit().createDefaultDocument();
doc.putProperty(Document.StreamDescriptionProperty, in);
synchronized (this)
{
if (loading != null)
{
/** we are loading asynchronously, so we need to cancel the old stream */
loading.cancel();
loading = null;
}
}
/**
* Asynchronous Loading: first setDocument() then create thread that loads content from input stream (via parser) see also
* AbstractDocument.readObject + readLock/readUnlock
*
* asynchronous loading is done by setting doc.setAsynchronousLoadPriority(priority_value) in <code>EditorKit.createDefaultDocument</code> where 1 <=
* priority_value <= 10
*/
if (doc instanceof AbstractDocument)
{
AbstractDocument adoc = (AbstractDocument)doc;
int p = adoc.getAsynchronousLoadPriority();
if (p >= Thread.MIN_PRIORITY)
{
setDocument(doc);
synchronized (this)
{
loading = new PageStream(in);
PageLoader pl = new PageLoader(doc, loading);
exe.execute(pl);
}
return;
}
}
/** Synchronous Loading: see how setDocument is called after reading the stream: that makes the difference */
/** I keep the original implementation of the read */
super.read(in, doc);
setDocument(doc);
}
}
/**
* A modified page loader for asynchronous document loading
*
* @author Paul
*/
class PageLoader implements Runnable
{
/** The stream to load the document with */
InputStream in;
/**
* The Document instance to load into. This is cached in case a new Document is created between the time the thread this is created and run.
*/
Document doc;
/**
* Construct an asynchronous page loader.
*/
PageLoader(Document doc, InputStream in)
{
this.in = in;
this.doc = doc;
}
/**
* Try to load the document, then scroll the view to the reference (if specified). When done, fire a page property change event.
*/
public void run()
{
try
{
read(in, doc);
synchronized (FixedJEditorPane.this)
{
loading = null;
}
}
catch (IOException ioe)
{
UIManager.getLookAndFeel().provideErrorFeedback(FixedJEditorPane.this);
}
}
}
static class PageStream extends FilterInputStream
{
boolean canceled;
public PageStream(InputStream i)
{
super(i);
canceled = false;
}
/**
* Cancel the loading of the stream by throwing an IOException on the next request.
*/
public synchronized void cancel()
{
canceled = true;
}
protected synchronized void checkCanceled() throws IOException
{
if (canceled)
{
throw new IOException("page canceled");
}
}
@Override
public int read() throws IOException
{
checkCanceled();
return super.read();
}
@Override
public long skip(long n) throws IOException
{
checkCanceled();
return super.skip(n);
}
@Override
public int available() throws IOException
{
checkCanceled();
return super.available();
}
@Override
public void reset() throws IOException
{
checkCanceled();
super.reset();
}
}
/**
* class used for parsing the header after charset directive
*
* @author paul
*
*/
class HeaderParser
{
String raw;
String[][] tab;
public HeaderParser(String raw)
{
this.raw = raw;
tab = new String[10][2];
parse();
}
private void parse()
{
if (raw != null)
{
raw = raw.trim();
char[] ca = raw.toCharArray();
int beg = 0, end = 0, i = 0;
boolean inKey = true;
boolean inQuote = false;
int len = ca.length;
while (end < len)
{
char c = ca[end];
if (c == '=')
{
tab[i][0] = new String(ca, beg, end - beg).toLowerCase();
inKey = false;
end++;
beg = end;
}
else if (c == '\"')
{
if (inQuote)
{
tab[i++][1] = new String(ca, beg, end - beg);
inQuote = false;
do
{
end++;
}
while (end < len && (ca[end] == ' ' || ca[end] == ','));
inKey = true;
beg = end;
}
else
{
inQuote = true;
end++;
beg = end;
}
}
else if (c == ' ' || c == ',')
{
if (inQuote)
{
end++;
continue;
}
else if (inKey)
{
tab[i++][0] = (new String(ca, beg, end - beg)).toLowerCase();
}
else
{
tab[i++][1] = (new String(ca, beg, end - beg));
}
while (end < len && (ca[end] == ' ' || ca[end] == ','))
{
end++;
}
inKey = true;
beg = end;
}
else
{
end++;
}
}
// get last key/val, if any
if (--end > beg)
{
if (!inKey)
{
if (ca[end] == '\"')
{
tab[i++][1] = (new String(ca, beg, end - beg));
}
else
{
tab[i++][1] = (new String(ca, beg, end - beg + 1));
}
}
else
{
tab[i][0] = (new String(ca, beg, end - beg + 1)).toLowerCase();
}
}
else if (end == beg)
{
if (!inKey)
{
if (ca[end] == '\"')
{
tab[i++][1] = String.valueOf(ca[end - 1]);
}
else
{
tab[i++][1] = String.valueOf(ca[end]);
}
}
else
{
tab[i][0] = String.valueOf(ca[end]).toLowerCase();
}
}
}
}
public String findKey(int i)
{
if (i < 0 || i > 10) return null;
return tab[i][0];
}
public String findValue(int i)
{
if (i < 0 || i > 10) return null;
return tab[i][1];
}
public String findValue(String key)
{
return findValue(key, null);
}
public String findValue(String k, String Default)
{
if (k == null) return Default;
k.toLowerCase();
for (int i = 0; i < 10; ++i)
{
if (tab[i][0] == null)
{
return Default;
}
else if (k.equals(tab[i][0]))
{
return tab[i][1];
}
}
return Default;
}
public int findInt(String k, int Default)
{
try
{
return Integer.parseInt(findValue(k, String.valueOf(Default)));
}
catch (Throwable t)
{
return Default;
}
}
}
/**
*
*/
public void cancelASyncLoad()
{
synchronized (this)
{
if (loading != null)
{
/** we are loading asynchronously, so we need to cancel the old stream */
loading.cancel();
loading = null;
}
}
}
}