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 |
paquete com.couchbase.móvil; importar com.couchbase.lite.*; importar com.fasterxml.jackson.núcleo.JsonProcessingException; importar javafx.aplicación.Plataforma; importar javafx.judías.valor.ChangeListener; importar javafx.judías.valor.ObservableValue; importar javafx.colecciones.FXCollections; importar javafx.colecciones.ObservableList; importar javafx.evento.AcciónEvento; importar javafx.fxml.FXML; importar javafx.escena.control.*; importar javafx.escena.control.Campo de texto; importar java.io.IOException; importar java.red.MalformedURLException; importar java.red.URL; importar java.util.Mapa; importar estático com.couchbase.móvil.Tiempo de ejecución.mapeador; público clase Controlador implementa LiveQuery.ChangeListener, ChangeListener, SGMonitor.ChangesFeedListener, Servicio DBS.ReplicationStateListener { privado estático final Cadena SYNC_GATEWAY_HOST = "http://localhost"; privado estático final Cadena SG_PUBLIC_URL = SYNC_GATEWAY_HOST + ":4984/" + Servicio DBS.BASE DE DATOS; privado estático final Cadena SG_ADMIN_URL = SYNC_GATEWAY_HOST + ":4985/" + Servicio DBS.BASE DE DATOS; privado estático final Cadena TOGGLE_INACTIVE = "-fx-background-color: #e6555d;"; privado estático final Cadena TOGGLE_ACTIVE = "-fx-background-color: #ade6a6;"; privado estático final Cadena TOGGLE_DISABLED = "-fx-background-color: #555555;"; @FXML privado ListView lista de documentos; privado ObservableList documentos = FXCollections.observableArrayList(); @FXML privado Área de texto contenidoTexto; @FXML privado Área de texto changesFeed; @FXML privado Campo de texto nombredeusuarioTexto; @FXML privado Campo de texto contraseñaTexto; @FXML privado ToggleButton applyCredentialsBtn; @FXML privado ToggleButton syncBtn; privado Servicio DBS servicio = Servicio DBS.getInstance(); privado Base de datos db = servicio.getDatabase(); privado SGMonitor cambiosMonitor; privado 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 privado void inicializar() { documentListInitialize(); lista de documentos.setItems(documentos); setState(applyCredentialsBtn, falso); setState(syncBtn, falso); servicio.addReplicationStateListener(este); cambiosMonitor = nuevo SGMonitor(SG_ADMIN_URL, "falso", "true", "0", "all_docs", este); cambiosMonitor.iniciar(); } privado void documentListInitialize() { Consulta consulta = db.createAllDocumentsQuery(); consulta.setAllDocsMode(Consulta.AllDocsMode.INCLUIR_BORRADO); liveQuery = consulta.toLiveQuery(); liveQuery.addChangeListener(este); liveQuery.iniciar(); lista de documentos.getSelectionModel().selectedItemProperty().addListener(este); } |
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 público void cambiado(LiveQuery.Evento de cambio evento) { si (evento.getSource().es igual a(liveQuery)) { Plataforma.runLater(() -> { QueryEnumerator filas = evento.getRows(); documentos.borrar(); filas.paraCada(queryRow -> documentos.añada(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 público void cambiado(ObservableValue observable, Cadena oldId, Cadena newId) { si (null == newId) devolver; Mapa propiedades = db.getDocument(newId).getProperties(); pruebe { Cadena json = mapeador.writeValueAsString(propiedades); contenidoTexto.setText(prettyText(json)); } captura (JsonProcessingException ex) { ex.printStackTrace(); Diálogo.mostrar(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 público void onResponse(Cadena cuerpo) { changesFeed.appendText(prettyText((Cadena) cuerpo)); } |
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 público void onChange(booleano 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 privado void applyCredentialsToggled(AcciónEvento evento) { Cadena nombre de usuario = null; Cadena contraseña = null; si (applyCredentialsBtn.isSelected()) { nombre de usuario = nombredeusuarioTexto.getText(); contraseña = contraseñaTexto.getText(); } Servicio DBS.getInstance().setCredentials(nombre de usuario, contraseña); 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 privado void saveContentsClicked(AcciónEvento evento) { Mapa propiedades = null; Documento documento; pruebe { propiedades = mapeador.readValue(contenidoTexto.getText(), Mapa.clase); } captura (IOException ex) { ex.printStackTrace(); Diálogo.mostrar(ex); } si (propiedades.containsKey("_id")) { documento = db.getDocument((Cadena) propiedades.consiga("_id")); } si no { documento = db.crearDocumento(); } pruebe { documento.putProperties(propiedades); } captura (CouchbaseLiteException ex) { ex.printStackTrace(); Diálogo.mostrar(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 _id
Supongo 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 privado void syncToggled(AcciónEvento evento) { pruebe { syncBtn.setDisable(verdadero); syncBtn.setStyle(TOGGLE_DISABLED); servicio.toggleReplication(nuevo URL(SG_PUBLIC_URL), verdadero); } captura (Excepción ex) { ex.printStackTrace(); Diálogo.mostrar(ex); syncBtn.setDisable(falso); } } |
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 privado void exitClicked(AcciónEvento evento) { // Intenta apagar todo con elegancia cambiosMonitor.stop(); liveQuery.stop(); servicio.stopReplication(); db.cerrar(); db.getManager().cerrar(); Plataforma.salida(); } privado void setState(ToggleButton btn, booleano activo) { btn.setSelected(activo); btn.setStyle(activo ? TOGGLE_ACTIVE : TOGGLE_INACTIVE); btn.setDisable(falso); } privado Cadena prettyText(Cadena json) { Cadena fuera = null; pruebe { Objeto objeto = mapeador.readValue(json, Objeto.clase); fuera = mapeador.writerWithDefaultPrettyPrinter().writeValueAsString(objeto); } captura (Excepción ex) { ex.printStackTrace(); } devolver fuera; } } |
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 |
paquete com.couchbase.móvil; importar com.couchbase.lite.Base de datos; importar com.couchbase.lite.JavaContext; importar com.couchbase.lite.Director; importar com.couchbase.lite.auth.Autentificador; importar com.couchbase.lite.auth.AuthenticatorFactory; importar com.couchbase.lite.replicador.Replicación; importar com.couchbase.lite.replicador.ReplicationState; importar java.red.URL; importar java.util.ArrayList; importar java.util.Lista; público clase Servicio DBS implementa Replicación.ChangeListener { público estático final Cadena BASE DE DATOS = "db"; privado estático final Cadena DB_DIRECTORY = "datos"; privado Director director; privado Base de datos base de datos; privado Replicación pushReplication = null; privado Replicación pullReplication = null; privado booleano replicaciónActiva = falso; privado Lista stateListeners = nuevo ArrayList(); privado Cadena nombre de usuario = null; privado Cadena contraseña = null; privado Servicio DBS() { pruebe { director = nuevo Director(nuevo JavaContext(DB_DIRECTORY), Director.OPCIONES_POR_DEFECTO); base de datos = director.getDatabase(BASE DE DATOS); } captura (Excepción ex) { ex.printStackTrace(); } } privado estático clase Soporte { privado estático Servicio DBS INSTANCIA = nuevo Servicio DBS(); } público interfaz ReplicationStateListener { void onChange(booleano isActive); } público estático Servicio DBS getInstance() { devolver Soporte.INSTANCIA; } público Base de datos getDatabase() { devolver base de datos; } público void setCredentials(Cadena nombre de usuario, Cadena contraseña) { este.nombre de usuario = nombre de usuario; este.contraseña = contraseña; } público void toggleReplication(URL pasarela, booleano continuo) { si (replicaciónActiva) { stopReplication(); } si no { startReplication(pasarela, continuo); } } público void startReplication(URL pasarela, booleano continuo) { si (replicaciónActiva) { stopReplication(); } pushReplication = base de datos.createPushReplication(pasarela); pullReplication = base de datos.createPullReplication(pasarela); pushReplication.setContinuous(continuo); pullReplication.setContinuous(continuo); si (nombre de usuario != null) { Autentificador auth = AuthenticatorFactory.createBasicAuthenticator(nombre de usuario, contraseña); pushReplication.setAuthenticator(auth); pullReplication.setAuthenticator(auth); } pushReplication.addChangeListener(este); pullReplication.addChangeListener(este); pushReplication.iniciar(); pullReplication.iniciar(); } público void stopReplication() { si (!replicaciónActiva) devolver; pushReplication.stop(); pullReplication.stop(); pushReplication = null; pullReplication = null; } público void addReplicationStateListener(ReplicationStateListener oyente) { stateListeners.añada(oyente); } público void removeReplicationStateListener(ReplicationStateListener oyente) { stateListeners.eliminar(oyente); } // Replication.ChangeListener @Override público void cambiado(Replicación.Evento de cambio changeEvent) { si (changeEvent.getError() != null) { Throwable últimoError = changeEvent.getError(); Diálogo.mostrar(últimoError.getMessage()); devolver; } si (changeEvent.getTransition() == null) devolver; ReplicationState dest = changeEvent.getTransition().getDestination(); replicaciónActiva = ((dest == ReplicationState.PARADA || dest == ReplicationState.PARADA) ? falso : verdadero); stateListeners.paraCada(oyente -> oyente.onChange(replicaciónActiva)); } } |
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 |
paquete com.couchbase.móvil; importar com.fasterxml.jackson.databind.JsonNode; importar okhttp3.*; importar java.io.IOException; importar java.red.SocketException; importar java.util.concurrente.Unidad de tiempo; importar estático com.couchbase.móvil.Tiempo de ejecución.mapeador; público clase SGMonitor { privado estático final OkHttpClient cliente = nuevo OkHttpClient.Constructor() .readTimeout(1, Unidad de tiempo.DÍAS) .construya(); privado ChangesFeedListener oyente; privado HttpUrl.Constructor urlBuilder; privado Hilo monitorThread; privado Cadena desde = "0"; privado Llame a llame a; SGMonitor(Cadena url, Cadena activeOnly, Cadena incluirDocs, Cadena desde, Cadena estilo, ChangesFeedListener oyente) { este.desde = desde; urlBuilder = HttpUrl.analizar(url).nuevoConstructor() .addPathSegment("_cambios") .addQueryParameter("active_only", activeOnly) .addQueryParameter("incluir_docs", incluirDocs) .addQueryParameter("estilo", estilo) .addQueryParameter("desde", desde) .addQueryParameter("feed", "longpoll") .addQueryParameter("timeout", "0"); este.oyente = oyente; } |
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 |
público interfaz ChangesFeedListener { void onResponse(Cadena cuerpo); } público void iniciar() { monitorThread = nuevo Hilo(() -> { mientras que (!Hilo.interrumpido()) { Solicitar solicitar = nuevo Solicitar.Constructor() .url(urlBuilder.construya()) .construya(); llame a = cliente.newCall(solicitar); pruebe (Respuesta respuesta = llame a.ejecutar()) { si (!respuesta.isSuccessful()) tirar nuevo IOException("Código inesperado" + respuesta); Cadena cuerpo = respuesta.cuerpo().cadena(); JsonNode árbol = mapeador.readTree(cuerpo); desde = árbol.consiga("last_seq").asText(); urlBuilder.setQueryParameter("desde", desde); oyente.onResponse(cuerpo); } captura (SocketException ex) { devolver; } captura (IOException ex) { ex.printStackTrace(); Diálogo.mostrar(ex); } } }); monitorThread.setDaemon(verdadero); monitorThread.iniciar(); } |
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 |
público void stop() { monitorThread.interrumpir(); llame a.cancelar(); } público Cadena getSince() { devolver desde; } } |
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