Introdução
O Couchbase Mobile Gateway de sincronização O feed de alterações oferece uma maneira de monitorar eventos em uma implantação móvel. O feed torna viável a criação de lógica comercial sofisticada. Escrevi uma ferramenta para ajudar a examinar e entender o feed. Você pode ler uma introdução e uma descrição em primeira parte desta série de duas partes. O código também serve como exemplo de escuta do feed.
O código
Incluí aqui as principais classes do código do aplicativo. Esta é a primeira versão, portanto, ela pode receber muitos aprimoramentos. Os parâmetros estão todos conectados. Confira o projeto aqui no GitHub para obter atualizações. Você também pode encontrar instruções para criar, executar e empacotar o aplicativo lá.
JavaFX: A classe Controller
O JavaFX divide aplicativos simples em uma classe controladora e uma interface de usuário declarativa. Vamos examinar o 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; |
Essa primeira listagem mostra um monte de código padrão. Implemento vários ouvintes para a interface do usuário dentro da própria classe para reduzir o número de arquivos. Isso é para fins ilustrativos.
O @FXML marcam todos os campos que a estrutura associará automaticamente a partes da interface do usuário.
Em seguida, vem a inicialização. O JavaFX chama esse método como parte de seu ciclo de vida padrão.
|
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); } |
Separei a inicialização da lista de documentos em sua própria rotina. A lista de documentos é vinculada ao documentList variável. Por sua vez documentList atualizará a interface do usuário sempre que a lista de itens que passamos for alterada.
Configurei uma consulta em tempo real para monitorar o banco de dados do cliente quanto a quaisquer alterações. Isso acontece por meio de uma consulta de "todos os documentos". Uma consulta de todos os documentos não requer uma visualização associada. Eu defino INCLUDE_DELETED para que a ferramenta possa mostrar a aparência de um documento excluído no banco de dados.
Com os outros vínculos em vigor, só precisamos atualizar o documentos lista. Veremos o ouvinte de consulta ao vivo que faz isso mais adiante.
As próximas linhas definem o estado inicial de alguns botões de alternância. Preciso de um ouvinte extra para manter o Sincronização consistente com o estado real das replicações. Mais informações sobre isso ao longo do artigo.
Escrevi uma classe separada para monitorar o Sync Gateway. O código de inicialização foi concluído com a criação de uma nova instância de monitor e seu início.
A próxima seção contém vários ouvintes.
|
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())); }); } } |
Aqui está o ouvinte de consulta ao vivo que é chamado sempre que o banco de dados local é alterado. Eu não projetei a ferramenta para trabalhar com bancos de dados enormes. Portanto, sempre que os dados mudavam, eu simplesmente adotava a abordagem de força bruta de reler todos os documentos. O getRows retorna um enumerador que indexará e fará exatamente isso. O JavaFX se encarrega de atualizar a interface do usuário quando documentos mudanças.
|
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); } } |
Esse ouvinte cuida do rastreamento quando um usuário clica em uma entrada na lista de documentos. As entradas são os IDs dos documentos, portanto, podemos usar uma seleção para extrair o documento diretamente do banco de dados.
|
1 2 3 4 5 |
// SGMonitor.ChangesFeedListener @Override public void onResponse(String body) { changesFeed.appendText(prettyText((String) body)); } |
Usei uma abordagem de retorno de chamada para obter os resultados do feed de alterações. A interface é definida na seção SGMonitor classe. Ela tem apenas um método. Nessa implementação, simplesmente pego o corpo da resposta do feed e o coloco sobre o texto existente no painel de texto do feed de alterações. Também foi feita uma pequena formatação para facilitar a leitura.
|
1 2 3 4 5 |
// DBService.ReplicationStateListener @Override public void onChange(boolean isActive) { setState(syncBtn, isActive); } |
Por fim, adicionei um ouvinte para a atividade de replicação. A interface vem da classe auxiliar DBService. Escrevi um pouco sobre como detectar o estado de uma replicação aqui. Para este aplicativo, só preciso saber se uma replicação está sendo executada ou não para manter a Sincronização estado do botão consistente. Isso lida com casos em que um usuário tenta iniciar uma sincronização, mas ela falha. Isso pode ocorrer se o usuário precisar fornecer credenciais de autenticação, mas não as tiver fornecido, por exemplo.
Em seguida, temos vários métodos vinculados a elementos da interface do usuário. O JavaFX cuida de grande parte da fiação.
|
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); } |
Aqui eu defino o uso de credenciais de autenticação sempre que o botão correspondente for acionado.
|
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); } } |
Esse código mostra alguns itens interessantes. Eu uso um Jackson Mapeador de objetos para converter o texto no painel de conteúdo em um mapa de propriedades.
Em seguida, verifico se há uma entrada _id. O Couchbase Mobile reserva a maioria das propriedades que começam com um "_" para uso do sistema (com exceções especiais). Se o texto que estivermos tentando converter contiver _idSe o documento não for editado, presumo que seja uma edição de um documento existente. Caso contrário, crio um novo documento.
Portanto, em poucas palavras, temos um exemplo de criação e atualização de documentos. Essa não é a maneira preferida de atualizar, embora seja suficiente em muitos casos. Você pode ler mais sobre atualizações aqui.
|
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); } } |
Isso reage à alternância do Sincronização botão. Lembre-se, porém, de que usamos um ouvinte para verificar o estado em outro 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; } } |
O restante do código aqui são apenas bits auxiliares e uma parte para encerrar tudo antes de sair.
A classe Database Helper
Isso mostra o código de uma classe auxiliar de banco de dados simples. Na maioria das vezes, considero essa classe um bom pacote das operações típicas necessárias para gerenciar um banco de dados e iniciar um conjunto bidirecional padrão de replicações. Estou incluindo-a aqui porque a considero útil e para maior clareza.
Eu implemento o Replicação.ChangeListener interface. Isso talvez seja um pouco incomum. Mencionei o motivo anteriormente. Este link leva você à página postagem no blog sobre isso.
|
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)); } } |
A classe Sync Gateway Monitor
Por fim, vamos dar uma olhada na classe auxiliar para monitorar o Sync Gateway. Também vou analisar isso em partes.
|
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; } |
Eu uso o Biblioteca OkHttp da Square. Atualmente, o Couchbase Lite também usa essa biblioteca internamente. O OkHttp usa um padrão de construtor. Preparo uma instância do builder que usarei durante o restante do código no construtor da classe. Você pode ler sobre o significado de todos os parâmetros na seção Documentação do 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(); } |
O iniciar tem a parte mais interessante do código. Ele ativa um thread em segundo plano. Sob a configuração do thread e o código de controle, executo um loop contínuo. O loop faz chamadas de rede síncronas. O tratamento de erros é simples. Basta lançar uma exceção se algo der errado.
O Sync Gateway responde com cadeias de caracteres JSON. Você pode ver que o código separa a resposta e analisa o JSON em um arquivo JsonNode objeto. Isso tudo é para chegar ao última_seq na resposta.
Para rastrear o que deve ser enviado em seguida, o feed de alterações depende de um mecanismo de sequência simples. Você deve tratar isso como um objeto opaco. Pegue o valor de última_seq da resposta anterior e defina o desde para esse mesmo valor na próxima solicitação.
Não há nenhum dano real em não fornecer o desde parâmetro. O Sync Gateway apenas reproduzirá todas as alterações desde o início se ele estiver faltando. É por isso que você verá que, neste exemplo, eu trapaceio um pouco e sempre crio a instância da classe com desde definido como a string "0".
Em um aplicativo do mundo real, talvez você queira ter uma maneira de salvar a última sequência de caracteres que seu aplicativo processou, em vez de ficar percorrendo o histórico de alterações todas as vezes.
O restante do código é apenas um par de métodos curtos.
|
1 2 3 4 5 6 7 8 9 |
public void stop() { monitorThread.interrupt(); call.cancel(); } public String getSince() { return since; } } |
E isso é tudo para as classes principais. Há outras necessárias para o aplicativo completo.
Confira o Repositório do GitHub para ver todo o código e as instruções para criá-lo.
Leia uma discussão sobre o aplicativo e como usá-lo em primeira parte.
Pós-escrito
Você pode encontrar mais recursos em nosso portal do desenvolvedor e nos siga no Twitter @CouchbaseDev.
Você pode postar perguntas em nosso fóruns. E participamos ativamente de Estouro de pilha.
Entre em contato comigo pelo Twitter com perguntas, comentários, tópicos que você gostaria de ver etc. @HodGreeley