blob: f4acbd39bcc441276f4c9f3b3a171219d759d37f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.core.output2;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.util.ArrayList;
import java.util.List;
import org.netbeans.core.output2.ui.AbstractOutputPane;
import org.openide.util.Exceptions;
/** An implementation of Document directly over a memory mapped file such that
* no (or nearly no) memory copies are required to fetch data to display.
*
* @author Tim Boudreau, Jesse Glick
*/
public class OutputDocument implements Document, Element, ChangeListener {
private static final Logger LOG =
Logger.getLogger(OutputDocument.class.getName());
private List<DocumentListener> dlisteners = new ArrayList<DocumentListener>();
private volatile Timer timer = null;
private OutWriter writer;
private StringBuffer inBuffer;
private AbstractOutputPane pane;
private int lastInputOff = -1; // offset of start of input string, #142721
/** Creates a new instance of OutputDocument */
OutputDocument(OutWriter writer) {
if (Controller.LOG) {
Controller.log ("Creating a Document for " + writer);
}
this.writer = writer;
getLines().addChangeListener(this);
inBuffer = new StringBuffer();
}
OutWriter getWriter() {
return writer;
}
//#119985
public int getOutputLength() {
return getLines().getCharCount();
}
//#114290
public void setPane(AbstractOutputPane pane) {
this.pane = pane;
}
/**
* Destroy this OutputDocument and its backing storage. The document should not be visible
* in the UI when this method is called.
*/
public void dispose() {
if (Controller.LOG) Controller.log ("Disposing document and backing storage for " + getLines().readLock());
disposeQuietly();
writer.dispose();
writer = null;
}
/**
* Destory this OutputDocument, but not its backing storage. The document should not be
* visible in the UI when this method is called.
*/
public void disposeQuietly() {
if (timer != null) {
timer.stop();
timer = null;
}
dlisteners.clear();
lastEvent = null;
getLines().removeChangeListener(this);
}
public synchronized void addDocumentListener(DocumentListener documentListener) {
dlisteners.add (documentListener);
lastEvent = null;
}
public void addUndoableEditListener(UndoableEditListener l) {
//do nothing
}
public Position createPosition(int offset) throws BadLocationException {
if (offset < 0 || offset > getLines().getCharCount() + inBuffer.length()) {
throw new BadLocationException ("Bad position", offset); //NOI18N
}
//TODO
return new ODPosition (offset);
}
public Element getDefaultRootElement() {
return this;
}
public Position getEndPosition() {
return new ODEndPosition();
}
public int getLength() {
return getLines().getCharCount() + inBuffer.length();
}
public Object getProperty(Object obj) {
return null;
}
public Element[] getRootElements() {
return new Element[] {this};
}
public Position getStartPosition() {
return new ODStartPosition();
}
public String getText(int offset, int length) throws BadLocationException {
if (length == 0) {
return ""; //NOI18N
}
String result;
synchronized (getLines().readLock()) {
if (offset < 0 || offset + length > getLines().getCharCount() + inBuffer.length() || length < 0) {
throw new BadLocationException("Bad: " + offset + "," + //NOI18N
length + " (" + getLines().getCharCount() + ", " + inBuffer.length() + ")", offset);
}
int linesOffset = Math.min(getLines().getCharCount(), offset);
int linesEnd = Math.min(getLines().getCharCount(), offset + length);
result = getLines().getText(linesOffset, linesEnd);
if (offset + length > getLines().getCharCount()) {
int inEnd = offset + length - getLines().getCharCount();
result = result + inBuffer.substring(0, inEnd);
}
}
return result;
}
private char[] reusableSubrange = new char [256];
public void getText(int offset, int length, Segment txt) throws BadLocationException {
if (length < 0) {
//document is empty
txt.array = new char[0];
txt.offset=0;
txt.count = 0;
return;
}
if (offset < 0) {
throw new BadLocationException ("Negative offset", offset); //NOI18N
}
if (getLines().getLineCount() == -1) {
txt.array = new char[] {'\n'};
txt.offset = 0;
txt.count = 1;
return;
}
if (length > reusableSubrange.length) {
reusableSubrange = new char[length];
}
try {
synchronized (getLines().readLock()) {
int charCount = getLines().getCharCount();
// #180404
if (charCount < 0) {
txt.array = new char[0];
txt.offset=0;
txt.count = 0;
return;
}
int linesOffset = Math.min(charCount, offset);
int linesEnd = Math.min(charCount, offset + length);
char[] chars = getLines().getText(linesOffset, linesEnd, reusableSubrange);
if (offset + length > charCount) {
int inEnd = offset - charCount + length;
int inStart = Math.max(0, offset - charCount);
// calling Math.min to prevent nasty AOOBE wich seem to come out of nowhere..
inBuffer.getChars(Math.min(inStart, inBuffer.length()), Math.min(inEnd, inBuffer.length()),
chars, linesEnd - linesOffset);
}
txt.array = chars;
txt.offset = 0;
txt.count = Math.min(length, chars.length);
}
} catch (OutOfMemoryError error) {
//#50189 - try to salvage what we can
OutWriter.lowDiskSpace = true;
//mkleint: is not necessary low disk space, can also mean too many mapped buffers were requirested too fast..
//Sets the error flag and releases the storage
writer.dispose();
Logger.getAnonymousLogger().log(Level.WARNING,
"OOME while reading output. Cleaning up.", //NOI18N
error);
System.gc();
}
}
public void insertString(int offset, String str, AttributeSet attributeSet) throws BadLocationException {
final int off;
final int docLen = getLength();
final int inputOff = docLen - inBuffer.length(); // start of input
if (inputOff != lastInputOff) { // output written since last input
lastInputOff = inputOff;
off = docLen;
} else if (offset < inputOff) { // cursor before start of input
off = inputOff;
} else {
off = Math.min(offset, inBuffer.length() + inputOff);
}
inBuffer.insert(off - inputOff, str);
final int len = str.length();
DocumentEvent ev = new DocumentEvent() {
public int getOffset() {
return off;
}
public int getLength() {
return len;
}
public Document getDocument() {
return OutputDocument.this;
}
public EventType getType() {
return EventType.INSERT;
}
public ElementChange getChange(Element arg0) {
return null;
}
};
if (getLines() instanceof AbstractLines) {
AbstractLines lines = (AbstractLines) getLines();
int start = lines.getLineStart(lines.getLineCount() - 1);
int length = getLength() - start;
lines.lineUpdated(2*start, 2*length, length, false);
}
fireDocumentEvent(ev);
}
public String sendLine() {
final int off = getLength() - inBuffer.length();
final int len = inBuffer.length();
String toReturn = inBuffer.toString();
inBuffer = new StringBuffer();
DocumentEvent ev = new DocumentEvent() {
public int getOffset() {
return off;
}
public int getLength() {
return len;
}
public Document getDocument() {
return OutputDocument.this;
}
public EventType getType() {
return EventType.REMOVE;
}
public ElementChange getChange(Element arg0) {
return null;
}
};
if (getLines() instanceof AbstractLines) {
AbstractLines lines = (AbstractLines) getLines();
int start = lines.getLineStart(lines.getLineCount() - 1);
lines.lineUpdated(2*start, 0, 0, false);
}
fireDocumentEvent(ev);
return toReturn;
}
public void putProperty(Object obj, Object obj1) {
//do nothing
}
public void remove(int offset, int length) throws BadLocationException {
int startOff = getLength() - inBuffer.length();
final int off = Math.max(startOff, offset);
final int len = Math.min(length, inBuffer.length());
if (startOff != lastInputOff) { // output written since last input
lastInputOff = startOff;
inBuffer.delete(inBuffer.length() - len, inBuffer.length());
} else if (off - startOff + len <= getLength()) {
inBuffer.delete(off - startOff, off - startOff + len);
DocumentEvent ev = new DocumentEvent() {
public int getOffset() {
return off;
}
public int getLength() {
return len;
}
public Document getDocument() {
return OutputDocument.this;
}
public EventType getType() {
return EventType.REMOVE;
}
public ElementChange getChange(Element arg0) {
return null;
}
};
if (getLines() instanceof AbstractLines) {
AbstractLines lines = (AbstractLines) getLines();
int start = lines.getLineStart(lines.getLineCount() - 1);
int l = getLength() - start;
lines.lineUpdated(2*start, 2*l, l, false);
}
fireDocumentEvent(ev);
}
}
public synchronized void removeDocumentListener(DocumentListener documentListener) {
dlisteners.remove(documentListener);
lastEvent = null;
if (dlisteners.isEmpty() && timer != null) {
timer.stop();
timer = null;
}
}
public Lines getLines() {
//Unit test will check for null to determine if dispose succeeded
return writer != null ? writer.getLines() : null;
}
public int getLineStart (int line) {
return getLines().getLineCount() > 0 ? getLines().getLineStart(line) : 0;
}
public int getLineEnd (int lineIndex) {
if (getLines().getLineCount() == 0) {
return 0;
}
int endOffset;
if (lineIndex >= getLines().getLineCount()-1) {
endOffset = getLines().getCharCount() + inBuffer.length();
} else {
endOffset = getLines().getLineStart(lineIndex+1) - 1;
}
return endOffset;
}
public void removeUndoableEditListener(UndoableEditListener undoableEditListener) {
//do nothing
}
public void render(Runnable runnable) {
getElementCount(); //Force a refresh of lastPostedLine
runnable.run();
}
public AttributeSet getAttributes() {
return SimpleAttributeSet.EMPTY;
}
public Document getDocument() {
return this;
}
public Element getElement(int index) {
int realIndex = getLines().visibleToRealLine(index);
return new ODElement(realIndex);
}
public int getElementCount() {
return Math.max(1, getLines().getVisibleLineCount());
}
public int getElementIndex(int offset) {
int realLine = getLines().getLineAt(offset);
if (realLine < 0) {
return realLine;
} else {
return getLines().realToVisibleLine(realLine);
}
}
public int getEndOffset() {
return getLength() + 1;
}
public String getName() {
return "foo"; //XXX
}
public Element getParentElement() {
return null;
}
public int getStartOffset() {
return 0;
}
public boolean isLeaf() {
return false;
}
private volatile DO lastEvent = null;
private int lastFiredLineCount = 0;
private int lastFiredLength = 0;
private int lastVisibleLineCount = 0;
public void stateChanged(ChangeEvent changeEvent) {
assert SwingUtilities.isEventDispatchThread();
if (Controller.VERBOSE) Controller.log(changeEvent != null ? "Document got change event from writer" : "Document timer polling");
if (dlisteners.isEmpty()) {
if (Controller.VERBOSE) Controller.log("listeners empty, not firing");
return;
}
Lines lines = getLines();
if (lines.checkDirty(true)) {
if (lastEvent != null && !lastEvent.isConsumed()) {
if (Controller.VERBOSE) Controller.log("Last event not consumed, not firing");
return;
}
boolean lastLineChanged;
int lineCount;
int visibleLineCount;
int size;
synchronized (lines.readLock()) {
lineCount = lines.getLineCount();
visibleLineCount = lines.getVisibleLineCount();
size = lines.getCharCount() + inBuffer.length();
if (size == lastFiredLength && visibleLineCount == lastVisibleLineCount) {
// nothing changed
if (Controller.VERBOSE) {
Controller.log("Size is same " + size + " - not firing");
}
return;
}
lastLineChanged = lastFiredLineCount == lineCount;
if (lastFiredLineCount > 0 && lineCount > lastFiredLineCount) {
int lastFiredLineEnd = lines.getLineStart(lastFiredLineCount);
if (lastFiredLineEnd > lastFiredLength) {
lastLineChanged = true;
}
}
}
if (lastFiredLineCount == lineCount
&& lastVisibleLineCount != visibleLineCount) {
lastEvent = new DO(0);
} else {
lastEvent = new DO(
lastLineChanged
? lastFiredLineCount - 1
: lastFiredLineCount);
}
lastFiredLineCount = lineCount;
lastVisibleLineCount = visibleLineCount;
lastFiredLength = size;
if (Controller.VERBOSE) Controller.log("Firing document event on EQ with start index " + lastEvent.first);
fireDocumentEvent(lastEvent);
if (pane != null) {
pane.getFoldingSideBar().repaint();
}
} else {
if (Controller.VERBOSE) Controller.log("Writer says it is not dirty, firing no change");
}
}
private void fireDocumentEvent (DocumentEvent de) {
for (DocumentListener dl: new ArrayList<DocumentListener>(dlisteners)) {
//#114290
if (!(de instanceof DO)) {
if (pane != null) {
pane.doUpdateCaret();
}
}
if (de.getType() == DocumentEvent.EventType.REMOVE) {
dl.removeUpdate(de);
} else if (de.getType() == DocumentEvent.EventType.CHANGE) {
dl.changedUpdate(de);
} else {
dl.insertUpdate(de);
}
//#114290
if (!(de instanceof DO)) {
if (pane != null) {
pane.dontUpdateCaret();
}
}
}
}
static final class ODPosition implements Position {
private int offset;
ODPosition (int offset) {
this.offset = offset;
}
public int getOffset() {
return offset;
}
@Override
public int hashCode() {
return offset * 11;
}
@Override
public boolean equals (Object o) {
return (o instanceof ODPosition) &&
((ODPosition) o).getOffset() == offset;
}
}
final class ODEndPosition implements Position {
public int getOffset() {
return getLines().getCharCount() + inBuffer.length();
}
private Document doc() {
return OutputDocument.this;
}
@Override
public boolean equals (Object o) {
return (o instanceof ODEndPosition) && ((ODEndPosition) o).doc() ==
doc();
}
@Override
public int hashCode() {
return -2390481;
}
}
final class ODStartPosition implements Position {
public int getOffset() {
return 0;
}
private Document doc() {
return OutputDocument.this;
}
@Override
public boolean equals (Object o) {
return (o instanceof ODStartPosition) && ((ODStartPosition) o).doc() ==
doc();
}
@Override
public int hashCode() {
return 2190481;
}
}
final class ODElement implements Element {
private int lineIndex;
private int startOffset = -1;
private int endOffset = -1;
ODElement (int lineIndex) {
this.lineIndex = lineIndex;
}
@Override
public int hashCode() {
return lineIndex;
}
@Override
public boolean equals (Object o) {
return (o instanceof ODElement) && ((ODElement) o).lineIndex == lineIndex &&
((ODElement) o).getDocument() == getDocument();
}
public AttributeSet getAttributes() {
return SimpleAttributeSet.EMPTY;
}
public Document getDocument() {
return OutputDocument.this;
}
public Element getElement(int param) {
return null;
}
public int getElementCount() {
return 0;
}
public int getElementIndex(int param) {
return -1;
}
public int getEndOffset() {
calc();
return endOffset;
}
public String getName() {
return null;
}
public Element getParentElement() {
return OutputDocument.this;
}
public int getStartOffset() {
calc();
return startOffset;
}
void calc() {
synchronized (getLines().readLock()) {
if (lineIndex < 0) {
return;
}
if (startOffset == -1) {
if (lineIndex >= getLines().getLineCount()) {
// line has been removed, probably due to reached limit
startOffset = 0;
endOffset = 0;
return;
}
startOffset = getLines().getLineStart(lineIndex);
if (lineIndex >= getLines().getLineCount()-1) {
endOffset = getLines().getCharCount() + inBuffer.length() + 1;
} else {
endOffset = getLines().getLineStart(lineIndex+1);
}
assert endOffset >= startOffset : "Illogical getLine #" + lineIndex
+ ", startOffset=" + startOffset + ", endOffset=" + endOffset
+ ", charCount=" + getLines().getCharCount()
+ " with lines " + getLines() + " or writer has been reset"
+ ". writer: " + (writer == null ? "is null" :
("writer.isDisposed(): " + writer.isDisposed()
+ ". writer.getStorage(): " + writer.getStorage()));
} else if (lineIndex >= getLines().getLineCount()-1) {
//always recalculate the last line...
endOffset = getLines().getCharCount() + inBuffer.length() + 1;
}
}
}
public boolean isLeaf() {
return true;
}
@Override
public String toString() {
try {
return OutputDocument.this.getText(getStartOffset(), getEndOffset()
- getStartOffset());
} catch (BadLocationException ble) {
Exceptions.printStackTrace(ble);
return "";
}
}
}
public class DO implements DocumentEvent, DocumentEvent.ElementChange {
private int offset = -1;
private int length = -1;
private int lineCount = -1;
private boolean consumed = false;
private int first = -1;
DO(int start) {
this.first = start;
if (start < 0) {
throw new IllegalArgumentException ("Illogical start: " + start);
}
}
private void calc() {
//#60414 related assertion. The exceptions in the bug can only happen
// when this method is called from 2 threads? but that should not be happening
assert SwingUtilities.isEventDispatchThread() : "Should be accessed from AWT only or we have a synchronization problem"; //NOI18N
if (!consumed) {
consumed = true;
// update lastFired info
synchronized (getLines().readLock()) {
lastFiredLineCount = getLines().getLineCount();
lastFiredLength = getLines().getCharCount() + inBuffer.length();
// fill event info
if (first < lastFiredLineCount) {
offset = getLines().getLineStart(first);
lineCount = lastFiredLineCount - first;
length = lastFiredLength - offset;
} else {
lineCount = 0;
length = 0;
offset = 0;
}
}
}
}
public boolean isConsumed() {
return consumed;
}
@Override
public String toString() {
boolean wasConsumed = isConsumed();
calc();
return "Event: first=" + first + " linecount=" + lineCount + " offset=" + offset + " length=" + length + " consumed=" + wasConsumed;
}
public DocumentEvent.ElementChange getChange(Element element) {
if (element == OutputDocument.this) {
return this;
} else {
return null;
}
}
public Document getDocument() {
return OutputDocument.this;
}
public int getLength() {
calc();
return length;
}
public int getOffset() {
calc();
return offset;
}
public DocumentEvent.EventType getType() {
return first == 0 ? DocumentEvent.EventType.CHANGE :
DocumentEvent.EventType.INSERT;
}
public Element[] getChildrenAdded() {
calc();
if (first + lineCount > getLines().getLineCount()) {
LOG.log(Level.INFO, "Document line count: {0}, OD line count: {1}",
new Object[]{getLines().getLineCount(), first + lineCount});
return new Element[0];
}
Element[] e = new Element[lineCount];
for (int i = 0; i < lineCount; i++) {
e[i] = new ODElement(first + i);
}
return e;
}
public Element[] getChildrenRemoved() {
return new Element[0];
}
public Element getElement() {
return OutputDocument.this;
}
public int getIndex() {
calc();
return first;
}
}
@Override
public String toString() {
return "OD@" + System.identityHashCode(this) + " for " + getLines().readLock();
}
}