3

I'm writing a static method for a program I've been working on to go through the entries in the "graylist" in my Firebase database and check against user input to see if that entry exists. I'm able to locate the entry just fine, but because of some constraints of the way the threads work when reading/writing data from/to Firebase, I'm unable to figure out how to inform the user if the operation was successful or not.

I have the following method:

public static void addUser() {
    final boolean [] complete = new boolean[1];
    final String email = Tools.getEmail();
    DatabaseReference grayRef = FirebaseDatabase.getInstance().getReference().child("graylist");

    // Search the graylist for the email specified.
    CountDownLatch latch = new CountDownLatch(1); // need this so Parse-Ly doesn't close before event fires
    grayRef.addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onCancelled(DatabaseError dbe) {}

        @Override
        public void onDataChange(DataSnapshot snap) {
            Iterable<DataSnapshot> graylist = snap.getChildren();
            for(DataSnapshot gray : graylist) {
                String uid = gray.getKey();
                String em = gray.getValue(String.class);
                if(em.equals(email)) {
                    // We found the one we're looking for. Insert its UID into the whitelist.
                    DatabaseReference whiteRef = FirebaseDatabase.getInstance().getReference().child("whitelist");
                    whiteRef.child(uid).setValue(email, new DatabaseReference.CompletionListener() {
                        @Override
                        public void onComplete(DatabaseError dbe, DatabaseReference dbr) {
                            complete[0] = true;
                            latch.countDown();
                            if(dbe != null) {
                                System.err.println("Error adding user to whitelist!");
                                dbe.toException().printStackTrace();
                            }
                        }
                    });
                    break;
                }
            }
            if(latch.getCount() > 0) {
                complete[0] = false;
                latch.countDown();
            }
        }
    });

    try {
        latch.await(); // wait for event to fire before we move on
    } catch(InterruptedException ie) {
        System.err.println("ERROR: Add user latch interrupted!");
        ie.printStackTrace();
    }

    if(complete[0]) ParselyMain.createAlert(AlertType.CONFIRMATION, "User Added", "User added to the whitelist!");
    else ParselyMain.createAlert(AlertType.INFORMATION, "User Not Found", "That user was not found in the graylist!");
}

I realize how hacky the final boolean array is, but even that is not working for me. The idea was to use a CountDownLatch and only count down when the write is successful. If all entries from the graylist are read and the CountDownLatch is still at 1, then nothing has been written, and it should count down to exit the thread, but the boolean would determine if the operation is successful or not.

The problem is that it seems to reach the if block after my for loop before the onComplete method is called, resulting in it thinking the operation failed (even though my database on Firebase shows otherwise), and the wrong alert is output.

Can anyone think of a better way to handle this so that I can properly inform them if the user was added to the whitelist or if the user was not found?

Darin Beaudreau
  • 375
  • 7
  • 30

2 Answers2

0

Operations on the Firebase database are asynchronous by nature, so even if somehow you make the above code work, eventually you'll run into more complex cases where much of the lines of code are doing synchronization instead of handling the functionality of your application. This would lead to unmaintainable code that is easy to break.

So the simplest and "natural" way of processing the database request is to handle each event in the code whenever it occurs. This means informing the user of a successful update when the onComplete method is invoked, and informing the user of a failure when all the children entities have been checked and no match was found:

public static void addUser() {
    final String email = Tools.getEmail();
    DatabaseReference grayRef = FirebaseDatabase.getInstance().getReference().child("graylist");

    // Search the graylist for the email specified.
    grayRef.addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onCancelled(DatabaseError dbe) {}

        @Override
        public void onDataChange(DataSnapshot snap) {
            boolean found = false;
            Iterable<DataSnapshot> graylist = snap.getChildren();
            for(DataSnapshot gray : graylist) {
                String uid = gray.getKey();
                String em = gray.getValue(String.class);
                if(em.equals(email)) {
                    found = true;
                    // We found the one we're looking for. Insert its UID into the whitelist.
                    DatabaseReference whiteRef = FirebaseDatabase.getInstance().getReference().child("whitelist");
                    whiteRef.child(uid).setValue(email, new DatabaseReference.CompletionListener() {
                        @Override
                        public void onComplete(DatabaseError dbe, DatabaseReference dbr) {
                            complete[0] = true;
                            if(dbe != null) {
                                System.err.println("Error adding user to whitelist!");
                                dbe.toException().printStackTrace();
                            } else {
                                Platform.runLater(() -> {                                  
                                    ParselyMain.createAlert(AlertType.CONFIRMATION, "User Added", "User added to the whitelist!");
                                });
                            }
                        }
                    });
                    break;
                }
            }
            if(!found) {
                Platform.runLater(() -> {
                    ParselyMain.createAlert(AlertType.INFORMATION, "User Not Found", "That user was not found in the graylist!");
                });
            }
        }
    });
}
M A
  • 71,713
  • 13
  • 134
  • 174
  • Not possible because onComplete is on a separate thread and JavaFX doesn't allow you to create dialog messages outside the main thread. It's rather frustrating. – Darin Beaudreau Apr 14 '17 at 19:01
  • Does this post help: http://stackoverflow.com/questions/19755031/how-javafx-application-thread-works? AFAIK Firebase triggers the `onComplete` callback on the main thread, so you need to call the UI on the JavaFX thread. – M A Apr 14 '17 at 19:56
  • BTW are you sure `onComplete` is called on a separate thread? In Android it gets called on the main thread and maybe on the JVM it is the same. In all cases, you can try the mechanism in the linked post (`Platform.runLater`). – M A Apr 14 '17 at 20:09
  • Yes, it is on a separate thread, because I get an exception when I try to call GUI functions from within that particular thread saying "Not on FX application thread;". This is apparently a well-known issue with JavaFX. – Darin Beaudreau Apr 15 '17 at 04:31
  • Again the post linked in my previous comment addresses this. Try wrapping the dialog invocation inside `Platform.runLater`. – M A Apr 15 '17 at 08:11
  • If I used that, I would need to remove the CountDownLatch, right? Otherwise, it will still only properly terminate if the onComplete method is called (where the latch is decremented). Also, does Platform.runLater simply execute whatever is inside as soon as it gets back to the main thread? Because I can see a problem with this. What if it iterates through all entries before onComplete is called? Then it will create a dialog for both successful and unsuccessful cases. – Darin Beaudreau Apr 16 '17 at 23:08
  • @DarinBeaudreau The whole point is to remove `CountDownLatch` and other synchronization logic, and deal with the asynchronous nature of Firebase. I updated the code in the answer to be more clear. For `Platform.runLater`, it will execute the code inside it on the JavaFX thread at some time in the future (see https://docs.oracle.com/javase/8/javafx/api/javafx/application/Platform.html#runLater-java.lang.Runnable-). Regarding your last question about `onComplete`, I don't really see a problem with it: it will only be called when an entry is found and we don't care on which thread it is called. – M A Apr 17 '17 at 12:26
  • I tried the Platform.runLater method and removed the CountDownLatch, and now I'm back to square one, where it exits before it can even call the onComplete method of the setValue thread or create the dialog alerts. I re-added the latch to countDown after Platform.runLater is called so it would at least get the dialog in there, but it still doesn't display because apparently the thread exits before the alert is shown. – Darin Beaudreau Apr 18 '17 at 18:45
0

I don't have enough reputation so I can't comment directly in your question. Sorry about that.

Firebase actually has its own thread. Upon calling grayRef.addListenerForSingleValueEvent, Firebase opens a new thread (or just use its already living independent thread), so your original thread continues to the if block immediately since it considers the operation to be done, which is what you see as a result.

Therefore, what you need is actually a listener that will communicate with your main activity once Firebase completes its operation. Since you said that you are not using android, you can create a specific class only to handle Firebase, which also includes interface and listener. For example,

public class FirebaseHandler {

    // parameters
    private DatabaseReference databaseReference;

    // constructor
    public FirebaseHandler() {
        // you'll most probably need these for some actions
        databaseReference = FirebaseDatabase.getInstance().getReference();
    }

    // interface
    public interface FirebaseInterface {
        void firebaseComplete(String serverStatus, String email);
    }

    // listener
    private FirebaseInterface listener;

    // set listener
    public void setListener(FirebaseInterface listener) {
        this.listener = listener;
    }

    /* getter for references, these will be useful to avoid calling the wrong place */
    // -- / 
    public DatabaseReference getRoot() {
        return databaseReference;
    }

    // -- /email       /* specify the data structure here */
    public DatabaseReference getEmailRef() {
        return getRoot().child("email");
    } 

    // finally your method
    public static void addUser() {
        // your codes here...
        whiteRef.child(uid).setValue(email, new DatabaseReference.CompletionListener() {
            @Override
            public void onComplete(DatabaseError dbe, DatabaseReference dbr) {
                // some action here....
                if (listener != null) listener.firebaseComplete("OK", email);
            }
        });
    }
}

You just need to attach the listener in your activity and implement its method. So in your UI thread...

class Main implements FirebaseHandler.FirebaseInterface {

    FirebaseHandler firebaseHandler = new FirebaseHandler();
    firebaseHandler.setListener(this);
    firebaseHandler.addUser();

    @Override
    public void firebaseComplete(String serverStatus, String email) {
        firebaseHandler.setListener(null);      // optional to remove listener
        if (serverStatus.equals("OK") {
            ParselyMain.createAlert(AlertType.CONFIRMATION, "User Added", "User added to the whitelist!");
        } else { 
            ParselyMain.createAlert(AlertType.INFORMATION, "User Not Found", "That user was not found in the graylist!"); 
        }
    }
}
tingyik90
  • 1,641
  • 14
  • 23