Introducción
En Couchbase Móvil Pasarela de sincronización changes feed proporciona una forma de monitorizar eventos en un despliegue móvil. El feed permite escribir lógica de negocio sofisticada. Escribí una herramienta para ayudar a examinar y comprender el feed. Puedes leer una introducción y una descripción en primera parte de esta serie de dos partes. El código también sirve de ejemplo para escuchar el feed.
El código
He incluido aquí las clases principales del código de la aplicación. Esta es la primera versión, por lo que puede utilizar un montón de mejoras. Los parámetros están todos cableados. Comprueba el proyecto aquí en GitHub para las actualizaciones. También encontrará instrucciones para crear, ejecutar y empaquetar la aplicación.
JavaFX: La clase Controller
JavaFX divide las aplicaciones sencillas en una clase controladora y una interfaz de usuario declarativa. Vamos a caminar a través del controlador.
|
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 |
package com.couchbase.mobile; import com.couchbase.lite.*; import com.fasterxml.jackson.core.JsonProcessingException; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.control.TextField; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import static com.couchbase.mobile.Runtime.mapper; public class Controller implements LiveQuery.ChangeListener, ChangeListener, SGMonitor.ChangesFeedListener, DBService.ReplicationStateListener { private static final String SYNC_GATEWAY_HOST = "https://localhost"; private static final String SG_PUBLIC_URL = SYNC_GATEWAY_HOST + ":4984/" + DBService.DATABASE; private static final String SG_ADMIN_URL = SYNC_GATEWAY_HOST + ":4985/" + DBService.DATABASE; private static final String TOGGLE_INACTIVE = "-fx-background-color: #e6555d;"; private static final String TOGGLE_ACTIVE = "-fx-background-color: #ade6a6;"; private static final String TOGGLE_DISABLED = "-fx-background-color: #555555;"; @FXML private ListView documentList; private ObservableList documents = FXCollections.observableArrayList(); @FXML private TextArea contentsText; @FXML private TextArea changesFeed; @FXML private TextField usernameText; @FXML private TextField passwordText; @FXML private ToggleButton applyCredentialsBtn; @FXML private ToggleButton syncBtn; private DBService service = DBService.getInstance(); private Database db = service.getDatabase(); private SGMonitor changesMonitor; private LiveQuery liveQuery; |
Esta primera lista muestra un montón de código. Implemento varias escuchas para la interfaz de usuario dentro de la propia clase para reducir el número de archivos. Esto es para fines ilustrativos.
En @FXML marcan todos los campos que el framework vinculará automáticamente a partes de la interfaz de usuario.
A continuación viene la inicialización. JavaFX llama a este método como parte de su ciclo de vida estándar.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@FXML private void initialize() { documentListInitialize(); documentList.setItems(documents); setState(applyCredentialsBtn, false); setState(syncBtn, false); service.addReplicationStateListener(this); changesMonitor = new SGMonitor(SG_ADMIN_URL, "false", "true", "0", "all_docs", this); changesMonitor.start(); } private void documentListInitialize() { Query query = db.createAllDocumentsQuery(); query.setAllDocsMode(Query.AllDocsMode.INCLUDE_DELETED); liveQuery = query.toLiveQuery(); liveQuery.addChangeListener(this); liveQuery.start(); documentList.getSelectionModel().selectedItemProperty().addListener(this); } |
He dividido la inicialización de la lista de documentos en su propia rutina. La lista de documentos se vincula a la lista de documentos variable. A su vez lista de documentos actualizará la interfaz de usuario cada vez que cambie la lista de elementos que le pasemos.
Configuro una consulta en tiempo real para supervisar la base de datos del cliente en busca de cambios. Esto ocurre a través de una consulta "all docs". Una consulta "all docs" no requiere una vista asociada. Configuro INCLUIR_BORRADO para que la herramienta pueda mostrar el aspecto de un documento eliminado en la base de datos.
Con el resto de vinculaciones en su sitio, sólo tenemos que actualizar el archivo documentos lista. Más adelante veremos el listener de consulta en vivo que hace eso.
Las siguientes líneas establecen el estado inicial de un par de botones. Necesito un listener extra para mantener el Sincroniza coherente con el estado real de las réplicas. Más sobre esto más adelante en el artículo.
Escribí una clase separada para monitorear Sync Gateway. El código de inicialización terminó creando una nueva instancia de monitor y arrancando.
La siguiente sección contiene varios oyentes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// LiveQuery.ChangeListener @Override public void changed(LiveQuery.ChangeEvent event) { if (event.getSource().equals(liveQuery)) { Platform.runLater(() -> { QueryEnumerator rows = event.getRows(); documents.clear(); rows.forEach(queryRow -> documents.add(queryRow.getDocumentId())); }); } } |
Aquí está el escuchador de consultas en vivo que es llamado cada vez que la base de datos local cambia. No diseñé la herramienta para trabajar con bases de datos masivas. Por lo tanto, cada vez que los datos cambian, acabo de tomar la fuerza bruta enfoque de volver a leer todos los documentos. El getRows devuelve un enumerador que indexará haciendo justamente eso. JavaFX se encarga de actualizar la interfaz de usuario cuando documentos cambios.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ListView ChangeListener @Override public void changed(ObservableValue observable, String oldId, String newId) { if (null == newId) return; Map properties = db.getDocument(newId).getProperties(); try { String json = mapper.writeValueAsString(properties); contentsText.setText(prettyText(json)); } catch (JsonProcessingException ex) { ex.printStackTrace(); Dialog.display(ex); } } |
Este listener se encarga de rastrear cuando un usuario hace clic en una entrada de la lista de documentos. Las entradas son los ID de los documentos, por lo que podemos utilizar una selección para extraer el documento directamente de la base de datos.
|
1 2 3 4 5 |
// SGMonitor.ChangesFeedListener @Override public void onResponse(String body) { changesFeed.appendText(prettyText((String) body)); } |
He utilizado un enfoque de devolución de llamada para obtener los resultados de la alimentación de cambios. La interfaz está definida en SGMonitor clase. Sólo tiene un método. En esta implementación simplemente tomo el cuerpo de la respuesta del feed y lo añado al texto existente en el panel de texto del feed de cambios. Hay un poco de formato hecho para que sea más fácil de leer, también.
|
1 2 3 4 5 |
// DBService.ReplicationStateListener @Override public void onChange(boolean isActive) { setState(syncBtn, isActive); } |
Finalmente añadí un listener para la actividad de replicación. La interfaz proviene de la clase de ayuda DBService. Escribí un poco sobre la detección del estado de una replicación aquí. Para esta aplicación sólo necesito saber si una replicación se está ejecutando o no para mantener el Sincroniza coherente con el estado del botón. Esto permite gestionar los casos en los que un usuario intenta iniciar una sincronización pero falla. Esto puede ocurrir, por ejemplo, si el usuario tiene que proporcionar credenciales de autenticación y no lo ha hecho.
A continuación tenemos varios métodos vinculados a elementos de la interfaz de usuario. JavaFX se encarga de gran parte del cableado.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@FXML private void applyCredentialsToggled(ActionEvent event) { String username = null; String password = null; if (applyCredentialsBtn.isSelected()) { username = usernameText.getText(); password = passwordText.getText(); } DBService.getInstance().setCredentials(username, password); applyCredentialsBtn.setStyle(applyCredentialsBtn.isSelected() ? TOGGLE_ACTIVE : TOGGLE_INACTIVE); } |
Aquí establezco el uso de credenciales de autenticación cada vez que se activa el botón correspondiente.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@FXML private void saveContentsClicked(ActionEvent event) { Map properties = null; Document document; try { properties = mapper.readValue(contentsText.getText(), Map.class); } catch (IOException ex) { ex.printStackTrace(); Dialog.display(ex); } if (properties.containsKey("_id")) { document = db.getDocument((String) properties.get("_id")); } else { document = db.createDocument(); } try { document.putProperties(properties); } catch (CouchbaseLiteException ex) { ex.printStackTrace(); Dialog.display(ex); } } |
Este código muestra un par de cosas interesantes. Utilizo un Jackson ObjectMapper para convertir el texto del panel de contenido en un mapa de propiedades.
A continuación compruebo si hay una entrada _id. Couchbase Mobile reserva la mayoría de las propiedades que empiezan por "_" para uso del sistema (con excepciones especiales). Si el texto que intentamos convertir contiene _idSupongo que se trata de una edición de un documento existente. De lo contrario, creo un nuevo documento.
En resumen, tenemos un ejemplo de creación y actualización de documentos. Esta no es la forma preferida de actualizar, aunque es suficiente en muchos casos. Puede leer más sobre actualizaciones aquí.
|
1 2 3 4 5 6 7 8 9 10 11 |
@FXML private void syncToggled(ActionEvent event) { try { syncBtn.setDisable(true); syncBtn.setStyle(TOGGLE_DISABLED); service.toggleReplication(new URL(SG_PUBLIC_URL), true); } catch (Exception ex) { ex.printStackTrace(); Dialog.display(ex); syncBtn.setDisable(false); } } |
Reacciona a la activación de Sincroniza botón. Recordemos, sin embargo, que utilizamos un oyente para verificar el estado en otro lugar.
|
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 |
@FXML private void exitClicked(ActionEvent event) { // Try to shut everything down gracefully changesMonitor.stop(); liveQuery.stop(); service.stopReplication(); db.close(); db.getManager().close(); Platform.exit(); } private void setState(ToggleButton btn, boolean active) { btn.setSelected(active); btn.setStyle(active ? TOGGLE_ACTIVE : TOGGLE_INACTIVE); btn.setDisable(false); } private String prettyText(String json) { String out = null; try { Object object = mapper.readValue(json, Object.class); out = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); } catch (Exception ex) { ex.printStackTrace(); } return out; } } |
El resto del código aquí son sólo bits de ayuda y una pieza para cerrar todo antes de salir.
La clase Database Helper
Esto muestra el código para una clase de ayuda de base de datos directa. En su mayor parte me parece que esta clase es un buen paquete de las operaciones típicas necesarias para la gestión de una base de datos y el inicio de un conjunto bidireccional estándar de réplicas. La incluyo aquí porque la encuentro útil y por claridad.
Pongo en práctica el Replication.ChangeListener interfaz. Eso es quizás un poco inusual. Antes he mencionado el motivo. Este enlace le lleva a la entrada del blog al respecto.
|
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 |
package com.couchbase.mobile; import com.couchbase.lite.Database; import com.couchbase.lite.JavaContext; import com.couchbase.lite.Manager; import com.couchbase.lite.auth.Authenticator; import com.couchbase.lite.auth.AuthenticatorFactory; import com.couchbase.lite.replicator.Replication; import com.couchbase.lite.replicator.ReplicationState; import java.net.URL; import java.util.ArrayList; import java.util.List; public class DBService implements Replication.ChangeListener { public static final String DATABASE = "db"; private static final String DB_DIRECTORY = "data"; private Manager manager; private Database database; private Replication pushReplication = null; private Replication pullReplication = null; private boolean replicationActive = false; private List stateListeners = new ArrayList(); private String username = null; private String password = null; private DBService() { try { manager = new Manager(new JavaContext(DB_DIRECTORY), Manager.DEFAULT_OPTIONS); database = manager.getDatabase(DATABASE); } catch (Exception ex) { ex.printStackTrace(); } } private static class Holder { private static DBService INSTANCE = new DBService(); } public interface ReplicationStateListener { void onChange(boolean isActive); } public static DBService getInstance() { return Holder.INSTANCE; } public Database getDatabase() { return database; } public void setCredentials(String username, String password) { this.username = username; this.password = password; } public void toggleReplication(URL gateway, boolean continuous) { if (replicationActive) { stopReplication(); } else { startReplication(gateway, continuous); } } public void startReplication(URL gateway, boolean continuous) { if (replicationActive) { stopReplication(); } pushReplication = database.createPushReplication(gateway); pullReplication = database.createPullReplication(gateway); pushReplication.setContinuous(continuous); pullReplication.setContinuous(continuous); if (username != null) { Authenticator auth = AuthenticatorFactory.createBasicAuthenticator(username, password); pushReplication.setAuthenticator(auth); pullReplication.setAuthenticator(auth); } pushReplication.addChangeListener(this); pullReplication.addChangeListener(this); pushReplication.start(); pullReplication.start(); } public void stopReplication() { if (!replicationActive) return; pushReplication.stop(); pullReplication.stop(); pushReplication = null; pullReplication = null; } public void addReplicationStateListener(ReplicationStateListener listener) { stateListeners.add(listener); } public void removeReplicationStateListener(ReplicationStateListener listener) { stateListeners.remove(listener); } // Replication.ChangeListener @Override public void changed(Replication.ChangeEvent changeEvent) { if (changeEvent.getError() != null) { Throwable lastError = changeEvent.getError(); Dialog.display(lastError.getMessage()); return; } if (changeEvent.getTransition() == null) return; ReplicationState dest = changeEvent.getTransition().getDestination(); replicationActive = ((dest == ReplicationState.STOPPING || dest == ReplicationState.STOPPED) ? false : true); stateListeners.forEach(listener -> listener.onChange(replicationActive)); } } |
La clase Sync Gateway Monitor
Por último, echemos un vistazo a la clase helper para monitorizar Sync Gateway. Voy a caminar a través de esto en pedazos, también.
|
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 |
package com.couchbase.mobile; import com.fasterxml.jackson.databind.JsonNode; import okhttp3.*; import java.io.IOException; import java.net.SocketException; import java.util.concurrent.TimeUnit; import static com.couchbase.mobile.Runtime.mapper; public class SGMonitor { private static final OkHttpClient client = new OkHttpClient.Builder() .readTimeout(1, TimeUnit.DAYS) .build(); private ChangesFeedListener listener; private HttpUrl.Builder urlBuilder; private Thread monitorThread; private String since = "0"; private Call call; SGMonitor(String url, String activeOnly, String includeDocs, String since, String style, ChangesFeedListener listener) { this.since = since; urlBuilder = HttpUrl.parse(url).newBuilder() .addPathSegment("_changes") .addQueryParameter("active_only", activeOnly) .addQueryParameter("include_docs", includeDocs) .addQueryParameter("style", style) .addQueryParameter("since", since) .addQueryParameter("feed", "longpoll") .addQueryParameter("timeout", "0"); this.listener = listener; } |
Utilizo el Biblioteca OkHttp de Square. Actualmente Couchbase Lite también usa esta librería, internamente. OkHttp utiliza un patrón constructor. Preparo una instancia del constructor que usaré durante el resto del código en el constructor de la clase. Puedes leer sobre el significado de todos los parámetros en la sección Documentación de Sync Gateway.
|
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 |
public interface ChangesFeedListener { void onResponse(String body); } public void start() { monitorThread = new Thread(() -> { while (!Thread.interrupted()) { Request request = new Request.Builder() .url(urlBuilder.build()) .build(); call = client.newCall(request); try (Response response = call.execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); String body = response.body().string(); JsonNode tree = mapper.readTree(body); since = tree.get("last_seq").asText(); urlBuilder.setQueryParameter("since", since); listener.onResponse(body); } catch (SocketException ex) { return; } catch (IOException ex) { ex.printStackTrace(); Dialog.display(ex); } } }); monitorThread.setDaemon(true); monitorThread.start(); } |
En iniciar tiene la parte más interesante del código. Hace girar un hilo de fondo. Bajo la configuración del hilo y el código de control ejecuto un bucle continuo. El bucle hace llamadas síncronas a la red. El manejo de errores es simple. Simplemente lanza una excepción si algo va mal.
Sync Gateway responde con cadenas JSON. Puede ver que el código separa la respuesta y analiza el JSON en un archivo JsonNode objeto. Todo esto es para llegar a la última_seq en la respuesta.
Para saber qué enviar a continuación, el feed de cambios se basa en un sencillo mecanismo de secuencia. Deberías tratar esto como un objeto opaco. Tome el valor de última_seq de la respuesta anterior, y establece el desde a ese mismo valor para la siguiente petición.
No hay ningún inconveniente en no suministrar el desde parámetro. Sync Gateway simplemente reproducirá todos los cambios desde el principio si falta. Por eso verás que en este ejemplo, hago un poco de trampa y siempre creo la instancia de la clase con desde a la cadena "0".
En una aplicación del mundo real, es posible que desee tener alguna manera de guardar la última cadena de secuencia que su aplicación ha procesado, en lugar de agitar a través de la historia de cambios cada vez.
El resto del código son sólo un par de métodos cortos.
|
1 2 3 4 5 6 7 8 9 |
public void stop() { monitorThread.interrupt(); call.cancel(); } public String getSince() { return since; } } |
Y eso es todo para las clases principales. Hay otras necesarias para la aplicación completa.
Echa un vistazo a la Repo de GitHub para ver todo el código y las instrucciones para construirlo.
Lea un análisis de la aplicación y cómo utilizarla en primera parte.
Posdata
Encontrará más recursos en nuestra portal para desarrolladores y síganos en Twitter @CouchbaseDev.
Puede enviar preguntas a nuestro foros. Y participamos activamente en Stack Overflow.
Envíame tus preguntas, comentarios, temas que te gustaría ver, etc. a Twitter. @HodGreeley