/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 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 Lesser General Public License for more details.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.openformula.ui.model2;
import org.pentaho.reporting.libraries.base.util.FastStack;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import java.util.ArrayList;
import java.util.HashMap;
public class FormulaDocument implements Document {
private static class FormulaDocumentEvent implements DocumentEvent {
private Document document;
private EventType type;
private int offset;
private int length;
private HashMap<Element, ElementChange> changes;
private FormulaDocumentEvent( final Document document,
final EventType type,
final int offset, final int length ) {
this.document = document;
this.type = type;
this.offset = offset;
this.length = length;
}
public void addChange( final Element element, final ElementChange change ) {
if ( changes == null ) {
changes = new HashMap<Element, ElementChange>();
}
changes.put( element, change );
}
/**
* Returns the offset within the document of the start of the change.
*
* @return the offset >= 0
*/
public int getOffset() {
return offset;
}
/**
* Returns the length of the change.
*
* @return the length >= 0
*/
public int getLength() {
return length;
}
/**
* Gets the document that sourced the change event.
*
* @return the document
*/
public Document getDocument() {
return document;
}
/**
* Gets the type of event.
*
* @return the type
*/
public EventType getType() {
return type;
}
/**
* Gets the change information for the given element. The change information describes what elements were added and
* removed and the location. If there were no changes, null is returned.
* <p/>
* This method is for observers to discover the structural changes that were made. This means that only elements
* that existed prior to the mutation (and still exist after the mutatino) need to have ElementChange records. The
* changes made available need not be recursive.
* <p/>
* For example, if the an element is removed from it's parent, this method should report that the parent changed and
* provide an ElementChange implementation that describes the change to the parent. If the child element removed
* had children, these elements do not need to be reported as removed.
* <p/>
* If an child element is insert into a parent element, the parent element should report a change. If the child
* element also had elements inserted into it (grandchildren to the parent) these elements need not report change.
*
* @param elem the element
* @return the change information, or null if the element was not modified
*/
public ElementChange getChange( final Element elem ) {
if ( changes == null ) {
return null;
}
return changes.get( elem );
}
}
private FormulaRootElement rootElement;
private EventListenerList listenerList;
private HashMap properties;
private boolean needRevalidateStructure;
public FormulaDocument() {
this.rootElement = new FormulaRootElement( this );
this.properties = new HashMap();
this.listenerList = new EventListenerList();
}
/**
* Returns number of characters of content currently in the document.
*
* @return number of characters >= 0
*/
public int getLength() {
return rootElement.getEndOffset();
}
/**
* Registers the given observer to begin receiving notifications when changes are made to the document.
*
* @param listener the observer to register
* @see Document#removeDocumentListener
*/
public void addDocumentListener( final DocumentListener listener ) {
listenerList.add( DocumentListener.class, listener );
}
/**
* Unregisters the given observer from the notification list so it will no longer receive change updates.
*
* @param listener the observer to register
* @see Document#addDocumentListener
*/
public void removeDocumentListener( final DocumentListener listener ) {
listenerList.remove( DocumentListener.class, listener );
}
protected void fireInsertEvent( final DocumentEvent event ) {
final DocumentListener[] listeners = listenerList.getListeners( DocumentListener.class );
for ( int i = 0; i < listeners.length; i++ ) {
final DocumentListener documentListener = listeners[ i ];
documentListener.insertUpdate( event );
}
}
protected void fireRemoveEvent( final DocumentEvent event ) {
final DocumentListener[] listeners = listenerList.getListeners( DocumentListener.class );
for ( int i = 0; i < listeners.length; i++ ) {
final DocumentListener documentListener = listeners[ i ];
documentListener.removeUpdate( event );
}
}
protected void fireChangeEvent( final DocumentEvent event ) {
final DocumentListener[] listeners = listenerList.getListeners( DocumentListener.class );
for ( int i = 0; i < listeners.length; i++ ) {
final DocumentListener documentListener = listeners[ i ];
documentListener.changedUpdate( event );
}
}
/**
* Registers the given observer to begin receiving notifications when undoable edits are made to the document.
*
* @param listener the observer to register
* @see UndoableEditEvent
*/
public void addUndoableEditListener( final UndoableEditListener listener ) {
listenerList.add( UndoableEditListener.class, listener );
}
/**
* Unregisters the given observer from the notification list so it will no longer receive updates.
*
* @param listener the observer to register
* @see UndoableEditEvent
*/
public void removeUndoableEditListener( final UndoableEditListener listener ) {
listenerList.remove( UndoableEditListener.class, listener );
}
/**
* Gets the properties associated with the document.
*
* @param key a non-<code>null</code> property key
* @return the properties
* @see #putProperty(Object, Object)
*/
public Object getProperty( final Object key ) {
return properties.get( key );
}
/**
* Associates a property with the document. Two standard property keys provided are: <a
* href="#StreamDescriptionProperty"> <code>StreamDescriptionProperty</code></a> and <a
* href="#TitleProperty"><code>TitleProperty</code></a>. Other properties, such as author, may also be defined.
*
* @param key the non-<code>null</code> property key
* @param value the property value
* @see #getProperty(Object)
*/
public void putProperty( final Object key, final Object value ) {
if ( value == null ) {
properties.remove( key );
} else {
properties.put( key, value );
}
}
/**
* Returns a position that represents the start of the document. The position returned can be counted on to track
* change and stay located at the beginning of the document.
*
* @return the position
*/
public Position getStartPosition() {
try {
return new FormulaDocumentPosition( rootElement, 0, true );
} catch ( BadLocationException e ) {
throw new IllegalStateException( "Should never happen" );
}
}
/**
* Returns a position that represents the end of the document. The position returned can be counted on to track
* change and stay located at the end of the document.
*
* @return the position
*/
public Position getEndPosition() {
try {
return new FormulaDocumentPosition( rootElement, 0, false );
} catch ( BadLocationException e ) {
throw new IllegalStateException( "Should never happen" );
}
}
/**
* This method allows an application to mark a place in a sequence of character content. This mark can then be used to
* tracks change as insertions and removals are made in the content. The policy is that insertions always occur prior
* to the current position (the most common case) unless the insertion location is zero, in which case the insertion
* is forced to a position that follows the original position.
*
* @param offs the offset from the start of the document >= 0
* @return the position
* @throws BadLocationException if the given position does not represent a valid location in the associated document
*/
public Position createPosition( final int offs ) throws BadLocationException {
final int elementIndex = rootElement.getElementIndex( offs );
final FormulaElement element = (FormulaElement) rootElement.getElement( elementIndex );
return new FormulaDocumentPosition( element, offs - element.getStartOffset(), true );
}
/**
* Returns all of the root elements that are defined. <p> Typically there will be only one document structure, but the
* interface supports building an arbitrary number of structural projections over the text data. The document can have
* multiple root elements to support multiple document structures. Some examples might be: </p> <ul> <li>Text
* direction. <li>Lexical token streams. <li>Parse trees. <li>Conversions to formats other than the native format.
* <li>Modification specifications. <li>Annotations. </ul>
*
* @return the root element
*/
public Element[] getRootElements() {
return new Element[] { rootElement };
}
/**
* Returns the root element that views should be based upon, unless some other mechanism for assigning views to
* element structures is provided.
*
* @return the root element
*/
public Element getDefaultRootElement() {
return rootElement;
}
public FormulaRootElement getRootElement() {
return rootElement;
}
/**
* Allows the model to be safely rendered in the presence of concurrency, if the model supports being updated
* asynchronously. The given runnable will be executed in a way that allows it to safely read the model with no
* changes while the runnable is being executed. The runnable itself may <em>not</em> make any mutations.
*
* @param r a <code>Runnable</code> used to render the model
*/
public synchronized void render( final Runnable r ) {
r.run();
}
/**
* Removes a portion of the content of the document. This will cause a DocumentEvent of type
* DocumentEvent.EventType.REMOVE to be sent to the registered DocumentListeners, unless an exception is thrown. The
* notification will be sent to the listeners by calling the removeUpdate method on the DocumentListeners.
* <p/>
* To ensure reasonable behavior in the face of concurrency, the event is dispatched after the mutation has occurred.
* This means that by the time a notification of removal is dispatched, the document has already been updated and any
* marks created by <code>createPosition</code> have already changed. For a removal, the end of the removal range is
* collapsed down to the start of the range, and any marks in the removal range are collapsed down to the start of the
* range. <p align=center><img src="doc-files/Document-remove.gif" alt="Diagram shows removal of 'quick' from 'The
* quick brown fox.'">
* <p/>
* If the Document structure changed as result of the removal, the details of what Elements were inserted and removed
* in response to the change will also be contained in the generated DocumentEvent. It is up to the implementation of
* a Document to decide how the structure should change in response to a remove.
* <p/>
* If the Document supports undo/redo, an UndoableEditEvent will also be generated.
*
* @param offs the offset from the beginning >= 0
* @param len the number of characters to remove >= 0
* @throws BadLocationException some portion of the removal range was not a valid part of the document. The location
* in the exception is the first bad position encountered.
* @see DocumentEvent
* @see DocumentListener
* @see UndoableEditEvent
* @see UndoableEditListener
*/
public void remove( final int offs, final int len ) throws BadLocationException {
if ( len == 0 ) {
return;
}
final int endPos = offs + len;
if ( endPos > getLength() ) {
throw new BadLocationException( "Document Size invalid", endPos );
}
final String orgText = getText( 0, getLength() );
final StringBuffer str = new StringBuffer( orgText );
str.delete( offs, offs + len );
rootElement.clear();
final FormulaElement[] formulaElements = FormulaParser.parseText( this, str.toString() );
for ( int i = 0; i < formulaElements.length; i++ ) {
final FormulaElement element = formulaElements[ i ];
rootElement.insertElement( i, element );
}
rootElement.revalidateStructure();
fireRemoveEvent( new FormulaDocumentEvent( this, DocumentEvent.EventType.REMOVE, offs, len ) );
}
/**
* Inserts a string of content. This will cause a DocumentEvent of type DocumentEvent.EventType.INSERT to be sent to
* the registered DocumentListers, unless an exception is thrown. The DocumentEvent will be delivered by calling the
* insertUpdate method on the DocumentListener. The offset and length of the generated DocumentEvent will indicate
* what change was actually made to the Document. <p align=center><img src="doc-files/Document-insert.gif"
* alt="Diagram shows insertion of 'quick' in 'The quick brown fox'">
* <p/>
* If the Document structure changed as result of the insertion, the details of what Elements were inserted and
* removed in response to the change will also be contained in the generated DocumentEvent. It is up to the
* implementation of a Document to decide how the structure should change in response to an insertion.
* <p/>
* If the Document supports undo/redo, an UndoableEditEvent will also be generated.
*
* @param offset the offset into the document to insert the content >= 0. All positions that track change at or after
* the given location will move.
* @param str the string to insert
* @param a the attributes to associate with the inserted content. This may be null if there are no attributes.
* @throws BadLocationException the given insert position is not a valid position within the document
* @see DocumentEvent
* @see DocumentListener
* @see UndoableEditEvent
* @see UndoableEditListener
*/
public void insertString( final int offset, final String str, final AttributeSet a ) throws BadLocationException {
final String orgText = getText( 0, getLength() );
final StringBuffer str2 = new StringBuffer( orgText );
str2.insert( offset, str );
rootElement.clear();
final FormulaElement[] formulaElements = FormulaParser.parseText( this, str2.toString() );
for ( int i = 0; i < formulaElements.length; i++ ) {
final FormulaElement element = formulaElements[ i ];
rootElement.insertElement( i, element );
}
rootElement.revalidateStructure();
fireInsertEvent( new FormulaDocumentEvent( this, DocumentEvent.EventType.INSERT, offset, str.length() ) );
}
/**
* Fetches the text contained within the given portion of the document.
*
* @param offset the offset into the document representing the desired start of the text >= 0
* @param length the length of the desired string >= 0
* @return the text, in a String of length >= 0
* @throws BadLocationException some portion of the given range was not a valid part of the document. The location in
* the exception is the first bad position encountered.
*/
public String getText( final int offset, final int length ) throws BadLocationException {
if ( offset + length > getLength() ) {
throw new BadLocationException( "Document Size invalid", offset + length );
}
if ( rootElement.getElementCount() == 0 ) {
return "";
}
final String s = rootElement.getText();
return s.substring( offset, offset + length );
}
public String getText() {
return rootElement.getText();
}
/**
* Fetches the text contained within the given portion of the document.
* <p/>
* If the partialReturn property on the txt parameter is false, the data returned in the Segment will be the entire
* length requested and may or may not be a copy depending upon how the data was stored. If the partialReturn property
* is true, only the amount of text that can be returned without creating a copy is returned. Using partial returns
* will give better performance for situations where large parts of the document are being scanned. The following is
* an example of using the partial return to access the entire document:
* <p/>
* <pre><code>
* <p/>
* int nleft = doc.getDocumentLength();
* Segment text = new Segment();
* int offs = 0;
* text.setPartialReturn(true);
* while (nleft > 0) {
* doc.getText(offs, nleft, text);
* // do someting with text
* nleft -= text.count;
* offs += text.count;
* }
* <p/>
* </code></pre>
*
* @param offset the offset into the document representing the desired start of the text >= 0
* @param length the length of the desired string >= 0
* @param txt the Segment object to return the text in
* @throws BadLocationException Some portion of the given range was not a valid part of the document. The location in
* the exception is the first bad position encountered.
*/
public void getText( final int offset, final int length, final Segment txt ) throws BadLocationException {
final String text = getText( offset, length );
txt.array = text.toCharArray();
txt.offset = 0;
txt.count = text.length();
}
public FunctionInformation getFunctionForPosition( final int offset ) {
final FormulaFunctionElement fn = getFunction( offset );
if ( fn == null ) {
return null;
}
final ArrayList<String> params = new ArrayList<String>();
final ArrayList<Integer> paramsStart = new ArrayList<Integer>();
final ArrayList<Integer> paramsEnd = new ArrayList<Integer>();
int parenCount = 0;
int paramStart = 0;
int paramEnd = 0;
int globalStart = -1;
int globalEnd = -1;
final int count = rootElement.getElementCount();
boolean found = false;
final StringBuffer b = new StringBuffer( rootElement.getEndOffset() - fn.getStartOffset() );
for ( int i = 0; i < count; i++ ) {
final FormulaElement node = (FormulaElement) rootElement.getElement( i );
if ( found == false ) {
if ( node == fn ) {
found = true;
}
continue;
}
if ( node instanceof FormulaOpenParenthesisElement ) {
if ( parenCount > 0 ) {
b.append( '(' ); // NON-NLS
} else {
globalStart = node.getEndOffset();
paramStart = node.getEndOffset();
}
parenCount += 1;
} else if ( node instanceof FormulaClosingParenthesisElement ) {
parenCount -= 1;
if ( parenCount > 0 ) {
b.append( ')' ); // NON-NLS
} else {
paramEnd = node.getStartOffset();
globalEnd = node.getEndOffset();
break;
}
} else if ( node instanceof FormulaSemicolonElement ) {
if ( parenCount == 1 ) {
paramEnd = node.getStartOffset();
params.add( b.toString() );
if ( paramEnd < paramStart ) {
throw new IllegalStateException();
}
paramsStart.add( paramStart );
paramsEnd.add( paramEnd );
b.delete( 0, b.length() );
paramStart = node.getEndOffset();
} else {
b.append( ';' );
}
} else if ( node != null ) {
b.append( node.getText() );
}
}
if ( paramEnd < paramStart ) {
paramEnd = rootElement.getEndOffset();
globalEnd = rootElement.getEndOffset();
}
if ( globalEnd < offset ) {
return null;
}
paramsStart.add( paramStart );
paramsEnd.add( paramEnd );
final int[] starts = new int[ paramsStart.size() ];
final int[] ends = new int[ paramsEnd.size() ];
for ( int i = 0; i < ends.length; i++ ) {
final Integer endVal = paramsEnd.get( i );
ends[ i ] = endVal.intValue();
final Integer startVal = paramsStart.get( i );
starts[ i ] = startVal.intValue();
}
params.add( b.toString() );
String functionImage = null;
try {
functionImage = getText( fn.getStartOffset(), globalEnd - fn.getStartOffset() );
} catch ( BadLocationException e ) {
e.printStackTrace();
}
return new FunctionInformation
( fn.getNormalizedFunctionName(), fn.getStartOffset(),
globalStart, globalEnd, functionImage, params.toArray( new String[ params.size() ] ),
starts, ends );
}
private FormulaFunctionElement getFunction( final int offset ) {
FormulaFunctionElement function = null;
final FastStack functionsStack = new FastStack();
final int count = rootElement.getElementCount();
boolean haveCloseParentheses = false;
for ( int i = 0; i < count; i++ ) {
final FormulaElement node = (FormulaElement) rootElement.getElement( i );
if ( ( node != null ) && ( node.getStartOffset() > offset ) ) {
if ( function == null ) {
return null;
}
return function;
}
if ( haveCloseParentheses ) {
if ( functionsStack.isEmpty() == false ) {
functionsStack.pop();
}
if ( functionsStack.isEmpty() ) {
function = null;
} else {
function = (FormulaFunctionElement) functionsStack.peek();
}
haveCloseParentheses = false;
}
if ( node instanceof FormulaFunctionElement ) {
function = (FormulaFunctionElement) node;
}
if ( node instanceof FormulaOpenParenthesisElement ) {
functionsStack.push( function );
}
if ( node instanceof FormulaClosingParenthesisElement ) {
haveCloseParentheses = true;
}
}
if ( functionsStack.isEmpty() == false ) {
final FormulaElement lastElement = ( count >= 1 ) ? (FormulaElement) rootElement.getElement( count - 1 ) : null;
if ( ( lastElement != null ) && ( lastElement.getEndOffset() >= offset ) ) {
return (FormulaFunctionElement) functionsStack.get( 0 );
} else {
return (FormulaFunctionElement) functionsStack.peek();
}
}
return function;
}
public void setText( final String text ) {
rootElement.clear();
final FormulaElement[] formulaElements = FormulaParser.parseText( this, text );
for ( int i = 0; i < formulaElements.length; i++ ) {
final FormulaElement element = formulaElements[ i ];
rootElement.insertElement( i, element );
}
rootElement.revalidateStructure();
rootElement.revalidateNodePositions();
needRevalidateStructure = false;
fireInsertEvent( new FormulaDocumentEvent( this, DocumentEvent.EventType.INSERT, 0, text.length() ) );
}
/**
* Retrieve the element at specified position. Note, the index is not the cursor index but rather the tokenized
* element position. So '=COUNT(1;2;3)' would contain 9 elements starting with element '=' at 0 index upto ')' at
* index 8.
*
* @param index
* @return FormulaElement specified at index. If index is invalid then return null.
*/
public FormulaElement getElementAtPosition( final int index ) {
return (FormulaElement) rootElement.getElement( index );
}
public void revalidateStructure() {
if ( needRevalidateStructure ) {
setText( getText() );
}
}
}