package de.unisiegen.gtitool.ui.style.document;
import java.awt.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java_cup.runtime.Symbol;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import de.unisiegen.gtitool.core.entities.Entity;
import de.unisiegen.gtitool.core.parser.GTIParser;
import de.unisiegen.gtitool.core.parser.Parseable;
import de.unisiegen.gtitool.core.parser.exceptions.ParserException;
import de.unisiegen.gtitool.core.parser.exceptions.ParserMultiException;
import de.unisiegen.gtitool.core.parser.exceptions.ParserWarningException;
import de.unisiegen.gtitool.core.parser.exceptions.ScannerException;
import de.unisiegen.gtitool.core.parser.scanner.AbstractScanner;
import de.unisiegen.gtitool.core.parser.scanner.GTIScanner;
import de.unisiegen.gtitool.core.parser.style.Style;
import de.unisiegen.gtitool.core.preferences.listener.ColorChangedAdapter;
import de.unisiegen.gtitool.logger.Logger;
import de.unisiegen.gtitool.ui.preferences.PreferenceManager;
import de.unisiegen.gtitool.ui.style.listener.ExceptionsChangedListener;
import de.unisiegen.gtitool.ui.style.listener.ParseableChangedListener;
import de.unisiegen.gtitool.ui.swing.JGTITextPane;
/**
* An implementation of the {@link StyledDocument} interface to enable syntax
* highlighting using the lexer.
*
* @author Christian Fehler
* @version $Id$
* @param <E> The {@link Entity}.
*/
public final class StyledParserDocument < E extends Entity < E > > extends
DefaultStyledDocument
{
/**
* The serial version uid.
*/
private static final long serialVersionUID = -2546142812930077554L;
/**
* The max length.
*/
private static final int MAX_LENGTH = 1000000;
/**
* The {@link Logger} for this class.
*/
private static final Logger logger = Logger.getLogger ( JGTITextPane.class );
/**
* The {@link Parseable} for which this document was allocated.
*/
private Parseable parseable;
/**
* The current exceptions from the scanner and parser.
*
* @see #getException()
*/
private ArrayList < ScannerException > exceptionList;
/**
* The extern exceptions.
*
* @see #getException()
*/
private ArrayList < ScannerException > externExceptionList;
/**
* Flag that indicates if the panel is read only.
*/
private boolean editable = true;
/**
* The parsed object.
*/
private E parsedObject;
/**
* The highlighted {@link Entity} list.
*/
private ArrayList < Entity < ? >> highlightedParseableEntityList;
/**
* The overwritten {@link Style}.
*/
private HashMap < String, Style > overwrittenStyle = new HashMap < String, Style > ();
/**
* Flag that indicates if the right alignment is active.
*/
private boolean rightAlignment = false;
/**
* Allocates a new {@link StyledParserDocument} for the given
* {@link Parseable}, where the {@link Parseable} is used to determine the
* scanner for the documents content and thereby dictates the syntax
* highlighting.
*
* @param parseable The {@link Parseable} for which to allocate a document.
* @throws NullPointerException if the {@link Parseable} is null.
*/
public StyledParserDocument ( Parseable parseable )
{
if ( parseable == null )
{
throw new NullPointerException ( "parseable is null" ); //$NON-NLS-1$
}
this.parseable = parseable;
this.highlightedParseableEntityList = new ArrayList < Entity < ? >> ();
this.exceptionList = new ArrayList < ScannerException > ();
this.externExceptionList = new ArrayList < ScannerException > ();
/*
* ColorChangedListener
*/
PreferenceManager.getInstance ().addColorChangedListener (
new ColorChangedAdapter ()
{
/**
* {@inheritDoc}
*/
@Override
public void colorChanged ()
{
parse ();
}
} );
}
/**
* Adds the given {@link ExceptionsChangedListener}.
*
* @param listener The {@link ExceptionsChangedListener}.
*/
public final void addExceptionsChangedListener (
ExceptionsChangedListener listener )
{
this.listenerList.add ( ExceptionsChangedListener.class, listener );
}
/**
* Adds the given style.
*
* @param token The token.
* @param style The {@link Style}.
*/
public final void addOverwrittenStyle ( String token, Style style )
{
this.overwrittenStyle.put ( token, style );
}
/**
* Adds the given {@link ParseableChangedListener}.
*
* @param listener The {@link ParseableChangedListener}.
*/
public final void addParseableChangedListener (
ParseableChangedListener < E > listener )
{
this.listenerList.add ( ParseableChangedListener.class, listener );
}
/**
* Clears the overwritten {@link Style}.
*
* @param style The {@link Style} to clear.
*/
public final void clearOverwrittenStyle ( Style style )
{
for ( Map.Entry < String, Style > current : this.overwrittenStyle
.entrySet () )
{
if ( current.getValue ().equals ( style ) )
{
this.overwrittenStyle.remove ( current.getKey () );
}
}
}
/**
* Let the listeners know that the exceptions has changed.
*/
public final void fireExceptionsChanged ()
{
ExceptionsChangedListener [] listeners = this.listenerList
.getListeners ( ExceptionsChangedListener.class );
for ( ExceptionsChangedListener current : listeners )
{
current.exceptionsChanged ();
}
}
/**
* Let the listeners know that the {@link Object} has changed.
*
* @param newObject The new {@link Object}.
*/
@SuppressWarnings ( "unchecked" )
public final void fireParseableChanged ( E newObject )
{
ParseableChangedListener [] listeners = this.listenerList
.getListeners ( ParseableChangedListener.class );
for ( ParseableChangedListener < E > current : listeners )
{
current.parseableChanged ( newObject );
}
}
/**
* Returns the error set.
*
* @return The error set.
*/
private final SimpleAttributeSet getAttributeSetError ()
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setForeground ( set, PreferenceManager.getInstance ()
.getColorItemParserError ().getColor () );
StyleConstants.setBold ( set, true );
StyleConstants.setUnderline ( set, true );
return set;
}
/**
* Returns the hide highlighted {@link Entity} set.
*
* @return The hide highlighted {@link Entity} set.
*/
private final SimpleAttributeSet getAttributeSetHideHighlightedParseableEntity ()
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setBackground ( set, Color.WHITE );
StyleConstants.setForeground ( set, Color.WHITE );
return set;
}
/**
* Returns the highlighted {@link Entity} set.
*
* @return The highlighted {@link Entity} set.
*/
private final SimpleAttributeSet getAttributeSetHighlightedParseableEntity ()
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setBackground ( set, PreferenceManager.getInstance ()
.getColorItemParserHighlighting ().getColor () );
return set;
}
/**
* Returns the normal set.
*
* @return The normal set.
*/
private final SimpleAttributeSet getAttributeSetNormal ()
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setForeground ( set, Color.BLACK );
StyleConstants.setBold ( set, false );
return set;
}
/**
* Returns the warning set.
*
* @return The warning set.
*/
private final SimpleAttributeSet getAttributeSetWarning ()
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setBackground ( set, PreferenceManager.getInstance ()
.getColorItemParserWarning ().getColor () );
return set;
}
/**
* Returns the current {@link ScannerException}s that were detected while
* trying to interpret the token stream and the {@link ScannerException}s
* which were set from extern.
*
* @return The {@link ScannerException}s.
*/
public final ArrayList < ScannerException > getException ()
{
ArrayList < ScannerException > result = new ArrayList < ScannerException > ();
result.addAll ( this.exceptionList );
result.addAll ( this.externExceptionList );
return result;
}
/**
* Returns the {@link Object} for the program text within this document.
* Throws an exception if a parsing error occurred.
*
* @return The {@link Object} for the program text.
*/
public final E getParsedObject ()
{
return this.parsedObject;
}
/**
* Highlights the {@link Entity}s.
*/
private final void highlightedParseableEntities ()
{
parse ();
SimpleAttributeSet highlightedParseableEntitySet;
if ( this.rightAlignment )
{
highlightedParseableEntitySet = getAttributeSetHideHighlightedParseableEntity ();
}
else
{
highlightedParseableEntitySet = getAttributeSetHighlightedParseableEntity ();
}
for ( Entity < ? > current : this.highlightedParseableEntityList )
{
highlightedParseableEntitySet.addAttribute ( "highlighting", current ); //$NON-NLS-1$
if ( ( current.getParserOffset ().getStart () < 0 )
&& ( current.getParserOffset ().getEnd () < 0 ) )
{
setCharacterAttributes ( getLength (), getLength (),
highlightedParseableEntitySet, false );
}
else
{
setCharacterAttributes ( current.getParserOffset ().getStart (),
current.getParserOffset ().getEnd ()
- current.getParserOffset ().getStart (),
highlightedParseableEntitySet, false );
}
}
}
/**
* {@inheritDoc}
*
* @see AbstractDocument#insertString(int, String, AttributeSet)
*/
@Override
public final void insertString ( int offset, String string,
AttributeSet attributeSet ) throws BadLocationException
{
if ( string == null )
{
return;
}
if ( getLength () + string.length () <= MAX_LENGTH )
{
super.insertString ( offset, string, attributeSet );
fireParseableChanged ( parse () );
}
else
{
logger.debug ( "insertString", //$NON-NLS-1$
"text not inserted, larger than " + MAX_LENGTH + "characters" ); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* Return the editable value.
*
* @return The editable value.
*/
public final boolean isEditable ()
{
return this.editable;
}
/**
* Returns the right alignment value.
*
* @return The right alignment value.
* @see #rightAlignment
*/
public final boolean isRightAlignment ()
{
return this.rightAlignment;
}
/**
* Parses the document and returns the parsed object or null, if the text
* could not be parsed.
*
* @return The parsed object or null, if the text could not be parsed.
*/
@SuppressWarnings ( "unchecked" )
public final E parse ()
{
this.exceptionList.clear ();
this.externExceptionList.clear ();
this.parsedObject = null;
setCharacterAttributes ( 0, getLength (), getAttributeSetNormal (), true );
if ( this.rightAlignment )
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setAlignment ( set, StyleConstants.ALIGN_RIGHT );
setParagraphAttributes ( 0, getLength () - 1, set, true );
}
else
{
SimpleAttributeSet set = new SimpleAttributeSet ();
StyleConstants.setAlignment ( set, StyleConstants.ALIGN_LEFT );
setParagraphAttributes ( 0, getLength () - 1, set, true );
}
try
{
/*
* Start scanner
*/
int offset = 0;
String content = getText ( offset, getLength () );
final GTIScanner scanner = this.parseable.newScanner ( content );
final ArrayList < Symbol > symbols = new ArrayList < Symbol > ();
while ( true )
{
try
{
Symbol symbol = scanner.nextSymbol ();
if ( symbol == null )
{
break;
}
symbols.add ( symbol );
Style style = scanner.getStyleBySymbol ( symbol );
SimpleAttributeSet set = new SimpleAttributeSet ();
// Use the overwritten
if ( this.overwrittenStyle.containsKey ( symbol.value.toString () ) )
{
Style newStyle = this.overwrittenStyle.get ( symbol.value
.toString () );
StyleConstants.setForeground ( set, newStyle.getColor () );
StyleConstants.setBold ( set, newStyle.isBold () );
StyleConstants.setItalic ( set, newStyle.isItalic () );
}
// Use the normal scanner style
else
{
StyleConstants.setForeground ( set, style.getColor () );
StyleConstants.setBold ( set, style.isBold () );
StyleConstants.setItalic ( set, style.isItalic () );
}
setCharacterAttributes ( offset + symbol.left, symbol.right
- symbol.left, set, true );
}
catch ( ScannerException ecx )
{
int newOffset = offset + ecx.getRight ();
content = content.substring ( ecx.getRight () );
ecx = new ScannerException ( offset + ecx.getLeft (), offset
+ ecx.getRight (), ecx.getMessage (), ecx.getCause () );
SimpleAttributeSet errorSet = getAttributeSetError ();
errorSet.addAttribute ( "exception", ecx ); //$NON-NLS-1$
setCharacterAttributes ( ecx.getLeft (), ecx.getRight ()
- ecx.getLeft (), errorSet, false );
offset = newOffset;
scanner.restart ( content );
this.exceptionList.add ( ecx );
}
}
/*
* Start parser only if the scanner has no exceptions
*/
if ( this.exceptionList.size () == 0 )
{
GTIParser parser = this.parseable.newParser ( new AbstractScanner ()
{
@Override
public Style getStyleBySymbolId ( int id )
{
return ( ( AbstractScanner ) scanner ).getStyleBySymbolId ( id );
}
public Symbol nextSymbol () throws ScannerException
{
if ( symbols.isEmpty () )
{
return null;
}
return symbols.remove ( 0 );
}
public void restart ( @SuppressWarnings ( "unused" ) String text )
{
throw new UnsupportedOperationException ();
}
} );
try
{
this.parsedObject = ( E ) parser.parse ();
}
catch ( ParserMultiException ecx )
{
String [] message = ecx.getMessages ();
int [] startOffset = ecx.getParserStartOffset ();
int [] endOffset = ecx.getParserEndOffset ();
for ( int i = 0 ; i < startOffset.length ; i++ )
{
ParserException newException = new ParserException (
startOffset [ i ], endOffset [ i ], message [ i ] );
SimpleAttributeSet errorSet = getAttributeSetError ();
errorSet.addAttribute ( "exception", newException ); //$NON-NLS-1$
setCharacterAttributes ( startOffset [ i ], endOffset [ i ]
- startOffset [ i ], errorSet, false );
this.exceptionList.add ( newException );
}
}
catch ( ParserWarningException ecx )
{
if ( this.editable )
{
SimpleAttributeSet warningSet = getAttributeSetWarning ();
warningSet.addAttribute ( "warning", ecx ); //$NON-NLS-1$
if ( ( ecx.getLeft () < 0 ) && ( ecx.getRight () < 0 ) )
{
setCharacterAttributes ( getLength (), getLength (), warningSet,
false );
}
else
{
setCharacterAttributes ( ecx.getLeft (), ecx.getRight ()
- ecx.getLeft (), warningSet, false );
}
this.exceptionList.add ( new ParserWarningException ( ecx
.getRight (), ecx.getRight (), ecx.getMessage (), ecx
.getInsertText () ) );
}
}
catch ( ParserException ecx )
{
SimpleAttributeSet errorSet = getAttributeSetError ();
errorSet.addAttribute ( "exception", ecx ); //$NON-NLS-1$
if ( ( ecx.getLeft () < 0 ) && ( ecx.getRight () < 0 ) )
{
setCharacterAttributes ( getLength (), getLength (), errorSet,
false );
}
else
{
setCharacterAttributes ( ecx.getLeft (), ecx.getRight ()
- ecx.getLeft (), errorSet, false );
}
this.exceptionList.add ( ecx );
}
}
}
catch ( Exception exc )
{
exc.printStackTrace ();
System.exit ( 1 );
}
fireExceptionsChanged ();
return this.parsedObject;
}
/**
* {@inheritDoc}
*
* @see Document#remove(int, int)
*/
@Override
public final void remove ( int offset, int length )
throws BadLocationException
{
super.remove ( offset, length );
fireParseableChanged ( parse () );
}
/**
* Removes the given {@link ExceptionsChangedListener}.
*
* @param listener The {@link ExceptionsChangedListener}.
*/
public final void removeExceptionsChangedListener (
ExceptionsChangedListener listener )
{
this.listenerList.remove ( ExceptionsChangedListener.class, listener );
}
/**
* Removes the given token.
*
* @param token The token to remove.
*/
public final void removeOverwrittenStyle ( String token )
{
this.overwrittenStyle.remove ( token );
}
/**
* Removes the given {@link ParseableChangedListener}.
*
* @param listener The {@link ParseableChangedListener}.
*/
public final void removeParseableChangedListener (
ParseableChangedListener < E > listener )
{
this.listenerList.remove ( ParseableChangedListener.class, listener );
}
/**
* Sets the editable value.
*
* @param editable The boolean to be set.
*/
public final void setEditable ( boolean editable )
{
this.editable = editable;
}
/**
* Sets the extern {@link ScannerException}s.
*
* @param exceptions The {@link ScannerException}s to set.
* @see #externExceptionList
*/
public final void setException ( Iterable < ScannerException > exceptions )
{
this.externExceptionList.clear ();
Iterator < ScannerException > iterator = exceptions.iterator ();
while ( iterator.hasNext () )
{
this.externExceptionList.add ( iterator.next () );
}
for ( ScannerException current : this.externExceptionList )
{
SimpleAttributeSet errorSet = getAttributeSetError ();
errorSet.addAttribute ( "exception", current ); //$NON-NLS-1$
if ( ( current.getLeft () < 0 ) && ( current.getRight () < 0 ) )
{
setCharacterAttributes ( getLength (), getLength (), errorSet, false );
}
else
{
setCharacterAttributes ( current.getLeft (), current.getRight ()
- current.getLeft (), errorSet, false );
}
}
fireExceptionsChanged ();
}
/**
* Sets the {@link Entity}s which should be highlighted.
*
* @param entities The {@linkEntity}s which should be highlighted.
*/
public final void setHighlightedParseableEntity ( Entity < ? > ... entities )
{
this.highlightedParseableEntityList.clear ();
for ( Entity < ? > current : entities )
{
this.highlightedParseableEntityList.add ( current );
}
highlightedParseableEntities ();
}
/**
* Sets the {@link Entity} which should be highlighted.
*
* @param entity The {@link Entity} which should be highlighted.
*/
public final void setHighlightedParseableEntity ( Entity < ? > entity )
{
ArrayList < Entity < ? >> entities = new ArrayList < Entity < ? > > ();
entities.add ( entity );
setHighlightedParseableEntity ( entities );
}
/**
* Sets the {@link Entity}s which should be highlighted.
*
* @param entities The {@link Entity}s which should be highlighted.
*/
public final void setHighlightedParseableEntity (
Iterable < ? extends Entity < ? > > entities )
{
this.highlightedParseableEntityList.clear ();
for ( Entity < ? > current : entities )
{
this.highlightedParseableEntityList.add ( current );
}
highlightedParseableEntities ();
}
/**
* Sets the right alignment.
*
* @param rightAlignment The right alignment to set.
* @see #rightAlignment
*/
public final void setRightAlignment ( boolean rightAlignment )
{
this.rightAlignment = rightAlignment;
}
}