Passei bastante tempo trabalhando com o Couchbase e aplicativos Web usando o incrível Java SDK, mas você sabia que também é possível criar aplicativos de desktop que usam o Couchbase para armazenar dados? As estruturas de desktop Java, como o JavaFX, podem usar o SDK Java do Couchbase, mas provavelmente não é uma boa ideia usar um SDK destinado ao servidor em seu aplicativo voltado para o cliente. Em vez disso, você pode usar a solução móvel do Couchbase para criar aplicativos de desktop voltados para o cliente. Ela usa as mesmas APIs do Android, mas foi projetada com os desktops em mente.
Vamos dar uma olhada na criação de um aplicativo de desktop simples usando JavaFX, Couchbase Lite e até mesmo o Couchbase Sync Gateway para sincronizar esses dados entre computadores.
Os requisitos
Existem alguns requisitos que devem ser atendidos para que esse projeto seja bem-sucedido.
- JDK 1.7+
- Maven
- Gateway de sincronização do Couchbase
Este projeto usará o Maven para reunir nossas dependências e criar um arquivo JAR. Embora o Sync Gateway não seja realmente um requisito, ele o será se você desejar adicionar suporte à sincronização ao seu aplicativo.
Criação de um novo projeto JavaFX com o Maven
Precisamos criar um projeto Maven básico. Isso pode ser feito em um IDE de sua escolha ou manualmente. Essencialmente, precisaremos da seguinte estrutura de arquivos e diretórios:
|
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 |
Entraremos em detalhes sobre o conteúdo de cada arquivo quando começarmos a desenvolver. Por enquanto, precisamos configurar nosso Maven pom.xml para que ele obtenha as dependências necessárias.
Abra o arquivo pom.xml e inclua o seguinte:
|
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 |
com.couchbase couchbase-javafx-example 4.0.0 Couchbase JavaFX Example jar 1.0 com.couchbase.lite couchbase-lite-java 1.3.0 com.couchbase.lite couchbase-lite-java-sqlcipher 1.3.0 junit junit 4.12 test com.zenjava javafx-maven-plugin 8.5.0 com.couchbase.Main org.apache.maven.plugins maven-compiler-plugin 3.3 1.7 1.7 maven-assembly-plugin 2.2.1 jar-with-dependencies true lib/ com.couchbase.Main make-assembly package single |
Sem entrar em muitos detalhes desnecessários no arquivo Maven acima, queremos prestar atenção a algumas coisas em particular.
|
1 2 3 4 5 6 7 |
com.couchbase.lite couchbase-lite-java 1.3.0 |
A dependência acima diz que estamos incluindo o Couchbase Lite em nosso projeto. Lembre-se de que o Couchbase Lite é um banco de dados local que não fica em um servidor em algum lugar. Os dados são armazenados localmente e o componente é empacotado em seu aplicativo.
Também queremos prestar atenção ao seguinte plug-in:
|
1 2 3 4 5 6 7 8 9 10 |
com.zenjava javafx-maven-plugin 8.5.0 com.couchbase.Main |
O plug-in acima serve para criar um projeto JavaFX. É claro que essa criação de projeto é muito mais fácil quando se usa um IDE como o IntelliJ, embora não seja necessário.
Criando a classe singleton do Couchbase
Antes de nos dedicarmos a criar a interface do usuário e os controladores para o nosso projeto JavaFX, vamos nos preocupar com a forma como os dados serão tratados.
Para simplificar, é uma ótima ideia criar uma classe singleton para gerenciar os dados em todo o nosso projeto. Isso também funciona muito bem ao configurar ouvintes de dados para evitar a necessidade de escrever consultas em todos os lugares do aplicativo. Vamos abrir a seção src/main/java/com/couchbase/CouchbaseSingleton.java e inclua o código a seguir. Depois, vamos detalhá-lo.
|
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 query() { ArrayList results = new ArrayList(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator 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; } } |
O que foi dito acima foi muito difícil de assimilar, mas foi necessário para evitar confusão.
Dentro do CouchbaseSingleton existem quatro variáveis privadas. O gerenciador de banco de dados nos permitirá abrir nosso banco de dados e também criá-lo. Os objetos de replicação são responsáveis pela sincronização em qualquer direção.
No construtor criamos e abrimos um banco de dados chamado projeto fx e configurar uma visualização que usaremos para consultar os dados. Essa visualização chamada todos emitirá um par de valores-chave de id de documento e documento para cada documento armazenado no banco de dados local. O construtor é privado porque o instanciamos por meio do método estático getInstance.
Embora não abordaremos a sincronização até o final do guia, é uma boa ideia estabelecer a base. Essencialmente, queremos apenas definir que teremos uma replicação contínua de e para um determinado URL de gateway de sincronização. Também queremos poder interromper a replicação quando o aplicativo for fechado. Isso nos leva aos nossos métodos para salvar e carregar dados.
|
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; } |
Nosso salvar receberá um método Todo e salvá-lo no banco de dados. O resultado disso será um objeto Todo que contém o ID do documento que é retornado ao método de chamada. Esse Todo é simples. Ela aceita informações básicas como id, título e descrição, e tem os métodos getter e setter apropriados que correspondem. Para referência, ela se parece com o seguinte e existe na seção src/main/java/com/couchbase/Todo.java arquivo.
|
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; } } |
Isso nos deixa com nossa última função relacionada a dados, a consulta função.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public ArrayList query() { ArrayList results = new ArrayList(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator 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; } |
Lembra-se da visualização que criamos? Desta vez, estamos consultando-a. O conjunto de resultados será carregado em uma matriz de Todo objetos. Isso traz nossa camada de dados permitindo que nos concentremos no desenvolvimento real do aplicativo.
Projetando o aplicativo de desktop
Embora não seja obrigatório, o aplicativo JavaFX, Criador de cenasO recurso de controle de interface do usuário (UI), torna muito simples a criação de uma UI gráfica e da classe controladora correspondente. Se você optar por não usá-lo, abra o arquivo src/main/resources/TodoFX.fxml e inclua a seguinte marcação XML:
|
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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
<!--?xml version="1.0" encoding="UTF-8"?--> <!--?import javafx.scene.control.*?--> <!--?import java.lang.*?--> <!--?import javafx.scene.layout.*?--> <textarea> <Button fx:id="fxSave" layoutX="530.0" layoutY="365.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="60.0" text="Save" /> </children> </Pane> </code> </pre> <p>The above markup will give us a UI that looks like the following:</p> <p><img src="/wp-content/original-assets/2016/september/using-couchbase-in-a-javafx-desktop-application/todo-javafx-example.png" /></p> <p>Nothing too fancy in the above, correct?</p> <p>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 <code>com.couchbase.TodoFXController</code>. This is the logic that will be bound to the particular FX view. Open the project's <strong>src/main/java/com/couchbase/TodoFXController.java</strong> and include the following:</p> <pre> <code> 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; } } </code> </pre> <p>There is a lot to take in when it comes to the above code, so we're going to break it down.</p> <pre> <code> @FXML private TextField fxTitle; @FXML private TextArea fxDescription; @FXML private ListView fxListView; @FXML private Button fxSave; </code> </pre> <p>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 <code>Initializable</code> interface that we're implementing. This requires an <code>initialize</code> function where we'll setup our component and database listeners.</p> <pre> <code> 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; } }); </code> </pre> <p>Because we're using a custom <code>Todo</code> 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.</p> <pre> <code> this.couchbase = CouchbaseSingleton.getInstance(); fxListView.getItems().addAll(this.couchbase.query()); </code> </pre> <p>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.</p> <p>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 <code>Platform.runLater</code> because the Couchbase change listener happens on a background thread. The changes to the UI must be done on the main thread.</p> <p>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.</p> <p>As of right now, we have an offline desktop application that saves data in Couchbase Lite. Now we can worry about data synchronization / replication.</p> <h2>Synchronizing Data Between Desktop and Server</h2> <p>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:</p> <pre> <code> { "log":["CRUD+", "REST+", "Changes+", "Attach+"], "databases": { "fx-example": { "server":"walrus:", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } } </code> </pre> <p>The above configuration file is pretty much as basic as you can get. Create an <code>fx-project</code> 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.</p> <p>Bouncing back into the JavaFX application. Open the project's <strong>src/main/java/com/couchbase/Main.java</strong> file and include the following:</p> <pre> <code> this.couchbase = CouchbaseSingleton.getInstance(); this.couchbase.startReplication(new URL("https://localhost:4984/fx-example/"), true); </code> </pre> <p>The above should be included in the <code>start</code> method. It will start the bi-directional replication process to our now running Sync Gateway instance.</p> <h2>Conclusion</h2> <p>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:</p> <pre> <code> mvn jfx:run </code> </pre> <p>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 <a href="https://github.com/couchbaselabs/couchbase-lite-javafx-example">here</a>.</p> </textarea> |