The purpose of my code is to mimic the functionality of Google Docs - when a user types on one machine, the letters they type appear on another machine. For simplistic purposes, each machine types text in a gui, and a main class handles all changes.
Every machine has an "Editor" and is linked up to a total of 1 "File Content Subject". The "File Content Subject" is supposed to make the changes a user makes and send the updated code to all "Editors"
This begins with a simple driver where I make a File Content Subject, create two Editors and connect these together
public class Driver {
public static void main(String[] args) {
FileContentSubject filecontentsubject = new FileContentSubject();
Editor e1 = new Editor(filecontentsubject);
Editor e2 = new Editor(filecontentsubject);
filecontentsubject.attach(e1);
filecontentsubject.attach(e2);
}
}
The Editor looks like this (the two pop up windows, not the rest):
The code that makes the Editor is here:
import javax.swing.*;
import java.util.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
public class Editor extends JFrame implements DocumentListener, Observer {
private FileContentSubject reference;
private Document doc;
private JScrollPane textAreaScrollPane;
private JTextArea textArea;
public Editor(FileContentSubject filecontentsubject) {
super("Editor");
initComponents();
this.reference = filecontentsubject;
textArea.getDocument().addDocumentListener(reference);
}
private void initComponents(){
textArea = new JTextArea();
textArea.setColumns(5);
textArea.setLineWrap(true);
textArea.setRows(50);
textArea.setWrapStyleWord(true);
textAreaScrollPane = new JScrollPane(textArea);
setLocation(600,100);
setSize(500,400);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(textArea, BorderLayout.CENTER);
}
@Override
public void changedUpdate(DocumentEvent arg0) {
}
@Override
public void insertUpdate(DocumentEvent arg0) {
reference.insertUpdate(arg0);
}
@Override
public void removeUpdate(DocumentEvent arg0) {
reference.removeUpdate(arg0);
}
@Override
public void update() {
//textArea.setText(reference.getJTextArea());
//textArea.setText(reference.temp);
}
}
The only Problem right now with my code is within the File Content Subject, when I try to change the code that I get passed by through the Editor. I am getting many "cannot mutate within notification" and somethings a "null pointer exception" error. The code that generates this is below and is the part I have commented out.
import java.util.ArrayList;
import java.util.List;
import javax.swing.JTextArea;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Position;
public class FileContentSubject implements Subject, DocumentListener {
private JTextArea textArea;
private Document doc;
private SubjectImpl reference;
@Override
public void attach(Observer o) {
reference.attach(o);
}
@Override
public void detach(Observer o) {
reference.detach(o);
}
@Override
public void notifyAllObservers() {
reference.notifyAllObservers();
}
public FileContentSubject(){
reference = new SubjectImpl();
textArea = new JTextArea();
textArea.setTabSize(5);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.getDocument().addDocumentListener(this);
}
@Override
public void changedUpdate(DocumentEvent arg0) {}
@Override
public void insertUpdate(DocumentEvent arg0) {
doc = (Document)arg0.getDocument();
// try {
// //this.textArea.setText(doc.getText(0, doc.getLength()-1));
// } catch (BadLocationException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
notifyAllObservers();
}
@Override
public void removeUpdate(DocumentEvent arg0) {
doc = (Document)arg0.getDocument();
// try {
// this.textArea.setText(doc.getText(0, doc.getLength()-1));
// } catch (BadLocationException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
notifyAllObservers();
}
public String getJTextArea(){
return textArea.getText();
}
}
So my question is, how can I pass text in (by passing in a DocumentEvent through an Editor) into the File Content Subject, have it change the File Content Subject, and notify all Editors?
My other classes that make this all happen (not important but shown for clarity):
Subject Interface
/**
* Interface
*/
public interface Subject {
public void attach(Observer o);
public void detach(Observer o);
public void notifyAllObservers();
}
Observer Interface
public interface Observer {
public void update();
}
SubjectImpl class
import java.util.ArrayList;
import java.util.List;
public class SubjectImpl implements Subject {
private List <Observer> observers;
public SubjectImpl(){
observers = new ArrayList<Observer>();
}
@Override
public void attach(Observer o) {
observers.add(o);
}
@Override
public void detach(Observer o) {
observers.remove(o);
}
@Override
public void notifyAllObservers() {
for(Observer o: observers){
o.update();
}
}
}
Needed to prevent the Editor that was editing itself from being updated. This was done with document properties and maintaining everything in a string rather than JTextArea. BIG thanks to acdcjunior for helping out and code answer are on the very last three code blocks in selected answer.
The java.lang.IllegalStateException: Attempt to mutate in notification
is thrown because in these lines in FileContentSubject
:
@Override
public void insertUpdate(DocumentEvent arg0) {
doc = (Document) arg0.getDocument();
try {
this.textArea.setText(doc.getText(0, doc.getLength()-1));
} catch (BadLocationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
notifyAllObservers();
}
@Override
public void removeUpdate(DocumentEvent arg0) {
doc = (Document) arg0.getDocument();
try {
this.textArea.setText(doc.getText(0, doc.getLength()-1));
} catch (BadLocationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
notifyAllObservers();
}
And this line at FileContentSubject
's constructor:
textArea.getDocument().addDocumentListener(this);
show that you are trying to change the value of the textArea
from inside the DocumentListener
methods.
As pointed out here, you should use a DocumentFilter
for that purpose.
Here's a simple working example of DocumentFilter
's use (it replaces all typed chars with their uppercase version):
//UpcaseFilter.java
//A simple DocumentFilter that maps lowercase letters to uppercase.
import javax.swing.*;
import javax.swing.text.*;
public class UpcaseFilter extends DocumentFilter {
public void insertString(DocumentFilter.FilterBypass fb, int offset,
String text, AttributeSet attr) throws BadLocationException {
fb.insertString(offset, text.toUpperCase(), attr);
}
// no need to override remove(): inherited version allows all removals
public void replace(DocumentFilter.FilterBypass fb, int offset, int length,
String text, AttributeSet attr) throws BadLocationException {
fb.replace(offset, length, text.toUpperCase(), attr);
}
public static void main(String[] args) {
DocumentFilter dfilter = new UpcaseFilter();
JTextArea jta = new JTextArea();
JTextField jtf = new JTextField();
((AbstractDocument) jta.getDocument()).setDocumentFilter(dfilter);
((AbstractDocument) jtf.getDocument()).setDocumentFilter(dfilter);
JFrame frame = new JFrame("UpcaseFilter");
frame.getContentPane().add(jta, java.awt.BorderLayout.CENTER);
frame.getContentPane().add(jtf, java.awt.BorderLayout.SOUTH);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(200, 120);
frame.setVisible(true);
}
}
(The above example was taken from the Java Swing, 2nd Edition book, chapter 22.)
In short, the problem is that this happens in your code:
Editor
instance's (let's call it edt
) textArea
changes...FileContentSubject
(as it is a DocumentListener
) notices the event and then notifies all its registered observers (edt
included!) to call their update()
method...update()
method of edt
changes its textArea
and... voilá! You are trying to change the textArea
who started the event (before the event ends)!All you have to do then is find a way of not notifying who started the event. The code below uses Document.putProperty()
and Document.getProperty()
for that: it detaches the event's source editor (reference.detach(e);
), notifies everyone of the change, and reattaches it (reference.attach(e);
).
(Also, I replaced FileContentSubject
's JTextArea
with a String
, as just that String
will suffice.)
So, heres the changed FileContentSubject
code:
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
public class FileContentSubject implements Subject, DocumentListener {
// private JTextArea textArea; // removed as a String field will suffice
// private Document doc; // should not be a field!
private String state;
public String getState() {
return this.state;
}
private SubjectImpl reference;
@Override
public void attach(Observer o) {
reference.attach(o);
}
@Override
public void detach(Observer o) {
reference.detach(o);
}
@Override
public void notifyAllObservers() {
reference.notifyAllObservers();
}
public FileContentSubject() {
reference = new SubjectImpl();
// textArea = new JTextArea();
// textArea.setTabSize(5);
// textArea.setLineWrap(true);
// textArea.setWrapStyleWord(true);
// textArea.getDocument().addDocumentListener(this);
}
@Override
public void changedUpdate(DocumentEvent arg0) {
}
@Override
public void insertUpdate(DocumentEvent arg0) {
Document doc = (Document) arg0.getDocument();
try {
// this.textArea.setText(doc.getText(0, doc.getLength()-1));
this.state = doc.getText(0, doc.getLength());
} catch (BadLocationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Editor e = (Editor) doc.getProperty("ownerEditor");
reference.detach(e); // so it will not be notified of its own change
notifyAllObservers(); // tell everybody else to catch up with the changes
reference.attach(e); // reattaches the editor
}
@Override
public void removeUpdate(DocumentEvent arg0) {
Document doc = (Document) arg0.getDocument();
try {
// this.textArea.setText(doc.getText(0, doc.getLength()-1));
this.state = doc.getText(0, doc.getLength());
} catch (BadLocationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Editor e = (Editor) doc.getProperty("ownerEditor");
reference.detach(e); // so it will not be notified of its own change
notifyAllObservers(); // tell everybody else to catch up with the changes
reference.attach(e); // reattaches the editor
}
// public String getJTextArea() {
// return textArea.getText();
// }
}
The changed Editor
's constructor:
public Editor(FileContentSubject filecontentsubject) {
super("Editor");
initComponents();
this.reference = filecontentsubject;
textArea.getDocument().addDocumentListener(reference);
textArea.getDocument().putProperty("ownerEditor", this); // <---- ADDED LINE
}
and update()
:
@Override
public void update() {
//textArea.setText(reference.getJTextArea());
//textArea.setText(reference.temp);
textArea.setText(reference.getState()); // ADDED
}
That's it!