/**
* Copyright (c) 2003, Spellcast development team
* http://spellcast.dev.java.net/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* [1] Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* [2] Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* [3] Neither the name "Spellcast development team" nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Copyright (c) 2005-2010, KoLmafia development team
* http://kolmafia.sourceforge.net/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* [1] Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* [2] Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* [3] Neither the name "KoLmafia" nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.java.dev.spellcast.utilities;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.LinkedHashSet;
import java.util.Stack;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import javax.swing.JEditorPane;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.text.Element;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
/**
* A multi-purpose message buffer which stores all sorts of the messages that can either be displayed or serialized in
* HTML form. In essence, this shifts the functionality of processing a chat message from the <code>ChatPanel</code> to
* an object external to it, which allows more object-oriented design and less complications.
*/
public class ChatBuffer
{
private static final Pattern TAG_PATTERN = Pattern.compile( "<\\s*([^\\s>]+)(.*?)>" );
private static final Pattern COMMENT_PATTERN = Pattern.compile( "<!--(.*?)-->" );
private final String title;
private final StringBuffer content = new StringBuffer();
private final LinkedList<JEditorPane> displayPanes = new LinkedList<JEditorPane>();
private final Set<JEditorPane> stickyPanes = new LinkedHashSet<JEditorPane>();
private final LinkedList<JEditorPane> addStickyPanes = new LinkedList<JEditorPane>();
private final LinkedList<JEditorPane> removeStickyPanes = new LinkedList<JEditorPane>();
private volatile int resetSequence = 0;
// Every queued update for this ChatBuffer carries the then-current value of resetSequence,
// which is incremented only on updates that completely rewrite the display. Any update
// with an outdated sequence number is simply ignored.
private PrintWriter logWriter;
protected static final HashMap<String, PrintWriter> ACTIVE_LOG_FILES = new HashMap<String, PrintWriter>();
private static final int MAXIMUM_LENGTH = 50000;
private static final int TRIM_TO_LENGTH = 45000;
/**
* Constructs a new <code>ChatBuffer</code>. However, note that this does not automatically translate into the
* messages being displayed; until a chat display is set, this buffer merely stores the message content to be
* displayed.
*/
public ChatBuffer( final String title )
{
this.title = title;
}
/**
* Adds a chat display used to display the chat messages currently being stored in the buffer.
*/
public JScrollPane addDisplay( final JEditorPane displayPane )
{
if ( displayPane == null )
{
return null;
}
displayPane.setContentType( "text/html" );
displayPane.setEditable( false );
displayPane.setText( this.getHTMLContent() );
this.displayPanes.addLast( displayPane );
this.addStickyPanes.addLast( displayPane );
JScrollPane scroller =
new JScrollPane(
displayPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER );
return scroller;
}
/**
* Sets the log file used to actively record messages that are being stored in the buffer.
*/
public void setLogFile( final File f )
{
if ( f == null || this.title == null )
{
return;
}
String filename = f.getPath();
if ( filename == null || this.title == null )
{
return;
}
if ( ChatBuffer.ACTIVE_LOG_FILES.containsKey( filename ) )
{
this.logWriter = ChatBuffer.ACTIVE_LOG_FILES.get( filename );
}
else
{
boolean shouldAppend = f.exists();
this.logWriter = new PrintWriter( DataUtilities.getOutputStream( f, shouldAppend ), true );
ChatBuffer.ACTIVE_LOG_FILES.put( filename, this.logWriter );
if ( !shouldAppend )
{
this.logWriter.println( "<html><head>" );
this.logWriter.println( "<title>" );
this.logWriter.println( this.title );
this.logWriter.println( "</title>" );
this.logWriter.println( "<style>" );
this.logWriter.println( this.getStyle() );
this.logWriter.println( "</style>" );
this.logWriter.println( "<body>" );
}
}
}
/**
* Closes the buffer.
*/
public void dispose()
{
this.displayPanes.clear();
this.stickyPanes.clear();
this.addStickyPanes.clear();
this.removeStickyPanes.clear();
if ( this.logWriter != null )
{
this.logWriter.close();
}
this.content.setLength( 0 );
}
private static void printHTML( final HTMLDocument doc )
{
HTMLEditorKit kit = new HTMLEditorKit();
StringWriter writer = new StringWriter();
try
{
kit.write(writer, doc, 0, doc.getLength());
}
catch ( Exception e )
{
}
String s = writer.toString();
System.out.println( "HTML = \"" + s + "\"" );
}
/**
* Clears the current buffer content.
*/
public void clear()
{
this.content.setLength( 0 );
SwingUtilities.invokeLater( new ResetHandler( this.getHTMLContent() ) );
}
/**
* Appends the given contents to the chat buffer.
*/
public void append( String newContents )
{
synchronized ( this.stickyPanes )
{
this.stickyPanes.addAll( this.addStickyPanes );
this.addStickyPanes.clear();
this.stickyPanes.removeAll( this.removeStickyPanes );
this.removeStickyPanes.clear();
}
if ( newContents == null )
{
SwingUtilities.invokeLater( new ResetHandler( this.getHTMLContent() ) );
return;
}
newContents = newContents.trim();
if ( newContents.length() == 0 )
{
return;
}
this.content.append( newContents );
if ( this.logWriter != null )
{
this.logWriter.println( newContents );
}
if ( this.content.length() < ChatBuffer.MAXIMUM_LENGTH )
{
SwingUtilities.invokeLater( new AppendHandler( newContents ) );
SwingUtilities.invokeLater( new ScrollHandler() );
return;
}
int lineIndex = this.content.indexOf( "<br", ChatBuffer.MAXIMUM_LENGTH - ChatBuffer.TRIM_TO_LENGTH );
if ( lineIndex != -1 )
{
lineIndex = this.content.indexOf( ">", lineIndex ) + 1;
}
if ( lineIndex == -1 )
{
this.clear();
return;
}
this.content.delete( 0, lineIndex );
SwingUtilities.invokeLater( new ResetHandler( this.getHTMLContent() ) );
SwingUtilities.invokeLater( new ScrollHandler() );
}
/**
* Returns the styling used by this buffer.
*/
public String getStyle()
{
return "body { font-family: sans-serif; }";
}
/**
* Returns all the content stored within this chat buffer.
*/
public String getContent()
{
return this.content.toString();
}
/**
* Returns all the styled content stored within this chat buffer.
*/
public String getHTMLContent()
{
StringBuffer htmlContent = new StringBuffer();
htmlContent.append( "<html><head><style>" );
htmlContent.append( this.getStyle() );
htmlContent.append( "</style></head><body>" );
htmlContent.append( this.content.toString() );
htmlContent.append( "</body></html>" );
return htmlContent.toString();
}
public void setSticky( JEditorPane editor, boolean sticky )
{
synchronized ( this.stickyPanes )
{
if ( sticky )
{
this.addStickyPanes.add( editor );
this.removeStickyPanes.remove( editor );
}
else
{
this.addStickyPanes.remove( editor );
this.removeStickyPanes.add( editor );
}
}
}
private class ResetHandler
implements Runnable
{
private final String htmlContent;
private final int resetSequence;
public ResetHandler( final String htmlContent )
{
this.htmlContent = htmlContent;
this.resetSequence = ++ChatBuffer.this.resetSequence;
}
public void run()
{
if ( this.resetSequence != ChatBuffer.this.resetSequence )
{
return; // outdated by a subsequent display reset
}
Iterator<JEditorPane> paneIterator = ChatBuffer.this.displayPanes.iterator();
while ( paneIterator.hasNext() )
{
JEditorPane displayPane = paneIterator.next();
if ( displayPane == null )
{
paneIterator.remove();
continue;
}
displayPane.setText( this.htmlContent );
}
}
}
private class AppendHandler
implements Runnable
{
private final String newContent;
private final int resetSequence;
public AppendHandler( final String newContent )
{
// Check for imbalanced HTML here
Stack<String> openTags = new Stack<String>();
Set<String> skippedTags = new HashSet<String>();
StringBuffer buffer = new StringBuffer();
String noCommentsContent = COMMENT_PATTERN.matcher( newContent ).replaceAll( "" );
Matcher tagMatcher = TAG_PATTERN.matcher( noCommentsContent );
while ( tagMatcher.find() )
{
String tagName = tagMatcher.group( 1 );
StringBuffer replacement = new StringBuffer();
if ( tagName.startsWith( "/" ) )
{
String closeTag = tagName.substring( 1 );
if ( skippedTags.contains( closeTag ) )
{
skippedTags.remove( closeTag );
}
else
{
while ( !openTags.isEmpty() )
{
String openTag = openTags.pop();
replacement.append( "</" );
replacement.append( openTag );
replacement.append( ">" );
if ( openTag.equalsIgnoreCase( closeTag ) )
{
break;
}
else if ( skippedTags.contains( closeTag ) )
{
skippedTags.remove( closeTag );
break;
}
else
{
skippedTags.add( closeTag );
}
}
}
}
else
{
if ( !tagName.equalsIgnoreCase( "br" ) )
{
openTags.push( tagName );
}
replacement.append( "<$1$2>" );
}
tagMatcher.appendReplacement( buffer, replacement.toString() );
}
tagMatcher.appendTail( buffer );
while ( !openTags.isEmpty() )
{
String openTag = openTags.pop();
buffer.append( "</" );
buffer.append( openTag );
buffer.append( ">" );
}
this.newContent = buffer.toString();
this.resetSequence = ChatBuffer.this.resetSequence;
}
public void run()
{
if ( this.resetSequence != ChatBuffer.this.resetSequence )
{
return; // outdated by a subsequent display reset
}
Iterator<JEditorPane> paneIterator = ChatBuffer.this.displayPanes.iterator();
while ( paneIterator.hasNext() )
{
JEditorPane displayPane = paneIterator.next();
if ( displayPane == null )
{
paneIterator.remove();
continue;
}
HTMLDocument currentHTML = (HTMLDocument) displayPane.getDocument();
Element contentElement = currentHTML.getDefaultRootElement();
while ( !contentElement.isLeaf() )
{
contentElement = contentElement.getElement( contentElement.getElementCount() - 1 );
}
try
{
currentHTML.insertAfterEnd( contentElement, this.newContent );
// If the insertion contained any non-ASCII characters, the "multiByte"
// property will be set on the document. This causes the use of
// an alternate layout algorithm that handles bidirectional text
// and other Unicode oddities: it's slower, and on some combinations
// of platform and JRE version, tremendously slower.
currentHTML.putProperty( "multiByte", Boolean.FALSE );
}
catch ( Exception e )
{
// If there's an exception, continue onward so that you
// still have an updated display. But, print the stack
// trace so you know what's going on.
e.printStackTrace();
}
// ChatBuffer.printHTML( currentHTML );
}
}
}
private class ScrollHandler
implements Runnable
{
private final int resetSequence;
public ScrollHandler()
{
this.resetSequence = ChatBuffer.this.resetSequence;
}
public void run()
{
if ( this.resetSequence != ChatBuffer.this.resetSequence )
{
return; // outdated by a subsequent display reset
}
Iterator<JEditorPane> paneIterator = ChatBuffer.this.stickyPanes.iterator();
while ( paneIterator.hasNext() )
{
JEditorPane stickyPane = paneIterator.next();
if ( stickyPane == null )
{
paneIterator.remove();
continue;
}
int contentLength = stickyPane.getDocument().getLength();
int caretPosition = Math.max( contentLength - 1, 0 );
stickyPane.setCaretPosition( caretPosition );
}
}
}
}