/*!
* 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 java.util.ArrayList;
import java.util.HashMap;
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 org.pentaho.reporting.libraries.base.util.FastStack;
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());
}
}
}