Sunday, March 04, 2018

Integrating Firebase with Jooby

Lately, I've become enamored of RESTful web microframeworks written in Java, such as:

Almost any of these frameworks will take 99% of the pain out of setting up a RESTful web app, and allow you to have working code almost immediately (assuming you know enough Maven and/or Gradle to get by), but I quickly settled on Jooby as the best impedance match for my particular needs (which are modest) and my skillset (likewise modest). Spark came in a close second.

Jooby is an amazingly powerful framework with some really endearing features (which I don't have time to go into here). I can't say enough good things about Jooby. Go to https://jooby.org/quickstart/ right now and check it out, if you're a Java developer.

Once I realized I could get Java compilation to work in Microsoft's fantastic Visual Studio Code (using the nifty Java extensions from RedHat), and once I realized I could launch my stuff straight from the Terminal in VS Code, I was off and running. When I learned that running a project with "mvn jooby:run" would not only start up the app and server (I'm using Undertow as the embedded server, incidentally, with great results...) but also let me automagically hot-deploy my Java app from VS Code just by doing a Save (with automatic recompilation triggered by Save), I was sold.

Jooby integrates with a lot of great stuff out of the box, but one thing I wanted was JSON-based NoSQL database persistence (without having to resort to something as elaborate as MongoDB). I spent an hour or so researching existing object databases to see which ones might be Java-friendly, easy to learn, embeddable, and lightweight (BWAH HA HA) -- to little avail. Ultimately, I decided it might be fun to go in a slightly different direction and give Google's Firebase Cloud a try.

That's when the misery started.

I tried putting the necessary dependencies in my pom.xml and setting up my imports in my Jooby app, but 9 lines of code immediately turned red and I had strange syntax errors on perfectly fine constructions, with obscure warnings about Maven m2e, such as maven-enforcer-plugin (goal "enforce") is ignored by m2e.

Then it turned out my Firebase import statements weren't working, but sometimes (if I moved code around) they did resolve. And then sometimes, a Maven compile would complete flawlessly; but then a mvn package or jooby:run would immediately fail with unresolved "compilation issues."

When things go this sour this fast, you have to take a deep breath and start looking at how to get something to work again (maybe by removing code).

Fast-forward a couple hours. I decided to go back to the Firebase web site and make sure I was following their Getting Started instructions exactly. I was. Then I Googled around until I came upon https://github.com/GoogleCloudPlatform/java-docs-samples, where there was a firestore (not Firebase) project with a Quickstart.java file that held all the answers.

Long story short, many of the imports were new, the techniques for setting the connection options and obtaining the database handle didn't end up matching anything on Google's Firebase Quickstart page, and I had to disable getUpdateTime() on results.get(), but ultimately, I succeeded in posting a new document record to the demo database from my Jooby test app.

The code that finally worked, for me, is shown below with no cleanups whatsoever; I decided to leave all the uglinesses in place so you can get a feel for how much trial and error was involved in finally getting this shit stuff to work.

You'll have to ignore some of my test routes. The main thing I wanted to get working was the /firebaseTest route, which triggers the updateFirebase() method.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.kt;

import org.jooby.Jooby;

import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.DocumentReference;
// [START fs_include_dependencies]



import java.util.HashMap;

import java.util.Map;
// import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.firestore.Firestore;
//import com.google.firebase.FirebaseOptions;
import com.google.cloud.firestore.FirestoreOptions;
// import com.google.firebase.FirebaseApp;
//import com.google.firebase.*;

public class App extends Jooby {
  Firestore db = null;
  {
    assets("/**"); // need this! else /css & /js not found (404)
    // put an index page up:
    assets("/", "index.html");

    get("/test", () -> "Wow. It's alive!").produces("text/html");

    ws("/ws", ws -> {
      ws.onMessage(message -> System.out.println(" message: " + message.value()));
      System.out.println("Websockets available on /ws...");
    });

    get("/firebaseTest", () -> {
      updateFirebase(); 
      return "called updateFirebase()";
    });

    
 // Use the application default credentials
    
    String projectId = "the-hello-world-project-98dea";
    //GoogleCredentials credentials=GoogleCredentials.getApplicationDefault();

    //FirebaseOptions options =  new FirebaseOptions.Builder().setCredentials(credentials).setProjectId(projectId).build();

    //FirebaseApp.initializeApp(options);

    FirestoreOptions firestoreOptions =
        FirestoreOptions.getDefaultInstance().toBuilder()
            .setProjectId(projectId)
            .build();
     db = firestoreOptions.getService();
   // Firestore db = FirestoreOptions.getDefaultInstance().getService();
    //Firestore db = FirestoreClient.getFirestore();

  }

  
  public void updateFirebase() {
    DocumentReference docRef = db.collection("users").document("kthomas");
    // Add document data 
    Map data = new HashMap<>();
    data.put("first", "Kas");
    data.put("last", "Thomas");
    data.put("born", 1792);
    //asynchronously write data
    ApiFuture result = docRef.set(data);
    // ...
    // result.get() blocks on response
    // System.out.println("Update time : " + result.get().getUpdateTime());
  }
  

  public static void main(final String[] args) {
    run(App::new, args);
  }

}

And so, before you say "but your code uses Firestore, not Firebase," first of all, yes, technically you are right, I am using the (newer, document-oriented) Firestore Cloud database rather than the legacy JSON-oriented Firebase Realtime Database. (Read about the differences here.) The nomenclature is extraordinarily confusing. And it's not made any easier by the Java package names. Who would guess, for example, that FirestoreClient is in the com.google.firebase.cloud  package?

Be that as it may: Google says my Firestore database instance did indeed update, as desired, in the Firebase console at https://console.firebase.google.com -- which was the whole point of the exercise. I wanted the cloud database (the one you can inspect in the Firebase console) to update. And it did.

But it's no thanks to Google, whose online tutorial failed miserably to do the job and led to hours wasted troubleshooting was what turned out to be a pom.xml dependency issue, the magic missing piece being:

<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-firestore</artifactId>
<version>0.37.0-beta</version>
</dependency>

My advice to Google would be:
  • Tear down the online tutorials and start over.
  • Make it simpler.
  • Make it foolproof. 
  • Stop with the confusing names: Fire-this, Fire-that. Something Something Cloud. Fuck that crap. Get it together. Simplify the branding.
  • Eliminate boilerplate code (not so much a problem in the above code, but a problem in the non-working example code shown on Google's site). But please don't put me in @AnnotationHell. Please don't be Spring.
  • Start leveraging Java 8 lambda syntax.
  • Provide easy out-of-the-box integration with microframeworks like Jooby, Play, etc. 
Let me know when this FirefoobarCloud stuff is out of beta. IMHO, it's not fully baked.