Search code examples
javadesign-patternsdocumentobserver-patternjtextarea

Java - Observer Pattern in regards to some code. Mimic syncronized text update


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"

enter image description here

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): enter image description here

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();
    }
}
}



ANSWER

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.


Solution

  • 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.)

    That's the reason for the exception thrown, but you'll have to do more to correct you code:

    In short, the problem is that this happens in your code:

    • When an Editor instance's (let's call it edt) textArea changes...
    • ...the FileContentSubject (as it is a DocumentListener) notices the event and then notifies all its registered observers (edt included!) to call their update() method...
    • ...the 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!