I've spent quite a bit of time dabbling with Couchbase and web applications using the awesome Java SDK, but did you know you can create desktop applications that use Couchbase to store data as well? Java desktop frameworks like JavaFX can make use of the Couchbase Java SDK, but it is probably not a good idea to use an SDK intended for server in your client facing application. Instead, you can actually use Couchbase's mobile solution to build client facing desktop applications. It uses the same APIs as Android, but was designed with desktops in mind.

We're going to take a look at building a simple desktop application using JavaFX, Couchbase Lite, and even Couchbase Sync Gateway to synchronize this data between computers.

The Requirements

There are a few requirements that must be met in order to make this project successful.

JDK 1.7+

Maven

Couchbase Sync Gateway

This project will use Maven to gather our dependencies and build a JAR file. While Sync Gateway isn't truly a requirement, it is if you wish to add synchronization support to your application.

Creating a Fresh JavaFX Project with Maven

We need to create a basic Maven project. This can be done in an IDE of your choice, or it can be done manually. Essentially what we'll need is the following file and directory structure:

src main java com couchbase CouchbaseSingleton.java Main.java Todo.java TodoFXController.java resources TodoFX.fxml pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 src main java com couchbase CouchbaseSingleton . java Main . java Todo . java TodoFXController . java resources TodoFX . fxml pom . xml

We'll get into the specifics of what each file contains when we start developing. For now we need to set up our Maven pom.xml file so it will obtain the necessary dependencies.

Open the project's pom.xml file and include the following:

<project> <groupId>com.couchbase</groupId> <artifactId>couchbase-javafx-example</artifactId> <modelVersion>4.0.0</modelVersion> <name>Couchbase JavaFX Example</name> <packaging>jar</packaging> <version>1.0</version> <dependencies> <dependency> <groupId>com.couchbase.lite</groupId> <artifactId>couchbase-lite-java</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>com.couchbase.lite</groupId> <artifactId>couchbase-lite-java-sqlcipher</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>com.zenjava</groupId> <artifactId>javafx-maven-plugin</artifactId> <version>8.5.0</version> <configuration> <mainClass>com.couchbase.Main</mainClass> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.2.1</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.couchbase.Main</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> 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 < project > < groupId > com . couchbase < / groupId > < artifactId > couchbase - javafx - example < / artifactId > < modelVersion > 4.0.0 < / modelVersion > < name > Couchbase JavaFX Example < / name > < packaging > jar < / packaging > < version > 1.0 < / version > < dependencies > < dependency > < groupId > com . couchbase . lite < / groupId > < artifactId > couchbase - lite - java < / artifactId > < version > 1.3.0 < / version > < / dependency > < dependency > < groupId > com . couchbase . lite < / groupId > < artifactId > couchbase - lite - java - sqlcipher < / artifactId > < version > 1.3.0 < / version > < / dependency > < dependency > < groupId > junit < / groupId > < artifactId > junit < / artifactId > < version > 4.12 < / version > < scope > test < / scope > < / dependency > < / dependencies > < build > < plugins > < plugin > < groupId > com . zenjava < / groupId > < artifactId > javafx - maven - plugin < / artifactId > < version > 8.5.0 < / version > < configuration > < mainClass > com . couchbase . Main < / mainClass > < / configuration > < / plugin > < plugin > < groupId > org . apache . maven . plugins < / groupId > < artifactId > maven - compiler - plugin < / artifactId > < version > 3.3 < / version > < configuration > < source > 1.7 < / source > < target > 1.7 < / target > < / configuration > < / plugin > < plugin > < artifactId > maven - assembly - plugin < / artifactId > < version > 2.2.1 < / version > < configuration > < descriptorRefs > < descriptorRef > jar - with - dependencies < / descriptorRef > < / descriptorRefs > < archive > < manifest > < addClasspath > true < / addClasspath > < classpathPrefix > lib / < / classpathPrefix > < mainClass > com . couchbase . Main < / mainClass > < / manifest > < / archive > < / configuration > < executions > < execution > < id > make - assembly < / id > < phase > package < / phase > < goals > < goal > single < / goal > < / goals > < / execution > < / executions > < / plugin > < / plugins > < / build > < / project >

Without going into too much unnecessary detail in the above Maven file, we want to pay attention to a few things in particular.

<dependency> <groupId>com.couchbase.lite</groupId> <artifactId>couchbase-lite-java</artifactId> <version>1.3.0</version> </dependency> 1 2 3 4 5 6 7 < dependency > < groupId > com . couchbase . lite < / groupId > < artifactId > couchbase - lite - java < / artifactId > < version > 1.3.0 < / version > < / dependency >

The above dependency says we are including Couchbase Lite into our project. Remember, Couchbase Lite is a local database that does not sit on a server somewhere. The data is stored locally and the component is bundled within your application.

We also want to pay attention to the following plugin:

<plugin> <groupId>com.zenjava</groupId> <artifactId>javafx-maven-plugin</artifactId> <version>8.5.0</version> <configuration> <mainClass>com.couchbase.Main</mainClass> </configuration> </plugin> 1 2 3 4 5 6 7 8 9 10 < plugin > < groupId > com . zenjava < / groupId > < artifactId > javafx - maven - plugin < / artifactId > < version > 8.5.0 < / version > < configuration > < mainClass > com . couchbase . Main < / mainClass > < / configuration > < / plugin >

The above plugin is for creating a JavaFX project. Of course this project creation is a lot easier when using an IDE like IntelliJ, even though it isn't required.

Creating the Couchbase Singleton Class

Before we get invested in creating the UI and controllers for our JavaFX project, let's worry about how data is going to be handled.

For simplicity, it is a great idea to create a singleton class for managing the data throughout our project. It also works quite well when setting up data listeners to prevent having to write queries everywhere in the application. Let's go ahead and open the project's src/main/java/com/couchbase/CouchbaseSingleton.java file and include the following code. We'll break it down after.

package com.couchbase; import com.couchbase.lite.*; import com.couchbase.lite.replicator.Replication; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class CouchbaseSingleton { private Manager manager; private Database database; private Replication pushReplication; private Replication pullReplication; private static CouchbaseSingleton instance = null; private CouchbaseSingleton() { try { this.manager = new Manager(new JavaContext("data"), Manager.DEFAULT_OPTIONS); this.database = this.manager.getDatabase("fx-project"); View todoView = database.getView("todos"); todoView.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { emitter.emit(document.get("_id"), document); } }, "1"); } catch (Exception e) { e.printStackTrace(); } } public static CouchbaseSingleton getInstance() { if(instance == null) { instance = new CouchbaseSingleton(); } return instance; } public Database getDatabase() { return this.database; } public void startReplication(URL gateway, boolean continuous) { this.pushReplication = this.database.createPushReplication(gateway); this.pullReplication = this.database.createPullReplication(gateway); this.pushReplication.setContinuous(continuous); this.pullReplication.setContinuous(continuous); this.pushReplication.start(); this.pullReplication.start(); } public void stopReplication() { this.pushReplication.stop(); this.pullReplication.stop(); } public Todo save(Todo todo) { Map<String, Object> properties = new HashMap<String, Object>(); Document document = this.database.createDocument(); properties.put("type", "todo"); properties.put("title", todo.getTitle()); properties.put("description", todo.getDescription()); try { todo.setDocumentId(document.putProperties(properties).getDocument().getId()); } catch (Exception e) { e.printStackTrace(); } return todo; } public ArrayList<Todo> query() { ArrayList<Todo> results = new ArrayList<Todo>(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator<QueryRow> it = result; it.hasNext(); ) { QueryRow row = it.next(); document = row.getDocument(); results.add(new Todo(document.getId(), (String) document.getProperty("title"), (String) document.getProperty("description"))); } } catch (Exception e) { e.printStackTrace(); } return results; } } 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 package com . couchbase ; import com . couchbase . lite . * ; import com . couchbase . lite . replicator . Replication ; import java . net . URL ; import java . util . ArrayList ; import java . util . HashMap ; import java . util . Iterator ; import java . util . Map ; public class CouchbaseSingleton { private Manager manager ; private Database database ; private Replication pushReplication ; private Replication pullReplication ; private static CouchbaseSingleton instance = null ; private CouchbaseSingleton ( ) { try { this . manager = new Manager ( new JavaContext ( "data" ) , Manager . DEFAULT_OPTIONS ) ; this . database = this . manager . getDatabase ( "fx-project" ) ; View todoView = database . getView ( "todos" ) ; todoView . setMap ( new Mapper ( ) { @ Override public void map ( Map < String , Object > document , Emitter emitter ) { emitter . emit ( document . get ( "_id" ) , document ) ; } } , "1" ) ; } catch ( Exception e ) { e . printStackTrace ( ) ; } } public static CouchbaseSingleton getInstance ( ) { if ( instance == null ) { instance = new CouchbaseSingleton ( ) ; } return instance ; } public Database getDatabase ( ) { return this . database ; } public void startReplication ( URL gateway , boolean continuous ) { this . pushReplication = this . database . createPushReplication ( gateway ) ; this . pullReplication = this . database . createPullReplication ( gateway ) ; this . pushReplication . setContinuous ( continuous ) ; this . pullReplication . setContinuous ( continuous ) ; this . pushReplication . start ( ) ; this . pullReplication . start ( ) ; } public void stopReplication ( ) { this . pushReplication . stop ( ) ; this . pullReplication . stop ( ) ; } public Todo save ( Todo todo ) { Map < String , Object > properties = new HashMap < String , Object > ( ) ; Document document = this . database . createDocument ( ) ; properties . put ( "type" , "todo" ) ; properties . put ( "title" , todo . getTitle ( ) ) ; properties . put ( "description" , todo . getDescription ( ) ) ; try { todo . setDocumentId ( document . putProperties ( properties ) . getDocument ( ) . getId ( ) ) ; } catch ( Exception e ) { e . printStackTrace ( ) ; } return todo ; } public ArrayList < Todo > query ( ) { ArrayList < Todo > results = new ArrayList < Todo > ( ) ; try { View todoView = this . database . getView ( "todos" ) ; Query query = todoView . createQuery ( ) ; QueryEnumerator result = query . run ( ) ; Document document = null ; for ( Iterator < QueryRow > it = result ; it . hasNext ( ) ; ) { QueryRow row = it . next ( ) ; document = row . getDocument ( ) ; results . add ( new Todo ( document . getId ( ) , ( String ) document . getProperty ( "title" ) , ( String ) document . getProperty ( "description" ) ) ) ; } } catch ( Exception e ) { e . printStackTrace ( ) ; } return results ; } }

The above was a lot to take in, but it was necessary in order to avoid confusion.

Inside the CouchbaseSingleton class there are four private variables. The database manager will allow us to open our database as well as create it. The replication objects are responsible for synchronization in either direction.

In the constructor method we create and open a database called fx-project and configure a view that we'll use when it comes to querying for data. This view called todos will emit a key-value pair of document id and document for every document that is stored in the local database. The constructor method is private because we instantiate it through the static method getInstance .

While we won't be looking at synchronization until later in the guide, it is a good idea to lay the foundation. Essentially we just want to define that we'll have continuous replication to and from a particular sync gateway URL. We also want to be able to stop replication when the application closes. This brings us to our methods for saving and loading data.

public Todo save(Todo todo) { Map<String, Object> properties = new HashMap<String, Object>(); Document document = this.database.createDocument(); properties.put("type", "todo"); properties.put("title", todo.getTitle()); properties.put("description", todo.getDescription()); try { todo.setDocumentId(document.putProperties(properties).getDocument().getId()); } catch (Exception e) { e.printStackTrace(); } return todo; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Todo save ( Todo todo ) { Map < String , Object > properties = new HashMap < String , Object > ( ) ; Document document = this . database . createDocument ( ) ; properties . put ( "type" , "todo" ) ; properties . put ( "title" , todo . getTitle ( ) ) ; properties . put ( "description" , todo . getDescription ( ) ) ; try { todo . setDocumentId ( document . putProperties ( properties ) . getDocument ( ) . getId ( ) ) ; } catch ( Exception e ) { e . printStackTrace ( ) ; } return todo ; }

Our save method will take a Todo object and save it into the database. The result of which will be a Todo object that contains the document id that gets returned to the calling method. This Todo class is simple. It accepts basic information like id, title, and description, and has the appropriate getter and setter methods that match. For reference, it looks like the following and exists in the project's src/main/java/com/couchbase/Todo.java file.

package com.couchbase; import java.util.*; public class Todo { private String documentId; private String title; private String description; Todo(String documentId, String title, String description) { this.documentId = documentId; this.title = title; this.description = description; } Todo(String title, String description) { this.documentId = UUID.randomUUID().toString(); this.title = title; this.description = description; } public void setDocumentId(String documentId) { this.documentId = documentId; } public String getDocumentId() { return this.documentId; } public String getTitle() { return this.title; } public String getDescription() { return this.description; } } 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 package com . couchbase ; import java . util . * ; public class Todo { private String documentId ; private String title ; private String description ; Todo ( String documentId , String title , String description ) { this . documentId = documentId ; this . title = title ; this . description = description ; } Todo ( String title , String description ) { this . documentId = UUID . randomUUID ( ) . toString ( ) ; this . title = title ; this . description = description ; } public void setDocumentId ( String documentId ) { this . documentId = documentId ; } public String getDocumentId ( ) { return this . documentId ; } public String getTitle ( ) { return this . title ; } public String getDescription ( ) { return this . description ; } }

This leaves us with our last data related function, the query function.

public ArrayList<Todo> query() { ArrayList<Todo> results = new ArrayList<Todo>(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator<QueryRow> it = result; it.hasNext(); ) { QueryRow row = it.next(); document = row.getDocument(); results.add(new Todo(document.getId(), (String) document.getProperty("title"), (String) document.getProperty("description"))); } } catch (Exception e) { e.printStackTrace(); } return results; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public ArrayList < Todo > query ( ) { ArrayList < Todo > results = new ArrayList < Todo > ( ) ; try { View todoView = this . database . getView ( "todos" ) ; Query query = todoView . createQuery ( ) ; QueryEnumerator result = query . run ( ) ; Document document = null ; for ( Iterator < QueryRow > it = result ; it . hasNext ( ) ; ) { QueryRow row = it . next ( ) ; document = row . getDocument ( ) ; results . add ( new Todo ( document . getId ( ) , ( String ) document . getProperty ( "title" ) , ( String ) document . getProperty ( "description" ) ) ) ; } } catch ( Exception e ) { e . printStackTrace ( ) ; } return results ; }

Remember that view that we created? This time we're querying it. The result set will be loaded into an array of Todo objects. This brings our data layer to a close, allowing us to focus on the actual application development.

Designing the Desktop Application

While not required, the JavaFX application, SceneBuilder, makes it very simple to create a graphical UI and corresponding controller class. If you choose not to use it, open your project's src/main/resources/TodoFX.fxml and include the following XML markup:

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import java.lang.*?> <?import javafx.scene.layout.*?> <Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.couchbase.TodoFXController"> <children> <ListView fx:id="fxListView" layoutX="10.0" layoutY="10.0" prefHeight="380.0" prefWidth="300.0" /> <TextField fx:id="fxTitle" layoutX="320.0" layoutY="10.0" prefHeight="25.0" prefWidth="270.0" promptText="Title" /> <TextArea fx:id="fxDescription" layoutX="320.0" layoutY="45.0" prefHeight="200.0" prefWidth="270.0" promptText="Description" /> <Button fx:id="fxSave" layoutX="530.0" layoutY="365.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="60.0" text="Save" /> </children> </Pane> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <? xml version = "1.0" encoding = "UTF-8" ?> <? import javafx . scene . control . * ?> <? import java . lang . * ?> <? import javafx . scene . layout . * ?> < Pane maxHeight = "-Infinity" maxWidth = "-Infinity" minHeight = "-Infinity" minWidth = "-Infinity" prefHeight = "400.0" prefWidth = "600.0" xmlns = "http://javafx.com/javafx/8" xmlns : fx = "http://javafx.com/fxml/1" fx : controller = "com.couchbase.TodoFXController" > < children > < ListView fx : id = "fxListView" layoutX = "10.0" layoutY = "10.0" prefHeight = "380.0" prefWidth = "300.0" / > < TextField fx : id = "fxTitle" layoutX = "320.0" layoutY = "10.0" prefHeight = "25.0" prefWidth = "270.0" promptText = "Title" / > < TextArea fx : id = "fxDescription" layoutX = "320.0" layoutY = "45.0" prefHeight = "200.0" prefWidth = "270.0" promptText = "Description" / > < Button fx : id = "fxSave" layoutX = "530.0" layoutY = "365.0" mnemonicParsing = "false" prefHeight = "25.0" prefWidth = "60.0" text = "Save" / > < / children > < / Pane >

The above markup will give us a UI that looks like the following:

Nothing too fancy in the above, correct?

We have a simple UI with a list, two input fields, and a save button, as described in the XML markup. In the markup we also reference com.couchbase.TodoFXController . This is the logic that will be bound to the particular FX view. Open the project's src/main/java/com/couchbase/TodoFXController.java and include the following:

package com.couchbase; import com.couchbase.lite.*; import com.couchbase.lite.Database.ChangeListener; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.util.Callback; import java.net.URL; import java.util.*; public class TodoFXController implements Initializable { private CouchbaseSingleton couchbase; @FXML private TextField fxTitle; @FXML private TextArea fxDescription; @FXML private ListView fxListView; @FXML private Button fxSave; @Override public void initialize(URL fxmlFileLocation, ResourceBundle resources) { try { this.couchbase = CouchbaseSingleton.getInstance(); fxListView.getItems().addAll(this.couchbase.query()); this.couchbase.getDatabase().addChangeListener(new ChangeListener() { @Override public void changed(Database.ChangeEvent event) { for(int i = 0; i < event.getChanges().size(); i++) { final Document retrievedDocument = couchbase.getDatabase().getDocument(event.getChanges().get(i).getDocumentId()); Platform.runLater(new Runnable() { @Override public void run() { int documentIndex = indexOfByDocumentId(retrievedDocument.getId(), fxListView.getItems()); for(int j = 0; j < fxListView.getItems().size(); j++) { if(((Todo)fxListView.getItems().get(j)).getDocumentId().equals(retrievedDocument.getId())) { documentIndex = j; break; } } if (retrievedDocument.isDeleted()) { if (documentIndex > -1) { fxListView.getItems().remove(documentIndex); } } else { if (documentIndex == -1) { fxListView.getItems().add(new Todo(retrievedDocument.getId(), (String) retrievedDocument.getProperty("title"), (String) retrievedDocument.getProperty("description"))); } else { fxListView.getItems().remove(documentIndex); fxListView.getItems().add(new Todo(retrievedDocument.getId(), (String) retrievedDocument.getProperty("title"), (String) retrievedDocument.getProperty("description"))); } } } }); } } }); } catch (Exception e) { e.printStackTrace(); } fxListView.setCellFactory(new Callback<ListView<Todo>, ListCell<Todo>>(){ @Override public ListCell<Todo> call(ListView<Todo> p) { ListCell<Todo> cell = new ListCell<Todo>(){ @Override protected void updateItem(Todo t, boolean bln) { super.updateItem(t, bln); if (t != null) { setText(t.getTitle()); } } }; return cell; } }); fxSave.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent e) { if(!fxTitle.getText().equals("") && !fxDescription.getText().equals("")) { fxListView.getItems().add(couchbase.save(new Todo(fxTitle.getText(), fxDescription.getText()))); fxTitle.setText(""); fxDescription.setText(""); } else { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Missing Information"); alert.setHeaderText(null); alert.setContentText("Both a title and description are required for this example."); alert.showAndWait(); } } }); } private int indexOfByDocumentId(String needle, ObservableList<Todo> haystack) { int result = -1; for(int i = 0; i < haystack.size(); i++) { if(haystack.get(i).getDocumentId().equals(needle)) { result = i; break; } } return result; } } 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 package com . couchbase ; import com . couchbase . lite . * ; import com . couchbase . lite . Database . ChangeListener ; import javafx . application . Platform ; import javafx . collections . ObservableList ; import javafx . event . ActionEvent ; import javafx . event . EventHandler ; import javafx . fxml . FXML ; import javafx . fxml . Initializable ; import javafx . scene . control . * ; import javafx . util . Callback ; import java . net . URL ; import java . util . * ; public class TodoFXController implements Initializable { private CouchbaseSingleton couchbase ; @ FXML private TextField fxTitle ; @ FXML private TextArea fxDescription ; @ FXML private ListView fxListView ; @ FXML private Button fxSave ; @ Override public void initialize ( URL fxmlFileLocation , ResourceBundle resources ) { try { this . couchbase = CouchbaseSingleton . getInstance ( ) ; fxListView . getItems ( ) . addAll ( this . couchbase . query ( ) ) ; this . couchbase . getDatabase ( ) . addChangeListener ( new ChangeListener ( ) { @ Override public void changed ( Database . ChangeEvent event ) { for ( int i = 0 ; i < event . getChanges ( ) . size ( ) ; i ++ ) { final Document retrievedDocument = couchbase . getDatabase ( ) . getDocument ( event . getChanges ( ) . get ( i ) . getDocumentId ( ) ) ; Platform . runLater ( new Runnable ( ) { @ Override public void run ( ) { int documentIndex = indexOfByDocumentId ( retrievedDocument . getId ( ) , fxListView . getItems ( ) ) ; for ( int j = 0 ; j < fxListView . getItems ( ) . size ( ) ; j ++ ) { if ( ( ( Todo ) fxListView . getItems ( ) . get ( j ) ) . getDocumentId ( ) . equals ( retrievedDocument . getId ( ) ) ) { documentIndex = j ; break ; } } if ( retrievedDocument . isDeleted ( ) ) { if ( documentIndex > - 1 ) { fxListView . getItems ( ) . remove ( documentIndex ) ; } } else { if ( documentIndex == - 1 ) { fxListView . getItems ( ) . add ( new Todo ( retrievedDocument . getId ( ) , ( String ) retrievedDocument . getProperty ( "title" ) , ( String ) retrievedDocument . getProperty ( "description" ) ) ) ; } else { fxListView . getItems ( ) . remove ( documentIndex ) ; fxListView . getItems ( ) . add ( new Todo ( retrievedDocument . getId ( ) , ( String ) retrievedDocument . getProperty ( "title" ) , ( String ) retrievedDocument . getProperty ( "description" ) ) ) ; } } } } ) ; } } } ) ; } catch ( Exception e ) { e . printStackTrace ( ) ; } fxListView . setCellFactory ( new Callback < ListView < Todo > , ListCell < Todo > > ( ) { @ Override public ListCell < Todo > call ( ListView < Todo > p ) { ListCell < Todo > cell = new ListCell < Todo > ( ) { @ Override protected void updateItem ( Todo t , boolean bln ) { super . updateItem ( t , bln ) ; if ( t ! = null ) { setText ( t . getTitle ( ) ) ; } } } ; return cell ; } } ) ; fxSave . setOnAction ( new EventHandler < ActionEvent > ( ) { @ Override public void handle ( ActionEvent e ) { if ( ! fxTitle . getText ( ) . equals ( "" ) && !fxDescription.getText().equals("")) { fxListView.getItems().add(couchbase.save(new Todo(fxTitle.getText(), fxDescription.getText()))); fxTitle . setText ( "" ) ; fxDescription . setText ( "" ) ; } else { Alert alert = new Alert ( Alert . AlertType . INFORMATION ) ; alert . setTitle ( "Missing Information" ) ; alert . setHeaderText ( null ) ; alert . setContentText ( "Both a title and description are required for this example." ) ; alert . showAndWait ( ) ; } } } ) ; } private int indexOfByDocumentId ( String needle , ObservableList < Todo > haystack ) { int result = - 1 ; for ( int i = 0 ; i < haystack . size ( ) ; i ++ ) { if ( haystack . get ( i ) . getDocumentId ( ) . equals ( needle ) ) { result = i ; break ; } } return result ; } }

There is a lot to take in when it comes to the above code, so we're going to break it down.

@FXML private TextField fxTitle; @FXML private TextArea fxDescription; @FXML private ListView fxListView; @FXML private Button fxSave; 1 2 3 4 5 6 7 8 9 10 11 12 13 @ FXML private TextField fxTitle ; @ FXML private TextArea fxDescription ; @ FXML private ListView fxListView ; @ FXML private Button fxSave ;

Remember the FXML file? Each of the above variables are mapped to the components of that file. However, what we really care about is the use of the Initializable interface that we're implementing. This requires an initialize function where we'll setup our component and database listeners.

fxListView.setCellFactory(new Callback<ListView<Todo>, ListCell<Todo>>(){ @Override public ListCell<Todo> call(ListView<Todo> p) { ListCell<Todo> cell = new ListCell<Todo>(){ @Override protected void updateItem(Todo t, boolean bln) { super.updateItem(t, bln); if (t != null) { setText(t.getTitle()); } } }; return cell; } }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fxListView . setCellFactory ( new Callback < ListView < Todo > , ListCell < Todo > > ( ) { @ Override public ListCell < Todo > call ( ListView < Todo > p ) { ListCell < Todo > cell = new ListCell < Todo > ( ) { @ Override protected void updateItem ( Todo t , boolean bln ) { super . updateItem ( t , bln ) ; if ( t ! = null ) { setText ( t . getTitle ( ) ) ; } } } ; return cell ; } } ) ;

Because we're using a custom Todo class in our list, we need to configure how the list rows show data. By default they are strings, but we actually want to extract the title of any of our data elements and show that instead.

this.couchbase = CouchbaseSingleton.getInstance(); fxListView.getItems().addAll(this.couchbase.query()); 1 2 3 4 this . couchbase = CouchbaseSingleton . getInstance ( ) ; fxListView . getItems ( ) . addAll ( this . couchbase . query ( ) ) ;

In the above we are obtaining the open database instance, performing a query, and adding all the data to the FX list that is on the screen. This is a one time thing that is done when the application loads. All future data loads are done through a data listener.

The database change listener will listen for all changes in data for as long as the application is open. The changes can happen in bulk, so we loop through the changes and retrieve the documents. If the document is new, add it to the list. If the document has a deleted indicator, remove it from the list. If the document is a change to an existing document, remove the old and add the new one to the list. This is all done in a Platform.runLater because the Couchbase change listener happens on a background thread. The changes to the UI must be done on the main thread.

Finally we have our save button that has its own click event. If clicked, and the input elements are populated, save the data to the database.

As of right now, we have an offline desktop application that saves data in Couchbase Lite. Now we can worry about data synchronization / replication.

Synchronizing Data Between Desktop and Server

The next part is easy thanks to Couchbase Mobile. You will need Couchbase Sync Gateway up and running before we start messing with the desktop application code. With Sync Gateway installed, create the following basic configuration file:

{ "log":["CRUD+", "REST+", "Changes+", "Attach+"], "databases": { "fx-example": { "server":"walrus:", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "log" : [ "CRUD+" , "REST+" , "Changes+" , "Attach+" ] , "databases" : { "fx-example" : { "server" : "walrus:" , "sync" : ` function ( doc ) { channel ( doc . channels ) ; } ` , "users" : { "GUEST" : { "disabled" : false , "admin_channels" : [ "*" ] } } } } }

The above configuration file is pretty much as basic as you can get. Create an fx-project partition with no read / write rules using the in-memory database. Note we're not using Couchbase Server here, but we could. Run this configuration file with Sync Gateway.

Bouncing back into the JavaFX application. Open the project's src/main/java/com/couchbase/Main.java file and include the following:

this.couchbase = CouchbaseSingleton.getInstance(); this.couchbase.startReplication(new URL("http://localhost:4984/fx-example/"), true); 1 2 3 4 this . couchbase = CouchbaseSingleton . getInstance ( ) ; this . couchbase . startReplication ( new URL ( "http://localhost:4984/fx-example/" ) , true ) ;

The above should be included in the start method. It will start the bi-directional replication process to our now running Sync Gateway instance.

Conclusion

You just saw how to create a simple JavaFX application that stores data in Couchbase Lite and synchronizes the data. To run this application using Maven and the command line, execute the following:

mvn jfx:run 1 2 3 mvn jfx : run

Because Couchbase is so flexible, this same application can be extended to mobile with very few changes to the application code. If you'd like to download the full project in this guide, it can be found on GitHub here.