Couchbase Lite Java Framework

      +

      GET STARTED

      Couchbase Lite Java enables development and deployment of Couchbase Lite applications to a JVM environment. You can deploy to a desktop or web server (for example, Tomcat), including embedded Tomcat deployments.

      Use this page to familiarize yourself with the product, its components, deployment, and use.

      You will learn:

      We use Intellij IDEA, gradle, Maven and Tomcat as tools to build and run the examples presented in this content. You are free to use the tools of your choice.

      Where to Get Binaries

      Couchbase Lite Java binaries are available for both Community (CE) and Enterprise (EE) editions from the Maven repositories — see: Preparing Your Build Environment for how.

      Alternatively, you can download a version in .zip format — see: Using Downloaded Binaries for how.

      Preparing Your Build Environment

      Prerequisites

      Assumptions

      This section shows how to set up and use Couchbase Lite Java to build desktop and web applications using gradle, Maven, Tomcat and Intellij IDEA Community Edition.

      It assumes a familiarity with these products, however you are free to use your own choice of development tools.

      The contents of your build.gradle file will depend on the type of project you are building, whether you are using Community or Enterprise edition and whether you prefer to use Maven-distributed binaries or a zip download.

      If you wish to use a downloaded zip instead of Maven then see - Using Downloaded Binaries
      If you are deploying to Linux, you will need the Couchbase Lite support library, which is available only on the zip download distributable — see Additional Steps for Linux

      Defining Dependencies and Repositories

      • Console App Development

      • Web App Development

      Steps

      1. Create a project folder

      2. Initialize it for a Gradle Java application

      3. Include the following in the build.gradle file:

        Community edition

        Compile options

        // Required only if your project has some Kotlin source code
        kotlinOptions { jvmTarget = '1.8' }
        
        // Set minimum JVM level to ensure availability of, for example, lambda expressions
        compileOptions {
            targetCompatibility 1.8
            sourceCompatibility 1.8
        
        //   ... other section content as required by user
        }

        Dependencies

        dependencies {
            implementation "com.couchbase.lite:couchbase-lite-java:${version}"
        
        //   ... other section content as required by user
        }
        Enterprise Edition

        Compile options

        // Required only if your project has some Kotlin source code
        kotlinOptions { jvmTarget = '1.8' }
        
        // Set minimum JVM level to ensure availability of, for example, lambda expressions
        compileOptions {
            targetCompatibility 1.8
            sourceCompatibility 1.8
        
        //   ... other section content as required by user
        }

        Dependencies

        dependencies {
            implementation "com.couchbase.lite:couchbase-lite-java-ee:${version}"
        
        //   ... other section content as required by user
            }

        Repositories

        repositories {
            maven {url 'https://mobile.maven.couchbase.com/maven2/dev/'}
        
        //   ... other section content as required by user
            }
      4. Open the project folder in Intellij IDEA and import the gradle settings.

        If you don’t have auto-import set for Gradle projects, then accept the Import Gradle Project prompt that is displayed bottom-right of the screen

        Note the Gradle menu at the extreme right of the screen:

        GradleMenuWebApp

      That’s it. You’re all set to start building your own Couchbase Lite Java applications — see Building a Getting Started App for an example of how to do that.

      Sample build.gradle content
      // Define the main class for the application.
      application {mainClassName = 'gettingstarted.GettingStarted'}
      
      plugins {
          id 'java'
          id 'application'
      }
      
      // Required only if your project has some Kotlin source code
      kotlinOptions { jvmTarget = '1.8' }
      
      // Set minimum JVM level to ensure availability of, for example, lambda expressions
      compileOptions {
          targetCompatibility 1.8
          sourceCompatibility 1.8
      
      //   ... other section content as required by user
      }
      
      dependencies {
          implementation "com.couchbase.lite:couchbase-lite-java-ee:${version}"
      
      //   ... other section content as required by user
          }
      
      repositories {
          maven {url 'https://mobile.maven.couchbase.com/maven2/dev/'}
      
      //   ... other section content as required by user
          }

      This section explains how to set-up a build project to create Couchbase Lite Java web apps using gradle and Intellij IDEA.

      The WebApp Build Environment

      In our examples here we build and deploy web apps using a gradle plugin based on the com.bmuschko.tomcat plugin. It provides a simple Tomcat harness that can be used from within gpIDE Community Edition or the command-line.

      Of course if you are using gpIDE Ultimate Edition you can use its integrated tomcat features to achieve the same results.

      For more about this plugin and its use see Using the Gradle WebApp Plugin

      Running multiple Couchbase Lite Java web apps Each web application has its own *class loader (WebappX). This loads the classes, jars, and resources found in the application’s WEB-INF/classes and WEB-INF/lib folders, together with any shared jar files from $CATALINA_BASE/lib — see tomcat documentation for more.

      So, if you are running multiple Couchbase Lite web applications, deploy your Couchbase Lite Java library <pathToCbl>/libs to $CATALINA_BASE/lib. This means you do not need to deploy it in each web app and minimizes the size of each app.

      Configuring Couchbase Lite logging functionality will affect the logging of all web applications as the common class loader shares Couchbase Lite Console, File and Custom logging functionalities across all web apps.

      For information about building a WAR file see Deploying a WAR file to tomcat

      Prerequisites

      • Ensure your build environment matches the runtime Tomcat environment. Specifically, that the Java and Tomcat versions are the same.

      • If your Tomcat server runs Linux, declare the shared libraries (<pathToCbl>/support —  as discussed in Additional Steps for Linux) in the $CATALINA_HOME/bin/setenv.sh script file.

      • Ensure the Couchbase Lite jars (<pathToCbl>/lib) are on the executable path within Tomcat — see: Running multiple Couchbase Lite Java web apps

        This also means you should declare the dependencies as providedCompile to avoid them being bundled into the WEB-INF/libs folder

      Steps

      1. Create a project folder and initialize it for a Gradle Java application

        gradle init
      2. Create your build.gradle file, including the following repositories and dependencies:

        • For Community edition

          dependencies {
              implementation "com.couchbase.lite:couchbase-lite-java:$2.7"
          
          //   ... other section content as required by user
          }
        • For Enterprise Edition

          repositories {
              maven {url 'https://mobile.maven.couchbase.com/maven2/dev/'}
          
          //   ... other section content as required by user
              }
          dependencies {
              implementation "com.couchbase.lite:couchbase-lite-java-ee:${version}"
          
          //   ... other section content as required by user
              }
      3. Open the project folder in Intellij IDEA and import the gradle settings.

        If you don’t have auto-import set for Gradle projects, then accept the Import Gradle Project prompt that is displayed bottom-right of the screen

        Note the Gradle menu at the extreme right of the screen:

        GradleMenuWebApp

      If you want to deploy your app to a local tomcat container then see Deploying a WAR file to tomcat

      That’s it. You’re all set to start building your own Couchbase Lite Java applications — see Building a Getting Started App for an example of how to do that.

      Sample build.gradle content
      
      ext{
        TOMCAT_VERSION="9.0.24"
      }
      
      apply plugin: 'java'
      apply plugin: 'war'
      apply plugin: 'com.bmuschko.tomcat'
      
      sourceCompatibility = 1.8
      
      buildscript {
          repositories {
              jcenter()
          }
          dependencies {
              classpath 'com.bmuschko:gradle-tomcat-plugin:2.5'
          }
      }
      
      repositories {
          mavenCentral()
      }
      
      dependencies {
          //  Use for Enterprise version
          compileOnly "com.couchbase.lite:couchbase-lite-java-ee:2.7.0"
          //  Use for community versions
          //    compileOnly "com.couchbase.lite:couchbase-lite-java:2.7.0"
          compileOnly "javax.servlet:javax.servlet-api:4.0.1"
          tomcat "org.apache.tomcat.embed:tomcat-embed-core:${TOMCAT_VERSION}",
              "org.apache.tomcat.embed:tomcat-embed-logging-juli:9.0.0.M6",
              "org.apache.tomcat.embed:tomcat-embed-jasper:${TOMCAT_VERSION}"
      }
      
      tomcat {
          httpPort = 8080
          httpProtocol = 'org.apache.coyote.http11.Http11Nio2Protocol'
          ajpProtocol  = 'org.apache.coyote.ajp.AjpNio2Protocol'
          contextPath = '/'
      }
      
      [tomcatRun, tomcatRunWar].each { task ->
          fileTree("libs").visit { FileVisitDetails details ->
              task.additionalRuntimeResources << file(details.file.path)
          }
      }

      Building a Getting Started App

      This section explains how to validate your configured build environment by building a starter app that uses many of Couchbase Lite Java’s more common features.

      The GettingStarted app demonstrates how to use Couchbase Lite Java. Console and Web App versions are available.

      • Console App

      • Web App

      Ensure you added the Couchbase Lite dependency to your build.gradle file

      Create, build and run a new project using the following GettingStarted.java code:

      import com.couchbase.lite.*;
      
      import java.io.File;
      import java.net.URI;
      import java.net.URISyntaxException;
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
      import java.util.Random;
      
      public class GettingStarted {
      
      private static final String DB_NAME = "getting-started";
      /*      Credentials declared this way purely for expediency in this demo - use OAUTH in production code */
      private static final String DB_USER = "sync_gateway";
      private static final String DB_PASS = "password"; (3)
      //    private static final String SYNC_GATEWAY_URL = "ws://127.0.0.1:4984/db" + DB_NAME;
      private static final String SYNC_GATEWAY_URL = "ws://127.0.0.1:4984/getting-started"; (1)
      private static final String DB_PATH = new File("").getAbsolutePath()+"/resources";
      
      
      public static void main (String [] args) throws CouchbaseLiteException, InterruptedException, URISyntaxException {
          Random RANDOM = new Random();
          int randPtrLang = RANDOM.nextInt(5) ;
          int randPtrType = RANDOM.nextInt(5) ;
          int numRows = 0;
      
          Double randVn = RANDOM.nextDouble() + 1;
      
          List<String> listLangs = new ArrayList<String> (Arrays.asList("Java","Swift","C#.Net","Objective-C","C++","Cobol"));
      //        List<String> listTypes = new ArrayList<String>();
          List<String> listTypes = new ArrayList<String> (Arrays.asList("SDK","API","Framework","Methodology","Language","IDE"));
      
          String Prop_Id ="id";
          String Prop_Language = "language";
          String Prop_Type = "type";
          String Prop_Version = "version";
          String searchStringType = "SDK";
          String dirPath = new File("").getAbsolutePath()+"/resources";
      
      
          // Initialize Couchbase Lite
          CouchbaseLite.init(); (2)
      
          // Get the database (and create it if it doesn’t exist).
          DatabaseConfiguration config = new DatabaseConfiguration();
          config.setDirectory(DB_PATH); (5)
          config.setEncryptionKey(new EncryptionKey(DB_PASS)); (3)
          Database database = new Database(DB_NAME, config);
      
          // Create a new document (i.e. a record) in the database.
          MutableDocument mutableDoc = new MutableDocument()
                  .setDouble(Prop_Version, randVn)
                  .setString(Prop_Type,listTypes.get(randPtrType));
      
          // Save it to the database.
          database.save(mutableDoc);
      
          // Update a document.
          mutableDoc = database.getDocument(mutableDoc.getId()).toMutable();
          mutableDoc.setString(Prop_Language, listLangs.get(randPtrLang));
          database.save(mutableDoc);
      
          Document document = database.getDocument(mutableDoc.getId());
          // Log the document ID (generated by the database) and properties
          System.out.println("Document ID is :: " + document.getId());
          System.out.println("Learning " + document.getString(Prop_Language));
      
          // Create a query to fetch documents of type SDK.
          System.out.println("== Executing Query 1");
          Query query = QueryBuilder.select(SelectResult.all())
                  .from(DataSource.database(database))
                  .where(Expression.property(Prop_Type).equalTo(Expression.string(searchStringType)));
          ResultSet result = query.execute();
          System.out.println(String.format("Query returned %d rows of type %s", result.allResults().size(), searchStringType));
      
          // Create a query to fetch all documents.
          System.out.println("== Executing Query 2");
          Query queryAll = QueryBuilder.select(SelectResult.expression(Meta.id),
                  SelectResult.property(Prop_Language),
                  SelectResult.property(Prop_Version),
                  SelectResult.property(Prop_Type))
              .from(DataSource.database(database));
              try {
                  for (Result thisDoc : queryAll.execute()) {
                    numRows++;
                    System.out.println(String.format("%d ... Id: %s is learning: %s version: %.2f type is %s",
                        numRows,
                        thisDoc.getString(Prop_Id),
                        thisDoc.getString(Prop_Language),
                        thisDoc.getDouble(Prop_Version),
                        thisDoc.getString(Prop_Type)));
                    }
              } catch (CouchbaseLiteException e) {
                  e.printStackTrace();
              }
          System.out.println(String.format("Total rows returned by query = %d", numRows));
      
          Endpoint targetEndpoint = new URLEndpoint(new URI(SYNC_GATEWAY_URL));
          ReplicatorConfiguration replConfig = new ReplicatorConfiguration(database, targetEndpoint);
          replConfig.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH_AND_PULL);
      
          // Add authentication.
          replConfig.setAuthenticator(new BasicAuthenticator(DB_USER, DB_PASS));
      
          // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
          Replicator replicator = new Replicator(replConfig);
      
          // Listen to replicator change events.
          replicator.addChangeListener(change -> {
              if (change.getStatus().getError() != null) {
                  System.err.println("Error code ::  " + change.getStatus().getError().getCode());
              }
          });
      
          // Start replication.
          replicator.start();
      
          // Check status of replication and wait till it is completed
          while (replicator.getStatus().getActivityLevel() != Replicator.ActivityLevel.STOPPED) {
              Thread.sleep(1000);
          }
      
          System.out.println("Finish!");
      
          System.exit(0); (4)
      }
      1 The app will start a replicator pointing to ws://localhost:4984/db, where db is the name of your Sync Gateway database
      2 This is the only API that differs from the Java (android) version. It accepts no parameters and should be called once only
      3 You can optionally AES-256 encrypt the database by providing a key
      4 This is needed for a tidy closedown of work executors
      5 It is advisable to set a specific directory path for the database.

      On running the app, you should see the document ID and property printed to the console together with a query result showing the number of rows in the database.

      This shows the document was successfully persisted to the database.

      See About the Getting Started App for more on the app itself

      This section explains how to set-up a build project to create Couchbase Lite Java web apps using gradle and Intellij IDEA.

      Steps

      1. Create a new project folder and add a build.gradle file containing the following:

        
        ext{
          TOMCAT_VERSION="9.0.24"
        }
        
        apply plugin: 'java'
        apply plugin: 'war'
        apply plugin: 'com.bmuschko.tomcat'
        
        sourceCompatibility = 1.8
        
        buildscript {
            repositories {
                jcenter()
            }
            dependencies {
                classpath 'com.bmuschko:gradle-tomcat-plugin:2.5'
            }
        }
        
        repositories {
            mavenCentral()
        }
        
        dependencies {
            //  Use for Enterprise version
            compileOnly "com.couchbase.lite:couchbase-lite-java-ee:2.7.0"
            //  Use for community versions
            //    compileOnly "com.couchbase.lite:couchbase-lite-java:2.7.0"
            compileOnly "javax.servlet:javax.servlet-api:4.0.1"
            tomcat "org.apache.tomcat.embed:tomcat-embed-core:${TOMCAT_VERSION}",
                "org.apache.tomcat.embed:tomcat-embed-logging-juli:9.0.0.M6",
                "org.apache.tomcat.embed:tomcat-embed-jasper:${TOMCAT_VERSION}"
        }
        
        tomcat {
            httpPort = 8080
            httpProtocol = 'org.apache.coyote.http11.Http11Nio2Protocol'
            ajpProtocol  = 'org.apache.coyote.ajp.AjpNio2Protocol'
            contextPath = '/'
        }
        
        [tomcatRun, tomcatRunWar].each { task ->
            fileTree("libs").visit { FileVisitDetails details ->
                task.additionalRuntimeResources << file(details.file.path)
            }
        }
      2. Within Intellij IDEA, open the new project folder

        If you don’t have auto-import enabled, then accept the Import from Gradle prompt that appears at the bottom right of the screen.

      3. Create a Java class GettingStarted using this code:

        import com.couchbase.lite.*;
        
        import javax.servlet.ServletException;
        import javax.servlet.annotation.WebServlet;
        import java.io.IOException;
        import java.net.URI;
        import java.net.URISyntaxException;
        import java.util.ArrayList;
        import java.util.Arrays;
        import java.util.List;
        import java.util.Random;
        
        @WebServlet( value = "/GettingStarted")
        public class GettingStarted extends javax.servlet.http.HttpServlet {
            private static final String DB_DIR = "/usr/local/var/tomcat/data"; (1)
            private static final String DB_NAME = "getting-started";
            /*      Credentials declared this way purely for expediency in this demo - use OAUTH in production code */
            private static final String DB_USER = "sync_gateway";
            private static final String DB_PASS = "password";
            //    private static final String SYNC_GATEWAY_URL = "ws://127.0.0.1:4984/db" + DB_NAME;
            private static final String SYNC_GATEWAY_URL = "ws://127.0.0.1:4984/getting-started"; (2)
            private static final String NEWLINETAG = "<br />";
            private String MYRESULTS;
            private int NUMROWS;
            private Random RANDOM = new Random();
        
            protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse
                    response) throws javax.servlet.ServletException, IOException {
                outputMessage("Servlet started :: doGet Invoked");
                doPost(request, response);
            }
        
            protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse
                    response) throws javax.servlet.ServletException, IOException {
                NUMROWS = 0;
                MYRESULTS = "";
                outputMessage("Servlet started :: doPost Invoked");
                String url = "/showDbItems.jsp";
                try {
                    MYRESULTS = testCouchbaseLite();
                } catch (CouchbaseLiteException | URISyntaxException | InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    outputMessage(String.format("CouchbaseLite Test Ended :: There are %d rows in DB", NUMROWS));
                }
                request.setAttribute("myRowCount", NUMROWS);
                request.setAttribute("myResults", MYRESULTS);
                getServletContext()
                        .getRequestDispatcher(url)
                        .forward(request, response);
                outputMessage("Servlet Ended :: doPost Exits");
            }
        
            public String testCouchbaseLite() throws CouchbaseLiteException, URISyntaxException, InterruptedException, ServletException {
        
                int randPtr = RANDOM.nextInt(5) + 1;
                long syncTotal = 0;
                Double randVn = RANDOM.nextDouble() + 1;
                List<String> listLangs = new ArrayList<String>(Arrays.asList("Java", "Swift", "C#.Net", "Objective-C", "C++", "Cobol"));
                List<String> listTypes = new ArrayList<String>(Arrays.asList("SDK", "API", "Framework", "Methodology", "Language", "IDE"));
        
                String Prop_Id = "id";
                String Prop_Language = "language";
                String Prop_Type = "type";
                String Prop_Version = "version";
                String searchStringType = "SDK";
        
                // Get and configure database
                // Note initialisation of CouchbaseLite is done in ServletContextListener
                outputMessage("== Opening DB and doing initial sync");
                Database database = DatabaseManager.manager().getDatabase(DB_NAME,DB_DIR,DB_PASS);
        
                // Initial DB sync prior to local updates
                syncTotal = DatabaseManager.manager().runOneShotReplication(database, SYNC_GATEWAY_URL, DB_USER, DB_PASS);
                outputMessage(String.format("Inital number of rows synchronised = %d", syncTotal));
        
                // Create a new document (i.e. a record) in the database.
                outputMessage("== Adding a record");
                MutableDocument mutableDoc = new MutableDocument()
                        .setDouble(Prop_Version, randVn)
                        .setString(Prop_Type, listTypes.get(RANDOM.nextInt(listTypes.size() - 1)));
        
                // Save it to the database.
                try {
                    database.save(mutableDoc);
                } catch (CouchbaseLiteException e) {
                    throw new ServletException("Error saving a document", e);
                }
        
                // Update a document.
                outputMessage("== Updating added record");
                mutableDoc = database.getDocument(mutableDoc.getId()).toMutable();
                mutableDoc.setString(Prop_Language, listLangs.get(RANDOM.nextInt(listLangs.size() - 1)));
                // Save it to the database.
                try {
                    database.save(mutableDoc);
                } catch (CouchbaseLiteException e) {
                    throw new ServletException("Error saving a document", e);
                }
        
                outputMessage("== Retrieving record by id");
                Document newDoc = database.getDocument(mutableDoc.getId());
                // Show the document ID (generated by the database) and properties
                outputMessage("Document ID :: " + newDoc.getId());
                outputMessage("Learning " + newDoc.getString(Prop_Language));
        
                // Create a query to fetch documents of type SDK.
                outputMessage("== Executing Query 1");
                Query query = QueryBuilder.select(SelectResult.all())
                        .from(DataSource.database(database))
                        .where(Expression.property(Prop_Type).equalTo(Expression.string(searchStringType)));
                try{
                    ResultSet result = query.execute();
                    outputMessage(String.format("Query returned %d rows of type %s", result.allResults().size(), searchStringType));
                } catch (CouchbaseLiteException e) {
                    e.printStackTrace();
                }
        
                // Create a query to fetch all documents.
                outputMessage("== Executing Query 2");
                Query queryAll = QueryBuilder.select(SelectResult.expression(Meta.id),
                        SelectResult.property(Prop_Language),
                        SelectResult.property(Prop_Version),
                        SelectResult.property(Prop_Type))
                        .from(DataSource.database(database));
                try {
                    for (Result thisDoc : queryAll.execute()) {
                      NUMROWS++;
                        outputMessage(String.format("%d ... Id: %s is learning: %s version: %.2f type is %s",
                          NUMROWS,
                          thisDoc.getString(Prop_Id),
                          thisDoc.getString(Prop_Language),
                          thisDoc.getDouble(Prop_Version),
                          thisDoc.getString(Prop_Type)));
                      }
                    } catch (CouchbaseLiteException e) {
                    e.printStackTrace();
                }
                outputMessage(String.format("Total rows returned by query = %d", NUMROWS));
        
        //      Do final single-shot replication to incorporate changed NumRows
                outputMessage("== Doing final single-shot sync");
                syncTotal = DatabaseManager.manager().runOneShotReplication(database, SYNC_GATEWAY_URL, DB_USER, DB_PASS);
                outputMessage(String.format("Total rows synchronised = %d", syncTotal));
                database.close();
                return MYRESULTS;
            }
        
            public void outputMessage(String msg) {
                String thisMsg = "Null message";
                if (msg.length() > 0) {
                    thisMsg = msg;
                }
                System.out.println(msg);
                MYRESULTS = MYRESULTS + msg + NEWLINETAG;
            }
        }
        1 It is advisable to set a specific directory path for the database.
        2 The app will start a replicator pointing to ws://localhost:4984/db, where db is the name of your Sync Gateway database
      4. Create a Java class Listener using this code:

        import javax.servlet.ServletContextEvent;
        import javax.servlet.ServletContextListener;
        import javax.servlet.annotation.WebListener;
        
        @WebListener
        public class Application implements ServletContextListener {
          @Override
          public void contextInitialized(ServletContextEvent event) {
              DatabaseManager.manager().init();
          }
        }
      5. Create a Java class Database Manager using this code:

        import com.couchbase.lite.*;
        import java.net.URI;
        import java.net.URISyntaxException;
        
        public class DatabaseManager {
        
            private static DatabaseManager instance;
            private Database database;
            public static synchronized DatabaseManager manager() {
                if (instance == null) {
                    instance = new DatabaseManager();
                }
                return instance;
            }
            public synchronized void init() {
                CouchbaseLite.init(); (1)
            }
            public synchronized Database getDatabase(String parDbname, String parDbDir,  String parDbPass) {
                if (database == null) {
                    try {
                        DatabaseConfiguration config = new DatabaseConfiguration();
                        config.setDirectory(parDbDir); (2)
                        config.setEncryptionKey(new EncryptionKey(parDb_PASS)); (3)
                        database = new Database(parDbname, config);
                    }
                    catch (CouchbaseLiteException e) {
                        throw new IllegalStateException("Cannot create database", e);
                    }
                }
                return database;
            }
        
            public synchronized long runOneShotReplication( Database parDb, String parURL, String parName, String parPassword) throws InterruptedException {
                long syncTotal = 0;
                // Set replicator endpoint
                URI sgURI = null;
                try {
                    sgURI = new URI(parURL);
                } catch (URISyntaxException e) {
                    e.printStackTrace();
                }
                URLEndpoint targetEndpoint = new URLEndpoint(sgURI);
        
                // Configure replication
                System.out.println("== Synchronising DB :: Configuring replicator");
                ReplicatorConfiguration replConfig = new ReplicatorConfiguration(parDb, targetEndpoint);
        
                replConfig.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH_AND_PULL);
                replConfig.setContinuous(false);    // make this a single-shot replication cf. a continuous replication
        
                // Add authentication.
        //        outputMessage("== Synchronising DB :: Setting authenticator");
                replConfig.setAuthenticator(new BasicAuthenticator(parName, parPassword));
        
                // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
        //        outputMessage("== Synchronising DB :: Creating replicator");
                Replicator replicator = new Replicator(replConfig);
        
                // Listen to replicator change events.
        //        System.out.println("== Synchronising DB :: Adding listener");
                replicator.addChangeListener(change -> {
                    if (change.getStatus().getError() != null) {
                        System.err.println("Error code ::  " + change.getStatus().getError().getCode());
                    }
                });
        
                // Start replication.
        //        outputMessage("== Synchronising DB :: Starting");
                replicator.start();
                // Check status of replication and wait till it is completed
                while ((replicator.getStatus().getActivityLevel() != Replicator.ActivityLevel.STOPPED) && (replicator.getStatus().getActivityLevel() != Replicator.ActivityLevel.IDLE)) {
                    Thread.sleep(1000);
                }
        
                syncTotal = replicator.getStatus().getProgress().getTotal();
                replicator.stop();
        //        outputMessage("== Synchronising DB :: Completed ");
                return replicator.getStatus().getProgress().getTotal();
        
                }
        }
        1 This is the only API that differs from the Java (android) version. It accepts no parameters and should be called once only
        2 It is advisable to set a specific directory path for the database.
        3 You can optionally AES-256 encrypt the database by providing a key
      6. Create an index.html file in src/main/web app with the following content:

        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <html>
        <head>
            <title>Couchbase Lite (Java Jvm) :: Getting Started App</title>
        </head>
        <body>
        <h1>Couchbase Lite (Java Jvm) :: Getting Started App</h1>
        
        <p>To invoke the GettingStarted servlet click <a href="GettingStarted">here</a></p>
        
        </body>
        </html>
      7. Create a showDbItems.jsp file in src/main/web app with the following content:

        <html>
        <head>
            <title>Couchbase Lite (Java Jvm) :: Getting Started App</title>
        </head>
        <body>
        <h1>Couchbase Lite (Java Jvm) :: Getting Started App</h1>
        <h2>List all current DB rows</h2>
        <hr>
        <p>NumRows = ${myRowCount}</p>
        <hr>
        ${myResults}
        <hr>
        </body>
        </html>
      8. Build, deploy and run the app using tomcatRun

        GradleMenuWebApp
        1. Point your browser to: localhost:8080/gettingstarted

          This opens the browser at your index.html page.

        2. Select the here link

          This launches the servlet and displays the results in showdDbItems.jsp. They are also added to the catalina.out log file.

      See About the Getting Started App for more on the app itself

      API REFERENCES

      This section introduces the Couchbase Lite Java API methods available to you.

      With the exception of the Initializer, all other APIs are identical to the Java (android) version of Couchbase Lite.

      Initializer

      Your first step in using the API must be to call its initializer. An exception is raised if any other API method is invoked before the initializer.

      The init() API accepts no parameters.

      // Initialize the Couchbase Lite system
      CouchbaseLite.init();

      New Database

      As the top-level entity in the API, new databases can be created using the Database class by passing in a name, configuration, or both. The following example creates a database using the Database(String name, DatabaseConfiguration config) method.

      DatabaseConfiguration config = new DatabaseConfiguration();
      Database database = new Database(DB_NAME, config);

      Just as before, the database will be created in a default location. Alternatively, the Database(string name, DatabaseConfiguration config) initializer can be used to provide specific options in the DatabaseConfiguration object such as the database directory.

      Database Encryption

      Enterprise Edition only
      Database encryption is an Enterprise Edition feature.

      The Couchbase Lite 2.1 release includes the ability to encrypt Couchbase Lite databases. This allows mobile applications to secure the data at rest, when it is being stored on the device. The algorithm used to encrypt the database is 256-bit AES.

      To enable encryption, you must set the DatabaseConfiguration.encryptionKey property with the encryption key of your choice. The encryption key is then required every time the database is opened.

      DatabaseConfiguration config = new DatabaseConfiguration();
      config.setEncryptionKey(new EncryptionKey("PASSWORD"));
      Database database = new Database(DB_NAME, config);

      Couchbase Lite does not persist the key. It is the application’s responsibility to manage the key and store it in a platform specific secure store such as Apple’s Keychain or Android’s Keystore.

      An encrypted database can only be opened with the same language SDK that was used to encrypt it in the first place (Swift, C#, Java or Objective-C). For example, if a database is encrypted with the Swift SDK and then exported, it will only be readable with the Swift SDK.

      Upgrading from 1.x when Encryption is Enabled

      If you’re migrating an application from Couchbase Lite 1.x to 2.x, note that the automatic database upgrade functionality is not supported for encrypted databases. Thus, to upgrade an encrypted 1.x database, you should do the following:

      1. Disable encryption using the Couchbase Lite 1.x framework (see 1.x encryption guide)

      2. Open the database file with encryption enabled using the Couchbase Lite 2.x framework (see database encryption).

      Since it is not possible to package Couchbase Lite 1.x and Couchbase Lite 2.x in the same application this upgrade path would require two successive upgrades. If you are using Sync Gateway to synchronize the database content, it may be preferable to run a pull replication from a new 2.x database with encryption enabled and delete the 1.x local database.

      Finding a Database File

      By default a Couchbase Lite Java database is created in a directory at the current location called <databaseName>.cblite2.

      This location is set by the DatabaseConfiguration method. You should always use it to explicitly set the database location. See the following example for how to do this:

      DatabaseConfiguration thisConfig = new DatabaseConfiguration(); thisConfig.setDirectory("yourDBpath");
      Database thisDB = new Database("db", thisConfig);

      CLI tool

      cblite is a command-line tool for inspecting and querying Couchbase Lite 2.x databases.

      You can download and build it from the couchbaselabs GitHub repository.

      The cblite tool is not supported by the Couchbase Support Policy.

      Logging

      If you are using a Couchbase Lite release prior to 2.5 see Deprecated functionality

      From version 2.5, Couchbase Lite provides a logging API that unifies the logging behavior across all platforms, making debugging and troubleshooting easier during development and in production.

      The retrieval of logs from the device is out of scope of this feature.

      Available logging features include:

      • Console based logging

      • File based logging

      • Custom logging

      Console based logging

      Default: Enabled.

      Console based logging is often used to facilitate troubleshooting during development.

      File based logging

      Default: Disabled.

      Available file based logging formats:

      • Binary — most efficient for storage and performance. It is the default for file based logging.

      • Plaintext

      We recommend using the binary log format and a decoder, such as cbl-log, to view them. Download cbl-log from couchbaselabs/couchbase-mobile-tools.

      The following example enables file based logging.

      final File path = new File("/usr/local/MyApp/logs")
      Database.log.getFile().setConfig(new LogFileConfiguration(path.toString()));
      Database.log.getFile().setLevel(LogLevel.INFO);

      Custom logging

      Default: Disabled.

      Allows registration of a callback function to receive Couchbase Lite log messages, which may be logged using any external logging framework.

      Apps must implement the Logger interface, as shown below:

      class LogTestLogger implements Logger {
          @NonNull
          private final LogLevel level;
      
          public LogTestLogger(@NonNull LogLevel level) { this.level = level; }
      
          @NonNull
          @Override
          public LogLevel getLevel() { return level; }
      
          @Override
          public void log(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) {
              // this method will never be called if param level < this.level
              // handle the message, for example piping it to a third party framework
          }
      }

      And set it on the custom property.

      // this custom logger will never be asked to log an event
      // with a log level < WARNING
      Database.log.setCustom(new LogTestLogger(LogLevel.WARNING));

      Decoding binary logs

      The cbl-log tool should be used to decode binary log files as shown in these examples.

      • macOS

      • CentOS

      • Windows

      Download the cbl-log tool using wget.

      wget https://packages.couchbase.com/releases/couchbase-lite-log/2.7.0/couchbase-lite-log-2.7.0-macos.zip

      Navigate to the bin directory and run the cbl-log executable.

      $ ./cbl-log logcat LOGFILE <OUTPUT_PATH>

      Download the cbl-log tool using wget.

      wget https://packages.couchbase.com/releases/couchbase-lite-log/2.7.0/couchbase-lite-log-2.7.0-centos.zip

      Navigate to the bin directory and run the cbl-log executable.

      cbl-log logcat LOGFILE <OUTPUT_PATH>

      Download the cbl-log tool using PowerShell.

      Invoke-WebRequest https://packages.couchbase.com/releases/couchbase-lite-log/2.7.0/couchbase-lite-log-2.7.0-windows.zip -OutFile couchbase-lite-log-2.7.0-windows.zip

      Run the cbl-log executable.

      $ .\cbl-log.exe logcat LOGFILE <OUTPUT_PATH>

      Logging functionality prior to Release 2.5

      This Logging functionality is deprecated. It was replaced by the current Logging API in release 2.5 and so this information is included for completeness only.

      The log messages are split into different domains (LogDomains) which can be tuned to different log levels. The following example enables verbose logging for the replicator and query domains.

      Database.setLogLevel(LogDomain.REPLICATOR, LogLevel.VERBOSE);
      Database.setLogLevel(LogDomain.QUERY, LogLevel.VERBOSE);

      Loading a pre-built database

      If your app needs to sync a lot of data initially, but that data is fairly static and won’t change much, it can be a lot more efficient to bundle a database in your application and install it on the first launch. Even if some of the content changes on the server after you create the app, the app’s first pull replication will bring the database up to date.

      To use a prebuilt database, you need to set up the database, build the database into your app bundle as a resource, and install the database during the initial launch. After your app launches, it needs to check whether the database exists. If the database does not exist, the application should copy it from the assets folder to the app’s files directory.

      // Note: Getting the path to a database is platform-specific.
      // For Android you need to extract it from your
      // assets to a temporary directory and then pass that path to Database.copy()
      DatabaseConfiguration configuration = new DatabaseConfiguration();
      if (!Database.exists("travel-sample", APP_DB_DIR)) {
          File tmpDir = new File(System.getProperty("java.io.tmpdir"));
          ZipUtils.unzip(getAsset("travel-sample.cblite2.zip"), tmpDir);
          File path = new File(tmpDir, "travel-sample");
          try {
              Database.copy(path, "travel-sample", configuration);
          } catch (CouchbaseLiteException e) {
              e.printStackTrace();
          }
      }

      In the example below, the ZipUtils.unzip method copies the zipped pre-built database from the application’s resource directory to the files directory. This method is provided below for information as it isn’t included in the Couchbase Lite library.

      public class ZipUtils {
          public static void unzip(InputStream in, File destination) throws IOException {
              byte[] buffer = new byte[1024];
              ZipInputStream zis = new ZipInputStream(in);
              ZipEntry ze = zis.getNextEntry();
              while (ze != null) {
                  String fileName = ze.getName();
                  File newFile = new File(destination, fileName);
                  if (ze.isDirectory()) {
                      newFile.mkdirs();
                  } else {
                      new File(newFile.getParent()).mkdirs();
                      FileOutputStream fos = new FileOutputStream(newFile);
                      int len;
                      while ((len = zis.read(buffer)) > 0) {
                          fos.write(buffer, 0, len);
                      }
                      fos.close();
                  }
                  ze = zis.getNextEntry();
              }
              zis.closeEntry();
              zis.close();
              in.close();
          }
      }

      Document

      In Couchbase Lite, a document’s body takes the form of a JSON object — a collection of key/value pairs where the values can be different types of data such as numbers, strings, arrays or even nested objects. Every document is identified by a document ID, which can be automatically generated (as a UUID) or specified programmatically; the only constraints are that it must be unique within the database, and it can’t be changed.

      Initializers

      The following methods/initializers can be used:

      • The MutableDocument() initializer can be used to create a new document where the document ID is randomly generated by the database.

      • The MutableDocument(String id) initializer can be used to create a new document with a specific ID.

      • The database.getDocument(String id) method can be used to get a document. If it doesn’t exist in the database, it will return null. This method can be used to check if a document with a given ID already exists in the database.

      The following code example creates a document and persists it to the database.

      MutableDocument newTask = new MutableDocument();
      newTask.setString("type", "task");
      newTask.setString("owner", "todo");
      newTask.setDate("createdAt", new Date());
      try {
          database.save(newTask);
      } catch (CouchbaseLiteException e) {
          Log.e(TAG, e.toString());
      }

      Mutability

      When a document is read from the database it is immutable. Use the document.toMutable() method to create an updateable instance of the document.

      Document document = database.getDocument("xyz");
      MutableDocument mutableDocument = document.toMutable();
      mutableDocument.setString("name", "apples");
      try {
          database.save(mutableDocument);
      } catch (CouchbaseLiteException e) {
          Log.e(TAG, e.toString());
      }

      Changes to the document are persisted to the database when the save method is called.

      Typed Accessors

      The Document class now offers a set of property accessors for various scalar types, including boolean, integers, floating-point and strings. These accessors take care of converting to/from JSON encoding, and make sure you get the type you’re expecting.

      In addition, as a convenience we offer Date accessors. Dates are a common data type, but JSON doesn’t natively support them, so the convention is to store them as strings in ISO-8601 format. The following example sets the date on the createdAt property and reads it back using the document.getDate(String key) accessor method.

      newTask.setValue("createdAt", new Date());
      Date date = newTask.getDate("createdAt");

      If the property doesn’t exist in the document it will return the default value for that getter method (0 for getInt, 0.0 for getFloat etc.). To check whether a given property exists in the document, you should use the Document.Contains(String key) method.

      Batch operations

      If you’re making multiple changes to a database at once, it’s faster to group them together. The following example persists a few documents in batch.

      try {
          database.inBatch(() -> {
              for (int i = 0; i < 10; i++) {
                  MutableDocument doc = new MutableDocument();
                  doc.setValue("type", "user");
                  doc.setValue("name", "user " + i);
                  doc.setBoolean("admin", false);
                  try {
                      database.save(doc);
                  } catch (CouchbaseLiteException e) {
                      Log.e(TAG, e.toString());
                  }
                  Log.i(TAG, String.format("saved user document %s", doc.getString("name")));
              }
          });
      } catch (CouchbaseLiteException e) {
          Log.e(TAG, e.toString());
      }

      At the local level this operation is still transactional: no other Database instances, including ones managed by the replicator can make changes during the execution of the block, and other instances will not see partial changes. But Couchbase Mobile is a distributed system, and due to the way replication works, there’s no guarantee that Sync Gateway or other devices will receive your changes all at once.

      Document change events

      It’s also possible to register for document changes. The following example registers for changes to the document with ID user.john and prints the verified_account property.

      database.addDocumentChangeListener(
          "user.john",
          change -> {
              Document doc = database.getDocument(change.getDocumentID());
              if (doc != null) {
                  Toast.makeText(context, "Status: " + doc.getString("verified_account"), Toast.LENGTH_SHORT).show();
              }
          });

      Document Constraints

      Couchbase Lite APIs do not explicitly disallow the use of attributes with the underscore prefix at the top level of document. This is to facilitate the creation of documents for use either in local only mode where documents are not synced, or when used exclusively in peer-to-peer sync.

      "_id", :"_rev" and "_sequence" are reserved keywords and must not be used as top-level attributes — see Example 1.

      Users are cautioned that any attempt to sync such documents to Sync Gateway will result in an error. To be future proof, you are advised to avoid creating such documents. Use of these attributes for user-level data may result in undefined system behavior.

      For more guidance — see: Sync Gateway - data modeling guidelines

      Example 1. Reserved Keys List
      • _attachments

      • _id

      • _deleted

      • _removed

      • _rev

      Document Expiration

      Document expiration allows users to set the expiration date to a document. When the document is expired, the document will be purged from the database. The purge will not be replicated to Sync Gateway.

      The following example sets the TTL for a document to 5 minutes from the current time.

      // Purge the document one day from now
      Instant ttl = Instant.now().plus(1, ChronoUnit.DAYS);
      database.setDocumentExpiration("doc123", new Date(ttl.toEpochMilli()));
      
      // Reset expiration
      database.setDocumentExpiration("doc1", null);
      
      // Query documents that will be expired in less than five minutes
      Instant fiveMinutesFromNow = Instant.now().plus(5, ChronoUnit.MINUTES);
      Query query = QueryBuilder
          .select(SelectResult.expression(Meta.id))
          .from(DataSource.database(database))
          .where(Meta.expiration.lessThan(Expression.doubleValue(fiveMinutesFromNow.toEpochMilli())));

      Blobs

      We’ve renamed "attachments" to "blobs". The new behavior should be clearer too: a Blob is now a normal object that can appear in a document as a property value. In other words, you just instantiate a Blob and set it as the value of a property, and then later you can get the property value, which will be a Blob object. The following code example adds a blob to the document under the avatar property.

      InputStream is = getAsset("avatar.jpg");
      if (is == null) { return; }
      try {
          Blob blob = new Blob("image/jpeg", is);
          newTask.setBlob("avatar", blob);
          database.save(newTask);
      
          Blob taskBlob = newTask.getBlob("avatar");
          byte[] bytes = taskBlob.getContent();
      } catch (CouchbaseLiteException e) {
          Log.e(TAG, e.toString());
      } finally {
          try { is.close(); }
          catch (IOException ignore) { }
      }

      The Blob API lets you access the contents as an in-memory byte array ( public byte[] getContent()) or as an InputStream (public InputStream getContentStream()). It also supports an optional type property that by convention stores the MIME type of the contents.

      In the example above, "image/jpeg" is the MIME type and "avatar" is the key which references that Blob. That key can be used to retrieve the Blob object at a later time.

      When a document is synchronized, the Couchbase Lite replicator will add an _attachments dictionary to the document’s properties if it contains a blob. A random access name will be generated for each Blob which is different to the "avatar" key that was used in the example above. On the image below, the document now contains the _attachments dictionary when viewed in the Couchbase Server Admin Console.

      attach replicated

      A blob also has properties such as "digest" (a SHA-1 digest of the data), "length" (the length in bytes), and optionally "content_type" (the MIME type). The data is not stored in the document, but in a separate content-addressable store, indexed by the digest.

      This Blob can be retrieved on the Sync Gateway REST API at http://localhost:4984/justdoit/user.david/blob_1. Notice that the blob identifier in the URL path is "blob_1" (not "avatar").

      Query

      Database queries are based on expressions, of the form "SELECT ____ FROM database WHERE ____, ORDERed by ____", with semantics based on Couchbase’s N1QL query language.

      There are several parts to specifying a query:

      SELECT

      Specifies the projection, which is the part of the document that is to be returned.

      FROM

      Specifies the database to query the documents from.

      JOIN

      Specifies the matching criteria in which to join multiple documents.

      WHERE

      Specifies the query criteria that the result must satisfy.

      GROUP BY

      Specifies the query criteria to group rows by.

      ORDER BY

      Specifies the query criteria to sort the rows in the result.

      SELECT statement

      With the SELECT statement, you can query and manipulate JSON data. With projections, you retrieve just the fields that you need and not the entire document.

      A SelectResult represents a single return value of the query statement. You can specify a comma separated list of SelectResult expressions in the select statement of your query. For instance the following select statement queries for the document _id as well as the type and name properties of all documents in the database. In the query result, we print the _id and name properties of each row using the property name getter method.

      {
          "_id": "hotel123",
          "type": "hotel",
          "name": "Apple Droid"
      }
      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("name"),
              SelectResult.property("type"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("hotel")))
          .orderBy(Ordering.expression(Meta.id));
      
      try {
          for (Result result : query.execute()) {
              Log.i("Sample", String.format("hotel id -> %s", result.getString("id")));
              Log.i("Sample", String.format("hotel name -> %s", result.getString("name")));
          }
      } catch (CouchbaseLiteException e) {
          Log.e("Sample", e.getLocalizedMessage());
      }

      The SelectResult.all() method can be used to query all the properties of a document. In this case, the document in the result is embedded in a dictionary where the key is the database name. The following snippet shows the same query using SelectResult.all() and the result in JSON.

      Query query = QueryBuilder
          .select(SelectResult.all())
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("hotel")));
      [
          {
              "travel-sample": {
                  "callsign": "MILE-AIR",
                  "country": "United States",
                  "iata": "Q5",
                  "icao": "MLA",
                  "id": 10,
                  "name": "40-Mile Air",
                  "type": "airline"
              }
          },
          {
              "travel-sample": {
                  "callsign": "TXW",
                  "country": "United States",
                  "iata": "TQ",
                  "icao": "TXW",
                  "id": 10123,
                  "name": "Texas Wings",
                  "type": "airline"
              }
          }
      ]

      WHERE statement

      Similar to SQL, you can use the where clause to filter the documents to be returned as part of the query. The select statement takes in an Expression. You can chain any number of Expressions in order to implement sophisticated filtering capabilities.

      Comparison

      The comparison operators can be used in the WHERE statement to specify on which property to match documents. In the example below, we use the equalTo operator to query documents where the type property equals "hotel".

      {
          "_id": "hotel123",
          "type": "hotel",
          "name": "Apple Droid"
      }
      Query query = QueryBuilder
          .select(SelectResult.all())
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("hotel")))
          .limit(Expression.intValue(10));
      for (Result result : query.execute()) {
          Dictionary all = result.getDictionary(DATABASE_NAME);
          Log.i("Sample", String.format("name -> %s", all.getString("name")));
          Log.i("Sample", String.format("type -> %s", all.getString("type")));
      }

      Collection Operators

      Collection operators are useful to check if a given value is present in an array.

      CONTAINS Operator

      The following example uses the Function.arrayContains to find documents whose public_likes array property contain a value equal to "Armani Langworth".

      {
          "_id": "hotel123",
          "name": "Apple Droid",
          "public_likes": ["Armani Langworth", "Elfrieda Gutkowski", "Maureen Ruecker"]
      }
      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("name"),
              SelectResult.property("public_likes"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("hotel"))
              .and(ArrayFunction
                  .contains(Expression.property("public_likes"), Expression.string("Armani Langworth"))));
      for (Result result : query.execute()) {
          Log.i(
              "Sample",
              String.format("public_likes -> %s", result.getArray("public_likes").toList()));
      }

      IN Operator

      The IN operator is useful when you need to select based on a specified term matching any element of a defined list of values. The following example compiles a list of values from first, last and username property value. It then selects if any of those values equals "Armani".

      Expression[] values = new Expression[] {
          Expression.property("first"),
          Expression.property("last"),
          Expression.property("username")
      };
      
      Query query = QueryBuilder.select(SelectResult.all())
          .from(DataSource.database(database))
          .where(Expression.string("Armani").in(values));

      Like Operator

      The like operator can be used for string matching.

      The like operator performs case sensitive matches. So, to make the string matching case-insensitive, use Function.lower or Function.upper to transform the matched strings to lowercase or uppercase equivalents.

      In the example below, we are looking for documents of type landmark where the name property exactly matches the string "Royal engineers museum".

      For case-insensitive comparison, use Function.lower to align the cases of the strings being compared.

      The following query returns "landmark" type documents regardless of the name’s case (so, for example: "Royal Engineers Museum", "royal engineers museum", "ROYAL ENGINEERS MUSEUM" are all matches).

      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("country"),
              SelectResult.property("name"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("landmark"))
              .and(Function.lower(Expression.property("name")).like(Function.Expression.string("royal engineers museum")))));
      for (Result result : query.execute()) {
          Log.i("Sample", String.format("name -> %s", result.getString("name")));
      }

      Wildcard Match

      We can use % sign within a like expression to do a wildcard match against zero or more characters. Using wildcards allows you to have some fuzziness in your search string.

      In the example below, we are looking for documents of type "landmark" where the name property matches any string that begins with "eng" followed by zero or more characters, the letter "e", followed by zero or more characters.

      The following query will return "landmark" type documents with name matching "Engineers", "engine", "english egg" , "England Eagle" and so on.

      Once again, we are using Function.lower to make the search act as case insensitive.
      The matches may span word boundaries.
      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("country"),
              SelectResult.property("name"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("landmark"))
          .and(Function.lower(Expression.property("name")).like(Expression.string("eng%e%"))));
      for (Result result : query.execute()) {
          Log.i("Sample", String.format("name -> %s", result.getString("name"))); }

      Wildcard Character Match

      We can use an _ sign within a like expression to do a wildcard match against a single character.

      In the example below, we are looking for documents of type "landmark" where the name property matches any string that begins with "eng" followed by exactly 4 wildcard characters and ending in the letter "r". The following query will return "landmark" type documents with the name matching "Engineer", "engineer" and so on.

      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("country"),
              SelectResult.property("name"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("landmark"))
          .and(Function.lower(Expression.property("name")).like(Expression.string("eng____r"))));
      for (Result result : query.execute()) {
          Log.i("Sample", String.format("name -> %s", result.getString("name"))); }

      Regex Operator

      Similar to wildcard like expressions, regex expressions based pattern matching allow you to have some fuzziness in your search string.

      The regex operator is case sensitive.

      For more on the regex spec used by Couchbase Lite see cplusplus regex reference page

      In the example below, we are looking for documents of type "landmark" where the name property matches any string (on word boundaries) that begins with "eng" followed by exactly 4 wildcard characters and ending in the letter "r". The following query will return "landmark" type documents with name matching "Engine", "engine" and so on. Note that the \b specifies that the match must occur on word boundaries.

      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("country"),
              SelectResult.property("name"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("landmark"))
          .and(Function.lower(Expression.property("name")).regex(Expression.string("\\beng.*r\\b"))));            ResultSet rs = query.execute();
      for (Result result : query.execute()) {
          Log.i("Sample", String.format("name -> %s", result.getString("name"))); }

      Query Deleted Document

      You can query documents that have been deleted (tombstones). The following example shows how to query deleted documents in the database.

      // Query documents that have been deleted
      Where query = QueryBuilder
          .select(SelectResult.expression(Meta.id))
          .from(DataSource.database(database))
          .where(Meta.deleted);

      JOIN statement

      The JOIN clause enables you to create new input objects by combining two or more source objects.

      The following example uses a JOIN clause to find the airline details which have routes that start from RIX. This example JOINS the document of type "route" with documents of type "airline" using the document ID (_id) on the "airline" document and airlineid on the "route" document.

      Query query = QueryBuilder.select(
          SelectResult.expression(Expression.property("name").from("airline")),
          SelectResult.expression(Expression.property("callsign").from("airline")),
          SelectResult.expression(Expression.property("destinationairport").from("route")),
          SelectResult.expression(Expression.property("stops").from("route")),
          SelectResult.expression(Expression.property("airline").from("route")))
          .from(DataSource.database(database).as("airline"))
          .join(Join.join(DataSource.database(database).as("route"))
              .on(Meta.id.from("airline").equalTo(Expression.property("airlineid").from("route"))))
          .where(Expression.property("type").from("route").equalTo(Expression.string("route"))
              .and(Expression.property("type").from("airline").equalTo(Expression.string("airline")))
              .and(Expression.property("sourceairport").from("route").equalTo(Expression.string("RIX"))));
      for (Result result : query.execute()) {
               Log.w("Sample", String.format("%s", result.toMap().toString()));
      }

      GROUP BY statement

      You can perform further processing on the data in your result set before the final projection is generated. The following example looks for the number of airports at an altitude of 300 ft or higher and groups the results by country and timezone.

      {
          "_id": "airport123",
          "type": "airport",
          "country": "United States",
          "geo": { "alt": 456 },
          "tz": "America/Anchorage"
      }
      Query query = QueryBuilder.select(
          SelectResult.expression(Function.count(Expression.string("*"))),
          SelectResult.property("country"),
          SelectResult.property("tz"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("airport"))
              .and(Expression.property("geo.alt").greaterThanOrEqualTo(Expression.intValue(300))))
          .groupBy(
              Expression.property("country"),
              Expression.property("tz"))
          .orderBy(Ordering.expression(Function.count(Expression.string("*"))).descending());
      for (Result result : query.execute()) {
          Log.i(
              "Sample",
              String.format(
                  "There are %d airports on the %s timezone located in %s and above 300ft",
                  result.getInt("$1"),
                  result.getString("tz"),
                  result.getString("country")));
      }
      There are 138 airports on the Europe/Paris timezone located in France and above 300 ft
      There are 29 airports on the Europe/London timezone located in United Kingdom and above 300 ft
      There are 50 airports on the America/Anchorage timezone located in United States and above 300 ft
      There are 279 airports on the America/Chicago timezone located in United States and above 300 ft
      There are 123 airports on the America/Denver timezone located in United States and above 300 ft

      ORDER BY statement

      It is possible to sort the results of a query based on a given expression result. The example below returns documents of type equal to "hotel" sorted in ascending order by the value of the title property.

      Query query = QueryBuilder
          .select(
              SelectResult.expression(Meta.id),
              SelectResult.property("name"))
          .from(DataSource.database(database))
          .where(Expression.property("type").equalTo(Expression.string("hotel")))
          .orderBy(Ordering.property("name").ascending())
          .limit(Expression.intValue(10));
      For (Result result : query.execute()) {
          Log.i("Sample", String.format("%s", result.toMap()));
      }
      Aberdyfi
      Achiltibuie
      Altrincham
      Ambleside
      Annan
      Ardèche
      Armagh
      Avignon

      Date/Time Functions

      Couchbase Lite documents support a date type that internally stores dates in ISO 8601 with the GMT/UTC timezone.

      Couchbase Lite 2.5 adds the ability to run date comparisons in your Couchbase Lite queries. To do so, four functions have been added to the Query Builder API:

      Function.StringToMillis(Expression.Property("date_time"))

      The input to this will be a validly formatted ISO 8601 date_time string. The end result will be an expression (with a numeric content) that can be further input into the query builder.

      Function.StringToUTC(Expression.Property("date_time"))

      The input to this will be a validly formatted ISO 8601 date_time string. The end result will be an expression (with string content) that can be further input into the query builder.

      Function.MillisToString(Expression.Property("date_time"))

      The input for this is a numeric value representing milliseconds since the Unix epoch. The end result will be an expression (with string content representing the date and time as an ISO 8601 string in the device’s timezone) that can be further input into the query builder.

      Function.MillisToUTC(Expression.Property("date_time"))

      The input for this is a numeric value representing milliseconds since the Unix epoch. The end result will be an expression (with string content representing the date and time as a UTC ISO 8601 string) that can be further input into the query builder.

      Live Query

      A live query stays active and monitors the database for changes. A live query is a great way to build reactive user interfaces, especially table/list views, that keep themselves up to date. For example, as the replicator runs and pulls new data from the server, a live query-driven UI will automatically update to show the data without the user having to manually refresh. This helps your app feel quick and responsive.

      Query query = QueryBuilder
          .select(SelectResult.all())
          .from(DataSource.database(database));
      
      // Adds a query change listener.
      // Changes will be posted on the main queue.
      ListenerToken token = query.addChangeListener(change -> {
          for (Result result : change.getResults()) {
              Log.d(TAG, "results: " + result.getKeys());
              /* Update UI */
          }
      });
      
      // Start live query.
      query.execute(); (1)
      1 To start a live query, you must call query.execute(). This will immediately execute the query and post the result to the change listener. When there’s a change it re-runs itself automatically, and posts the new query result to any observers (change listeners).

      The following example stops the live query with the token from the previous example.

      query.removeChangeListener(token);

      Predictive Query

      Enterprise Edition only
      Predictive Query is an Enterprise Edition feature.

      Predictive Query enables Couchbase Lite queries to use machine learning, by providing query functions that can process document data (properties or blobs) via trained ML models.

      Let’s consider an image classifier model that takes a picture as input and outputs a label and probability.

      predictive diagram

      To run a predictive query with a model as the one shown above, you must implement the following steps.

      Integrate the Model

      To integrate a model with Couchbase Lite, you must implement the PredictiveModel interface which has only one function called predict().

      // `tensorFlowModel` is a fake implementation
      // this would be the implementation of the ml model you have chosen
      class ImageClassifierModel implements PredictiveModel {
          @Override
          public Dictionary predict(@NonNull Dictionary input) {
              Blob blob = input.getBlob("photo");
              if (blob == null) { return null; }
      
              // `tensorFlowModel` is a fake implementation
              // this would be the implementation of the ml model you have chosen
              return new MutableDictionary(TensorFlowModel.predictImage(blob.getContent())); (1)
          }
      }
      
      class TensorFlowModel {
          public static Map<String, Object> predictImage(byte[] data) {
              return null;
          }
      }
      1 The predict(input) -> output method provides the input and expects the result of using the machine learning model. The input and output of the predictive model is a DictionaryObject. Therefore, the supported data type will be constrained by the data type that the DictionaryObject supports.

      Register the Model

      To register the model you must create a new instance and pass it to the Database.prediction.registerModel static method.

      Database.prediction.registerModel("ImageClassifier", new ImageClassifierModel());

      Create an Index

      Creating an index for a predictive query is highly recommended. By computing the predictions during writes and building a prediction index, you can significantly improve the speed of prediction queries (which would otherwise have to be computed during reads).

      There are two types of indexes for predictive queries:

      Value Index

      The code below creates a value index from the "label" value of the prediction result. When documents are added or updated, the index will call the prediction function to update the label value in the index.

      ValueIndex index = IndexBuilder.valueIndex(ValueIndexItem.expression(Expression.property("label")));
      database.createIndex("value-index-image-classifier", index);

      Predictive Index

      Predictive Index is a new index type used for predictive query. The Predictive Index is different from the value index in that the Predictive Index caches the predictive result and creates the value index from the cached predictive result when the predictive results values are specified.

      The code below creates a predictive index from the "label" value of the prediction result.

      Map<String, Object> inputMap = new HashMap<>();
      inputMap.put("numbers", Expression.property("photo"));
      Expression input = Expression.map(inputMap);
      
      PredictiveIndex index = IndexBuilder.predictiveIndex("ImageClassifier", input, null);
      database.createIndex("predictive-index-image-classifier", index);

      Run a Prediction Query

      The code below creates a query that calls the prediction function to return the "label" value for the first 10 results in the database.

      Map<String, Object> inputProperties = new HashMap<>();
      inputProperties.put("photo", Expression.property("photo"));
      Expression input = Expression.map(inputProperties);
      PredictionFunction prediction = PredictiveModel.predict("ImageClassifier", input); (1)
      
      Query query = QueryBuilder
          .select(SelectResult.all())
          .from(DataSource.database(database))
          .where(Expression.property("label").equalTo(Expression.string("car"))
              .and(Expression.property("probability").greaterThanOrEqualTo(Expression.doubleValue(0.8))));
      
      // Run the query.
      ResultSet result = query.execute();
      Log.d(TAG, "Number of rows: " + result.allResults().size());
      1 The PredictiveModel.predict() method returns a constructed Prediction Function object which can be used further to specify a property value extracted from the output dictionary of the PredictiveModel.predict() function.
      The null value returned by the prediction method will be interpreted as MISSING value in queries.

      Unregister the Model

      To unregister the model you must call the Database.prediction.unregisterModel static method.

      Database.prediction.unregisterModel("ImageClassifier");

      Indexing

      Creating indexes can speed up the performance of queries. While indexes make queries faster, they also make writes slightly slower, and the Couchbase Lite database file slightly larger. As such, it is best to only create indexes when you need to optimize a specific case for better query performance.

      The following example creates a new index for the type and name properties.

      {
          "_id": "hotel123",
          "type": "hotel",
          "name": "Apple Droid"
      }
      database.createIndex(
          "TypeNameIndex",
          IndexBuilder.valueIndex(
              ValueIndexItem.property("type"),
              ValueIndexItem.property("name")));

      If there are multiple expressions, the first one will be the primary key, the second the secondary key, etc.

      Every index has to be updated whenever a document is updated, so too many indexes can hurt performance. Thus, good performance depends on designing and creating the right indexes to go along with your queries.

      To run a full-text search (FTS) query, you must have created a full-text index on the expression being matched. Unlike regular queries, the index is not optional. The following example creates an FTS index on the name property.

      database.createIndex(
          "nameFTSIndex",
          IndexBuilder.fullTextIndex(FullTextIndexItem.property("name")).ignoreAccents(false));

      Multiple properties to index can be specified in the index creation method.

      With the index created for a given property, you can now construct and run FTS queries on that property. The full-text search criteria is defined as a FullTextExpression. The left-hand side is the full-text index to use and the right-hand side is the pattern to match.

      Expression whereClause = FullTextExpression.index("nameFTSIndex").match("buy");
      Query ftsQuery = QueryBuilder.select(SelectResult.expression(Meta.id))
          .from(DataSource.database(database))
          .where(whereClause);
      ResultSet ftsQueryResult = ftsQuery.execute();
      for (Result result : ftsQueryResult) {
          Log.i(
              TAG,
              String.format("document properties %s", result.getString(0)));
      }

      In the example above, the pattern to match is a word, the full-text search query matches all documents that contain the word "buy" in the value of the doc.name property.

      Search is supported for all languages that use whitespace to separate words.

      Stemming, which is the process of fuzzy matching parts of speech, like "fast" and "faster", is supported in the following languages: danish, dutch, english, finnish, french, german, hungarian, italian, norwegian, portuguese, romanian, russian, spanish, swedish and turkish.

      The pattern to match can also be in the following forms:

      prefix queries

      The query expression used to search for a term prefix is the prefix itself with a "*" character appended to it. For example

      "'lin*'"

      Will search for all documents containing a term with the prefix lin, poducing a match for all documents that contain "linux" and also those that contain terms "linear", "linker", "linguistic" and so on.

      overriding the property name that is being indexed

      Normally, a token or token prefix query is matched against the document property specified as the left-hand side of the match operator. This may be overridden by specifying a property name followed by a ":" character before a basic term query. There may be space between the ":" and the term to query for, but not between the property name and the ":" character. For example:

      'title:linux problems'
      -- Query the database for documents for which the term "linux" appears in
      -- the document title, and the term "problems" appears in either the title
      -- or body of the document.
      phrase queries

      A phrase query is a query that retrieves all documents that contain a nominated set of terms or term prefixes in a specified order with no intervening tokens. Phrase queries are specified by enclosing a space separated sequence of terms or term prefixes in double quotes ("). For example:

      "'"linux applications"'"
      -- Query for all documents that contain the phrase "linux applications".
      NEAR queries

      A NEAR query is a query that returns documents that contain a two or more nominated terms or phrases within a specified proximity of each other (by default with 10 or less intervening terms). A NEAR query is specified by putting the keyword "NEAR" between two phrase, token or token prefix queries. To specify a proximity other than the default, an operator of the form "NEAR/" may be used, where is the maximum number of intervening terms allowed. For example:

      "'database NEAR/2 "replication"'"
      -- Search for a document that contains the phrase "replication" and the term
      -- "database" with not more than 2 terms separating the two.
      AND, OR & NOT query operators

      The enhanced query syntax supports the AND, OR and NOT binary set operators. Each of the two operands to an operator may be a basic FTS query, or the result of another AND, OR or NOT set operation. Operators must be entered using capital letters. Otherwise, they are interpreted as basic term queries instead of set operators. For example:

      'couchbase AND database'
      -- Return the set of documents that contain the term "couchbase", and the
      -- term "database". This query will return the document with docid 3 only.

      When using the enhanced query syntax, parenthesis may be used to specify the precedence of the various operators. For example:

      '("couchbase database" OR "sqlite library") AND linux'
      -- Query for the set of documents that contains the term "linux", and at least
      -- one of the phrases "couchbase database" and "sqlite library".

      Ordering results

      It’s very common to sort full-text results in descending order of relevance. This can be a very difficult heuristic to define, but Couchbase Lite comes with a ranking function you can use. In the OrderBy array, use a string of the form Rank(X), where X is the property or expression being searched, to represent the ranking of the result.

      Replication

      Couchbase Mobile’s replication protocol id based on WebSockets [1].

      The replicator is designed to send documents from a source to a target database. The target can be one of the following:

      URLEndpoint

      To replicate data between a local Couchbase Lite database and remote Sync Gateway database.

      DatabaseEndpoint

      To replicate data between two local Couchbase Lite databases to store data on secondary storage.

      MessageEndpoint

      To replicate with another Couchbase Lite database via a custom transportation protocol such iOS Multipeer Connectivity, Android WiFi Direct, Android NearByConnection, socket based transportation etc.

      Replications can be run in one of two modes:

      • one shot - single exchange of data

      • continuous - a continual replication of data changes as they occur

      Compatibility

      The new protocol is incompatible with CouchDB-based databases. And since Couchbase Lite 2 only supports the new protocol, you will need to run a version of Sync Gateway that supports it — see: Sync Gateway Compatibility Matrix.

      To use this protocol with Couchbase Lite 2.0, the replication URL should specify WebSockets as the URL scheme (see the Starting a Replication section below). Mobile clients using Couchbase Lite 1.x can continue to use http as the URL scheme. Sync Gateway 2.0 will automatically use the 1.x replication protocol when a Couchbase Lite 1.x client connects through http://localhost:4984/db and the 2.0 replication protocol when a Couchbase Lite 2.0 client connects through "ws://localhost:4984/db".

      Starting Sync Gateway

      Download Sync Gateway and start it from the command line with the configuration file created above.

      ~/Downloads/couchbase-sync-gateway/bin/sync_gateway

      For platform specific installation instructions, refer to the Sync Gateway installation guide.

      Starting a Replication

      Replication can be bidirectional, this means you can start a push/pull replication with a single instance. The replication’s parameters can be specified through the ReplicatorConfiguration object; for example, if you wish to start a push only or pull only replication.

      The following example creates a pull replication with Sync Gateway.

      class MyClass {
          Database database;
          Replicator replicator; (1)
      
          void startReplication() {
              URI uri = null;
              try {
                  uri = new URI("wss://10.0.2.2:4984/db"); (2)
              } catch (URISyntaxException e) {
                  e.printStackTrace();
              }
              Endpoint endpoint = new URLEndpoint(uri);
              ReplicatorConfiguration config = new ReplicatorConfiguration(database, endpoint);
              config.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PULL);
              this.replicator = new Replicator(config);
              this.replicator.start();
          }
      
      }
      1 A replication is an asynchronous operation. To keep a reference to the replicator object, you can set it as an instance property.
      2 The URL scheme for remote database URLs has changed in Couchbase Lite 2.0. You should now use ws:, or wss: for SSL/TLS connections. In this example the hostname is 10.0.2.2.

      To verify that documents have been replicated, you can:

      • Monitor the Sync Gateway sequence number returned by the database endpoint (GET /{db}/). The sequence number increments for every change that happens on the Sync Gateway database.

      • Query a document by ID on the Sync Gateway REST API (GET /{db}/{id}).

      • Query a document from the Query Workbench on the Couchbase Server Console.

      Couchbase Lite 2.0 uses WebSockets as the communication protocol to transmit data. Some load balancers are not configured for WebSocket connections by default (NGINX for example); so it might be necessary to explicitly enable them in the load balancer’s configuration (see Load Balancers).

      By default, the WebSocket protocol uses compression to optimize for speed and bandwidth utilization. The level of compression is set on Sync Gateway and can be tuned in the configuration file (replicator_compression).

      Replication Ordering

      To optimize for speed, the replication protocol doesn’t guarantee that documents will be received in a particular order. So do not rely receiving replicated data in a specific sequence when using the replication or database change listeners for example.

      Replicator Notifications on a Custom Executor

      Prior to version 2.6, Couchbase Lite spun up multiple executors. This policy could result in too many threads being spun up.

      An executor manages a pool of threads and, perhaps, a queue in front of the executor, to handle the asynchronous callbacks. Couchbase Lite API calls which are processed by an executor are listed below.

      Query.addChangeListener
      MessageEndpointListerner.addChangeListener
      LiveQuery.addChangeListener
      AbstractReplicator.addDocumentReplicationListener
      AbstractReplicator.addChangeListener
      Database.addChangeListener
      Database.addDocumentChangeListener
      Database.addDatabaseChangeListener
      Database.addChangeListener

      As of version 2.6, Couchbase sometimes uses its own internal executor to run asynchronous client code. While this is fine for small tasks, larger tasks — those that take significant compute time, or that perform I/O — can block Couchbase processing. If this happens your application will fail with a RejectedExecutionException and it may be necessary to create a separate executor on which to run the large tasks.

      The following examples show how to specify a separate executor in the client code. The client code executor can enforce an application policy for delivery ordering and the number of threads.

      Guaranteed Order Delivery

      /**
       * This version guarantees in order delivery and is parsimonious with space
       * The listener does not need to be thread safe (at least as far as this code is concerned).
       * It will run on only thread (the Executor's thread) and must return from a given call
       * before the next call commences.  Events may be delivered arbitrarily late, though,
       * depending on how long it takes the listener to run.
       */
      public class InOrderExample {
          private static final ExecutorService IN_ORDER_EXEC = Executors.newSingleThreadExecutor();
      
          public Replicator runReplicator(Database db1, Database db2, ReplicatorChangeListener listener)
              throws CouchbaseLiteException {
              ReplicatorConfiguration config = new ReplicatorConfiguration(db1, new DatabaseEndpoint(db2));
              config.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH_AND_PULL);
              config.setContinuous(false);
      
              Replicator repl = new Replicator(config);
              ListenerToken token = repl.addChangeListener(IN_ORDER_EXEC, listener::changed);
      
              repl.start();
      
              return repl;
          }
      }

      Maximum Throughput

      /**
       * This version maximizes throughput.  It will deliver change notifications as quickly
       * as CPU availability allows. It may deliver change notifications out of order.
       * Listeners must be thread safe because they may be called from multiple threads.
       * In fact, they must be re-entrant because a given listener may be running on mutiple threads
       * simultaneously.  In addition, when notifications swamp the processors, notifications awaiting
       * a processor will be queued as Threads, (instead of as Runnables) with accompanying memory
       * and GC impact.
       */
      public class MaxThroughputExample {
          private static final ExecutorService MAX_THROUGHPUT_EXEC = Executors.newCachedThreadPool();
      
          public Replicator runReplicator(Database db1, Database db2, ReplicatorChangeListener listener)
              throws CouchbaseLiteException {
              ReplicatorConfiguration config = new ReplicatorConfiguration(db1, new DatabaseEndpoint(db2));
              config.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH_AND_PULL);
              config.setContinuous(false);
      
              Replicator repl = new Replicator(config);
              ListenerToken token = repl.addChangeListener(MAX_THROUGHPUT_EXEC, listener::changed);
      
              repl.start();
      
              return repl;
          }
      }

      Extreme Configurability

      /**
       * This version demonstrates the extreme configurability of the CouchBase Lite replicator callback system.
       * It may deliver updates out of order and does require thread-safe and re-entrant listeners
       * (though it does correctly synchronizes tasks passed to it using a SynchronousQueue).
       * The thread pool executor shown here is configured for the sweet spot for number of threads per CPU.
       * In a real system, this single executor might be used by the entire application and be passed to
       * this module, thus establishing a reasonable app-wide threading policy.
       * In an emergency (Rejected Execution) it lazily creates a backup executor with an unbounded queue
       * in front of it.  It, thus, may deliver notifications late, as well as out of order.
       */
      public class PolicyExample {
          private static final int CPUS = Runtime.getRuntime().availableProcessors();
      
          private static ThreadPoolExecutor BACKUP_EXEC;
      
          private static final RejectedExecutionHandler BACKUP_EXECUTION
              = new RejectedExecutionHandler() {
              public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                  synchronized (this) {
                      if (BACKUP_EXEC == null) { BACKUP_EXEC = createBackupExecutor(); }
                  }
                  BACKUP_EXEC.execute(r);
              }
          };
      
          private static ThreadPoolExecutor createBackupExecutor() {
              ThreadPoolExecutor exec
                  = new ThreadPoolExecutor(CPUS + 1, 2 * CPUS + 1, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
              exec.allowCoreThreadTimeOut(true);
              return exec;
          }
      
          private static final ThreadPoolExecutor STANDARD_EXEC
              = new ThreadPoolExecutor(CPUS + 1, 2 * CPUS + 1, 30, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
      
          static { STANDARD_EXEC.setRejectedExecutionHandler(BACKUP_EXECUTION); }
      
          public Replicator runReplicator(Database db1, Database db2, ReplicatorChangeListener listener)
              throws CouchbaseLiteException {
              ReplicatorConfiguration config = new ReplicatorConfiguration(db1, new DatabaseEndpoint(db2));
              config.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH_AND_PULL);
              config.setContinuous(false);
      
              Replicator repl = new Replicator(config);
              ListenerToken token = repl.addChangeListener(STANDARD_EXEC, listener::changed);
      
              repl.start();
      
              return repl;
          }
      }

      Troubleshooting

      As always, when there is a problem with replication, logging is your friend. The following example increases the log output for activity related to replication with Sync Gateway.

      Database.setLogLevel(LogDomain.REPLICATOR, LogLevel.VERBOSE);

      Authentication

      By default, Sync Gateway does not enable authentication. This is to make it easier to get up and running with synchronization. You can enable authentication with the following properties in the configuration file:

      {
        "databases": {
          "mydatabase": {
            "users": {
              "GUEST": {"disabled": true}
            }
          }
        }
      }

      To authenticate with Sync Gateway, an associated user must first be created. Sync Gateway users can be created through the POST /{db}/_user endpoint on the Admin REST API. Provided that the user exists on Sync Gateway, there are two ways to authenticate from a Couchbase Lite client: Basic Authentication or Session Authentication.

      Basic Authentication

      You can provide a user name and password to the basic authenticator class method. Under the hood, the replicator will send the credentials in the first request to retrieve a SyncGatewaySession cookie and use it for all subsequent requests during the replication. This is the recommended way of using basic authentication. The following example initiates a one-shot replication as the user username with the password password.

      URLEndpoint target = new URLEndpoint(new URI("ws://localhost:4984/mydatabase"));
      
      ReplicatorConfiguration config = new ReplicatorConfiguration(database, target);
      config.setAuthenticator(new BasicAuthenticator("username", "password"));
      
      // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
      replicator = new Replicator(config);
      replicator.start();

      Session Authentication

      Session authentication is another way to authenticate with Sync Gateway. A user session must first be created through the POST /{db}/_session endpoint on the Public REST API. The HTTP response contains a session ID which can then be used to authenticate as the user it was created for. The following example initiates a one-shot replication with the session ID that is returned from the POST /{db}/_session endpoint.

      URLEndpoint target = new URLEndpoint(new URI("ws://localhost:4984/mydatabase"));
      
      ReplicatorConfiguration config = new ReplicatorConfiguration(database, target);
      config.setAuthenticator(new SessionAuthenticator("904ac010862f37c8dd99015a33ab5a3565fd8447"));
      
      // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
      replicator = new Replicator(config);
      replicator.start();

      Replication Status

      The replication.Status.Activity property can be used to check the status of a replication. For example, when the replication is actively transferring data and when it has stopped.

      replicator.addChangeListener(change -> {
          if (change.getStatus().getActivityLevel() == Replicator.ActivityLevel.STOPPED) {
              Log.i(TAG, "Replication stopped");
          }
      });

      The following table lists the different activity levels in the API and the meaning of each one.

      State Meaning

      STOPPED

      The replication is finished or hit a fatal error.

      OFFLINE

      The replicator is offline as the remote host is unreachable.

      CONNECTING

      The replicator is connecting to the remote host.

      IDLE

      The replication caught up with all the changes available from the server. The IDLE state is only used in continuous replications.

      BUSY

      The replication is actively transferring data.

      The replication change object also has properties to track the progress (change.status.completed and change.status.total). But since the replication occurs in batches and the total count can vary through the course of a replication, those progress indicators are not very useful from the standpoint of an app user. Hence, these should not be used for tracking the actual progress of the replication.

      Handling Network Errors

      When replicator detects a network error it updates its status depending on the error type (permanent or temporary) and returns an appropriate HTTP error code.

      The following code snippet adds a Change Listener, which monitors a replication for errors and logs the the returned error code.

      Monitoring for network errors
      replicator.addChangeListener(change -> {
          CouchbaseLiteException error = change.getStatus().getError();
          if (error != null) { Log.w(TAG, "Error code:: %d", error); }
      });
      replicator.start();

      For permanent network errors (for example, 404 not found, or 401 unauthorized): Replicator will stop permanently, whether setContinuous is true or false. Of course, it sets its status to STOPPED

      For recoverable or temporary errors: Replicator sets its status to OFFLINE, then:

      • If setContinuous=true it retries the connection indefinitely

      • If setContinuous=false (one-shot) it retries the connection a limited number of times.

      The following error codes are considered temporary by the Couchbase Lite replicator and thus will trigger a connection retry.

      • 408: Request Timeout

      • 429: Too Many Requests

      • 500: Internal Server Error

      • 502: Bad Gateway

      • 503: Service Unavailable

      • 504: Gateway Timeout

      • 1001: DNS resolution error

      Replication Events

      You can choose to register for document updates during a replication.

      For example, the code snippet below registers a listener to monitor document replication performed by the replicator referenced by the variable replicator. It prints the document ID of each document received and sent.

      ListenerToken token = replicator.addDocumentReplicationListener(replication -> {
      
          Log.i(TAG, "Replication type: " + ((replication.isPush()) ? "Push" : "Pull"));
          for (ReplicatedDocument document : replication.getDocuments()) {
              Log.i(TAG, "Doc ID: " + document.getID());
      
              CouchbaseLiteException err = document.getError();
              if (err != null) {
                  // There was an error
                  Log.e(TAG, "Error replicating document: ", err);
                  return;
              }
      
              if (document.flags().contains(DocumentFlag.DocumentFlagsDeleted)) {
                  Log.i(TAG, "Successfully replicated a deleted document");
              }
          }
      });
      
      replicator.start();

      The following example stops the change listener with the token from the previous example.

      replicator.removeChangeListener(token);

      Document Access Removal Behavior

      When access to a document is removed on Sync Gateway, the document replication listener sends a notification with the AccessRemoved flag set to true and subsequently purges the document from the database.

      Custom Headers

      Custom headers can be set on the configuration object. And the replicator will send those header(s) in every request. As an example, this feature can be useful to pass additional credentials when there is an authentication or authorization step being done by a proxy server (between Couchbase Lite and Sync Gateway).

      ReplicatorConfiguration config = new ReplicatorConfiguration(database, endpoint);
      Map<String, String> headers = new HashMap<>();
      headers.put("CustomHeaderName", "Value");
      config.setHeaders(headers);

      Replication Checkpoint Reset

      Replicators use checkpoints to keep track of documents sent to the target database. Without checkpoints, Couchbase Lite would replicate the entire database content to the target database on each connection, even though previous replications may already have replicated some or all of that content.

      This functionality is generally not a concern to application developers. However, if you do want to force the replication to start again from zero, use the checkpoint reset method replicator.resetCheckpoint() before starting the replicator.

      replicator.resetCheckpoint();
      replicator.start();

      Replication Filters

      Replication Filters allow you to have quick control over which documents are stored as the result of a push and/or pull replication.

      Push Filter

      A push filter allows an app to push a subset of a database to the server, which can be very useful in some circumstances. For instance, high-priority documents could be pushed first, or documents in a "draft" state could be skipped.

      The following example filters out documents whose type property is equal to draft.

      URLEndpoint target = new URLEndpoint(new URI("ws://localhost:4984/mydatabase"));
      
      ReplicatorConfiguration config = new ReplicatorConfiguration(database, target);
      config.setPushFilter((document, flags) -> flags.contains(DocumentFlag.DocumentFlagsDeleted)); (1)
      
      // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
      replicator = new Replicator(config);
      replicator.start();
      1 The callback should follow the semantics of a pure function. Otherwise, long running functions would slow down the replicator considerably. Furthermore, your callback should not make assumptions about what thread it is being called on.

      Pull Filter

      A pull filter gives an app the ability to validate documents being pulled, and skip ones that fail. This is an important security mechanism in a peer-to-peer topology with peers that are not fully trusted.

      Pull replication filters are not a substitute for channels. Sync Gateway channels are designed to be scalable (documents are filtered on the server) whereas a pull replication filter is applied to a document once it has been downloaded.
      URLEndpoint target = new URLEndpoint(new URI("ws://localhost:4984/mydatabase"));
      
      ReplicatorConfiguration config = new ReplicatorConfiguration(database, target);
      config.setPullFilter((document, flags) -> "draft".equals(document.getString("type"))); (1)
      
      // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
      replicator = new Replicator(config);
      replicator.start();
      1 The callback should follow the semantics of a pure function. Otherwise, long running functions would slow down the replicator considerably. Furthermore, your callback should not make assumptions about what thread it is being called on.
      Losing access to a document (via the Sync Function) also triggers the pull replication filter. Filtering out such an event would retain the document locally. As a result, there would be a local copy of the document disjointed from the one that resides on Couchbase Server. Further updates to the document stored on Couchbase Server would not be received in pull replications and further local edits could be potentially pushed, which would result in 409 errors since access has been revoked.

      Handling Conflicts

      Document conflicts can occur if multiple changes are made to the same version of a document by multiple peers in a distributed system. For Couchbase Mobile, this can be a Couchbase Lite or Sync Gateway database instance.

      Such conflicts can occur after either of the following events:

      Deletes always win. So, in either of the above cases, if one of the changes was a Delete then that change wins.

      The following sections discuss each scenario in more detail.

      Case 1: Conflicts when a replication is in progress

      There’s no practical way to prevent a conflict when incompatible changes to a document are be made in multiple instances of an app. The conflict is realized only when replication propagates the incompatible changes to each other.

      Example 2. A typical replication conflict scenario:
      1. Molly uses her device to create DocumentA.

      2. Replication syncs DocumentA to Naomi’s device.

      3. Molly uses her device to apply ChangeX to DocumentA.

      4. Naomi uses her device to make a different change, ChangeY, to DocumentA.

      5. Replication syncs ChangeY to Molly’s device.

        This device already has ChangeX putting the local document in conflict.

      6. Replication syncs ChangeX to Naomi’s device.

        This device already has ChangeY and now Naomi’s local document is in conflict.

      Automatic Conflict Resolution

      These rules apply only to conflicts arising from replication.

      Couchbase Lite uses the following rules to handle conflicts such as those described in A typical replication conflict scenario:

      • If one of the changes is a deletion:

        A deleted document (that is, a tombstone) always wins over a document update.

      • If both changes are document changes:

        The change with the most revisions will win.

        Since each change creates a revision with an ID prefixed by an incremented version number, the winner is the change with the highest version number.

      The result is saved internally by the Couchbase Lite replicator. Those rules describe the internal behavior of the replicator. For additional control over the handling of conflicts, including when a replication is in progress, see Custom Conflict Resolution.

      Custom Conflict Resolution

      Starting in Couchbase Lite 2.6, application developers who want more control over how document conflicts are handled can use custom logic to select the winner between conflicting revisions of a document.

      If a custom conflict resolver is not provided, the system will automatically resolve conflicts as discussed in Automatic Conflict Resolution, and as a consequence there will be no conflicting revisions in the database.

      While this is true of any user defined functions, app developers must be strongly cautioned against writing sub-optimal custom conflict handlers that are time consuming and could slow down the client’s save operations.

      To implement custom conflict resolution during replication, you must implement the following steps.

      Conflict Resolver

      Apps have the following strategies for resolving conflicts:

      • Local Wins: The current revision in the database wins.

      • Remote Wins: The revision pulled from the remote endpoint through replication wins.

      • Merge: Merge the content bodies of the conflicting revisions.

      • Local Wins

      • Remote Wins

      • Merge

      class LocalWinConflictResolver implements ConflictResolver {
          public Document resolve(Conflict conflict) {
              return conflict.getLocalDocument();
          }
      }
      class RemoteWinConflictResolver implements ConflictResolver {
          public Document resolve(Conflict conflict) {
              return conflict.getRemoteDocument();
          }
      }
      class MergeConflictResolver implements ConflictResolver {
          public Document resolve(Conflict conflict) {
              Map<String, Object> merge = conflict.getLocalDocument().toMap();
              merge.putAll(conflict.getRemoteDocument().toMap());
              return new MutableDocument(conflict.getDocumentId(), merge);
          }
      }

      When a null document is returned by the resolver, the conflict will be resolved as a document deletion.

      Important Guidelines and Best Practices

      There are some important points to be noted:

      1. If you have multiple replicators, it is recommended that instead of distinct resolvers, you should use a unified conflict resolver across all replicators. Failure to do so could potentially lead to data loss under exception cases or if the app is terminated (by the user or an app crash) while there are pending conflicts.

      2. If the document ID of the document returned by the resolver does not correspond to the document that is in conflict then the replicator will log a warning message.

        Developers are encouraged to review the warnings and fix the resolver to return a valid document ID.
      3. If a document from a different database is returned, the replicator will treat it as an error. A document replication event will be posted with an error and an error message will be logged.

        Apps are encouraged to observe such errors and take appropriate measures to fix the resolver function.
      4. When the replicator is stopped, the system will attempt to resolve outstanding and pending conflicts before stopping. Hence apps should expect to see some delay when attempting to stop the replicator depending on the number of outstanding documents in the replication queue and the complexity of the resolver function.

      5. If there is an exception thrown in the resolve() method, the exception will be caught and handled:

        1. The conflict to resolve will be skipped. The pending conflicted documents will be resolved when the replicator is restarted.

        2. The exception will be reported in the warning logs.

        3. The exception will be reported in the document replication event.

          While the system will handle exceptions in the manner specified above, it is strongly encouraged for the resolver function to catch exceptions and handle them in a way appropriate to their needs.
      Configure the Replicator

      The implemented custom conflict resolver can be registered on the replicator configuration object. The default value of the conflictResolver is null. When the value is null, the default conflict resolution will be applied.

      URLEndpoint target = new URLEndpoint(new URI("ws://localhost:4984/mydatabase"));
      
      ReplicatorConfiguration config = new ReplicatorConfiguration(database, target);
      config.setConflictResolver(new LocalWinConflictResolver());
      
      Replicator replication = new Replicator(config);
      replication.start();

      Case 2: Conflicts when saving a document

      When updating a document, you need to consider the possibility of update conflicts. Update conflicts can occur when you try to update a document that’s been updated since you read it. Here’s a typical sequence of events that would create an update conflict:

      1. Your code reads the document’s current properties, and constructs a modified copy to save.

      2. Another thread (perhaps the replicator) updates the document, creating a new revision with different properties.

      3. Your code updates the document with its modified properties, for example using db.save( mutableDocumentName).

      Automatic Conflict Resolution

      In Couchbase Lite 2.0, by default, the conflict is automatically resolved and only one document update is stored in the database. The Last-Write-Win (LWW) algorithm is used to pick the winning update. So in effect, the changes from step 2 would be overwritten and lost.

      If the probability of update conflicts is high in your app and you wish to avoid the possibility of overwritten data, the save and delete APIs provide additional method signatures with concurrency control:

      • save(document: MutableDocument, concurrencyControl: ConcurrencyControl): attempts to save the document with a concurrency control. The concurrency control parameter has two possible values:

        • lastWriteWins (default): The last operation wins if there is a conflict.

        • failOnConflict: The operation will fail if there is a conflict. In this case, the app can detect the error that is being thrown, and handle it by re-reading the document, making the necessary conflict resolution, then trying again.

      Similarly to the save operation, the delete operation also has two method signatures to specify how to handle a possible conflict:

      • delete(document: Document): The last write will win if there is a conflict.

      • delete(document: Document, concurrencyControl: ConcurrencyControl): attempts to delete the document with a concurrency control. The concurrency control parameter has two possible values:

        • lastWriteWins (default): The last operation wins if there is a conflict.

        • failOnConflict: The operation will fail if there is a conflict. In this case, the app can detect the error that is being thrown, and handle it by re-reading the document, making the necessary conflict resolution, then trying again.

      Custom Conflict Resolution

      Starting in Couchbase Lite 2.6, we allow developers to hook a conflict handler when saving a document so that developers can easily handle the conflict in a single save method call.

      To implement custom conflict resolution when saving a document, apps must call the save() method with a conflict handler.

      The following code snippet shows an example of merging properties from the existing document (current) into the one being saved (new). In the event of conflicting keys, it will pick the key value from new.

      Document doc = database.getDocument("xyz");
      if (doc == null) { return; }
      MutableDocument mutableDocument = doc.toMutable();
      mutableDocument.setString("name", "apples");
      
      database.save(
          mutableDocument,
          (newDoc, curDoc) -> {
              if (curDoc == null) { return false; }
              Map<String, Object> dataMap = curDoc.toMap();
              dataMap.putAll(newDoc.toMap());
              newDoc.setData(dataMap);
              return true;
          });

      Important points to be noted:

      1. Within the conflict handler, you can modify the document parameter which is the same instance of Document that is passed to the save() method. So in effect, you will be directly modifying the document that is being saved.

      2. When handling is done, the method must return true.

      3. If the handler could not resolve the conflict, it can return false. In this case, the save method will cancel the save operation and return false the same way as using the save() method with the failOnConflict concurrency control.

      4. If there is an exception thrown in the handle() method, the exception will be caught and rethrown in the save() method.

      Database Replicas

      Enterprise Edition ONLY
      Database replicas are available only in CouchbaseLite Enterprise Edition (https://www.couchbase.com/downloads).

      Couchbase Lite supports replication between two local databases, which allows a Couchbase Lite replicator to store data on secondary storage. This is especially useful in scenarios where a user’s device may be damaged and its data moved to a different device.

      DatabaseEndpoint targetDatabase = new DatabaseEndpoint(database2);
      ReplicatorConfiguration replicatorConfig = new ReplicatorConfiguration(database1, targetDatabase);
      replicatorConfig.setReplicatorType(ReplicatorConfiguration.ReplicatorType.PUSH);
      
      // Create replicator (be sure to hold a reference somewhere that will prevent the Replicator from being GCed)
      replicator = new Replicator(replicatorConfig);
      replicator.start();

      Certificate Pinning

      Couchbase Lite supports certificate pinning. Certificate pinning is a technique that can be used by applications to "pin" a host to its certificate. The certificate is typically delivered to the client by an out-of-band channel and bundled with the client. In this case, Couchbase Lite uses this embedded certificate to verify the trustworthiness of the server and no longer needs to rely on a trusted third party for that (commonly referred to as the Certificate Authority).

      The following steps describe how to configure certificate pinning between Couchbase Lite and Sync Gateway.

      1. Create your own self-signed certificate with the openssl command. After completing this step, you should have 3 files: cert.pem, cert.cer and privkey.pem.

      2. Configure Sync Gateway with the cert.pem and privkey.pem files. After completing this step, Sync Gateway is reachable over https/wss.

      3. On the Couchbase Lite side, the replication must point to a URL with the wss scheme and configured with the cert.cer file created in step 1.

        InputStream is = getAsset("cert.cer");
        byte[] cert = IOUtils.toByteArray(is);
        if (is != null) {
            try { is.close(); }
            catch (IOException ignore) {}
        }
        
        config.setPinnedServerCertificate(cert);

        This example loads the certificate from the application sandbox, then converts it to the appropriate type to configure the replication object.

      4. Build and run your app. The replication should now run successfully over https with certificate pinning.

      Troubleshooting

      If Sync Gateway is configured with a self signed certificate but your app points to a ws scheme instead of wss you will encounter an error with status code 11006.

      CouchbaseLite Replicator ERROR: {Repl#2} Got LiteCore error: WebSocket error 1006 "connection closed abnormally"

      If Sync Gateway is configured with a self signed certificate, and your app points to a wss scheme but the replicator configuration isn’t using the certificate you will encounter an error with status code 5011.

      CouchbaseLite Replicator ERROR: {Repl#2} Got LiteCore error: Network error 11 "server TLS certificate is self-signed or has unknown root cert"

      Peer-to-Peer Sync

      Enterprise Edition only
      Peer-to-peer sync is an Enterprise Edition feature. You must purchase the Enterprise License which includes official Couchbase Support to use it in production (also see the FAQ).

      Peer-to-peer sync allows devices running Couchbase Lite to directly sync data with each other. As part of this, Couchbase Lite is responsible for storing the data and keeping track of the data exchange, but isn’t responsible for the data transfer itself. Sending and receiving data must be handled by the platform APIs or a third party framework. In this section, we will refer to these third party frameworks as communication frameworks.

      Thus, to enable peer-to-peer sync with Couchbase Lite, the application must use the Communication Framework with Couchbase Lite. The following sections describe a typical peer-to-peer workflow. Where applicable, we discuss how to integrate Couchbase Lite into the workflow.

      In Couchbase Lite, a peer can take on one of these two roles:

      • Active Peer: The peer that initializes the connection and replication (i.e the "client" side).

      • Passive Peer: The passive side reacts to things that it receives but does not initiate any communication on its own (i.e. the "server" side).

      Peer Discovery

      Peer discovery is the first step. The communication framework will generally include a peer discovery API for devices to advertise themselves on the network and to browse for other peers.

      discovery

      Active Peer

      The first step is to initialize the Couchbase Lite database.

      Passive Peer

      In addition to initializing the database, the passive peer must initialize the MessageEndpointListener. The MessageEndpointListener acts as as a listener for incoming connections.

      DatabaseConfiguration databaseConfiguration = new DatabaseConfiguration();
      Database database = new Database(DB_NAME, databaseConfiguration);
      MessageEndpointListenerConfiguration listenerConfiguration = new MessageEndpointListenerConfiguration(
          database,
          ProtocolType.MESSAGE_STREAM);
      this.messageEndpointListener = new MessageEndpointListener(listenerConfiguration);

      Peer Selection and Connection Setup

      Once a peer device is found, it is the application code’s responsibility to decide whether it should establish a connection with that peer. This step includes inviting a peer to a session and peer authentication.

      This is handled by the Communication Framework.

      selection

      Once the remote peer has been authenticated, the next step is to connect with that peer and initialize the Message Endpoint API.

      Replication Setup

      connection

      Active Peer

      When the connection is established, the active peer must instantiate a MessageEndpoint object corresponding to the remote peer.

      DatabaseConfiguration databaseConfiguration = new DatabaseConfiguration(context);
      Database database = new Database(DB_NAME, databaseConfiguration);
      
      // The delegate must implement the `MessageEndpointDelegate` protocol.
      MessageEndpoint messageEndpointTarget = new MessageEndpoint(
          "UID:123",
          "active",
          ProtocolType.MESSAGE_STREAM,
          this);

      The MessageEndpoint initializer takes the following arguments.

      1. uid: a unique ID that represents the remote active peer.

      2. target: This represents the remote passive peer and could be any suitable representation of the remote peer. It could be an Id, URL etc. If using the MultiPeerConnectivity Framework, this could be the MCPeerID.

      3. protocolType: specifies the kind of transport you intend to implement. There are two options.

        • The default (MessageStream) means that you want to "send a series of messages", or in other words the Communication Framework will control the formatting of messages so that there are clear boundaries between messages.

        • The alternative (ByteStream) means that you just want to send raw bytes over the stream and Couchbase should format for you to ensure that messages get delivered in full.

          Typically, the Communication Framework will handle message assembly and disassembly so you would use the MessageType option in most cases.

      4. delegate: the delegate that will implement the MessageEndpointDelegate protocol, which is a factory for MessageEndpointConnection.

      Then, a Replicator is instantiated with the initialized MessageEndpoint as the target.

      ReplicatorConfiguration config = new ReplicatorConfiguration(database, messageEndpointTarget);
      
      // Create the replicator object.
      replicator = new Replicator(config);
      // Start the replication.
      replicator.start();

      Next, Couchbase Lite will call back the application code through the MessageEndpointDelegate.createConnection interface method. When the application receives the callback, it must create an instance of MessageEndpointConnection and return it.

      /* implementation of MessageEndpointDelegate */
      @NonNull
      @Override
      public MessageEndpointConnection createConnection(@NonNull MessageEndpoint endpoint) {
          return new ActivePeerConnection(); /* implements MessageEndpointConnection */
      }

      Next, Couchbase Lite will call back the application code through the MessageEndpointConnection.open method.

      /* implementation of MessageEndpointConnection */
      @Override
      public void open(@NonNull ReplicatorConnection connection, @NonNull MessagingCompletion completion) {
          replicatorConnection = connection;
          completion.complete(true, null);
      }

      The connection argument is then set on an instance variable. The application code must keep track of every ReplicatorConnection associated with every MessageEndpointConnection.

      The MessageError argument in the completion block is used to specify if the error is recoverable or not. If it is a recoverable error, the replicator will kick off a retry process which will result to creating a new MessageEndpointConnection instance.

      Passive Peer

      The first step after connection establishment on the passive peer is to initialize a new MessageEndpointConnection and pass it to the listener. This tells the listener to accept incoming data from that peer.

      PassivePeerConnection connection = new PassivePeerConnection(context); /* implements
      MessageEndpointConnection */
      messageEndpointListener.accept(connection);

      messageEndpointListener is the instance of the MessageEndpointListener that was created in the first step (Peer Discovery)

      Couchbase Lite will then call back the application code through the MessageEndpointConnection.open method.

      /* implementation of MessageEndpointConnection */
      @Override
      public void open(@NonNull ReplicatorConnection connection, @NonNull MessagingCompletion completion) {
          replicatorConnection = connection;
          completion.complete(true, null);
      }

      The connection argument is then set on an instance variable. The application code must keep track of every ReplicatorConnection associated with every MessageEndpointConnection.

      At this point, the connection is established and both peers are ready to exchange data.

      Push/Pull Replication

      Typically, an application needs to send data and receive data. Directionality of the replication could be any of the following.

      • Push only: The data is pushed from the local database to the remote database.

      • Pull only: The data is pulled from the remote database to the local database.

      • Push and Pull: The data is exchanged both ways.

      Usually, the remote is a Sync Gateway database which is identified through a URL. In the context of peer-to-peer syncing, the remote is another Couchbase Lite database.

      replication

      The replication lifecycle is handled through the MessageEndpointConnection.

      Active Peer

      When Couchbase Lite calls back the application code through the MessageEndpointConnection.send method, you should send that data to the other peer using the communication framework.

      /* implementation of MessageEndpointConnection */
      @Override
      public void send(@NonNull Message message, @NonNull MessagingCompletion completion) {
          /* send the data to the other peer */
          /* ... */
          /* call the completion handler once the message is sent */
          completion.complete(true, null);
      }

      Once the data is sent, call the completion block to acknowledge the completion. You can use the MessageError in the completion block to specify if the error is recoverable or not. If it is a recoverable error, the replicator will kick off a retry process which will result to creating a new MessageEndpointConnection.

      When data is received from the passive peer via the Communication Framework, you call the ReplicatorConnection.receive method.

      replicatorConnection.receive(message);

      The replication connection’s receive method is called which then processes the data in order to persist it to the local database.

      Passive Peer

      As in the case of the active peer, the passive peer must implement the MessageEndpointConnection.send method to send data to the other peer.

      /* implementation of MessageEndpointConnection */
      @Override
      public void send(@NonNull Message message, @NonNull MessagingCompletion completion) {
          /* send the data to the other peer */
          /* ... */
          /* call the completion handler once the message is sent */
          completion.complete(true, null);
      }

      Once the data is sent, call the completion block to acknowledge the completion. You can use the MessageError in the completion block to specify if the error is recoverable or not. If it is a recoverable error, the replicator will kick off a retry process which will result to creating a new MessageEndpointConnection.

      When data is received from the active peer via the Communication Framework, you call the ReplicatorConnection.receive method.

      replicatorConnection.receive(message);

      Connection Teardown

      When a peer disconnects from a peer-to-peer network, all connected peers are notified. The disconnect notification is a good opportunity to close and remove a replication connection. The steps to teardown the connection are slightly different depending on whether it is the active or passive peer that disconnects first. We will cover each case below.

      Initiated by Active Peer

      dis active

      Active Peer

      When an active peer disconnects, it must call the ReplicatorConnection.close method.

      replicatorConnection.close(null);

      Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

      @Override
      public void close(Exception error, @NonNull MessagingCloseCompletion completion) {
          /* disconnect with communications framework */
          /* ... */
          /* call completion handler */
          completion.complete();
      }

      Passive Peer

      When the passive peer receives the corresponding disconnect notification from the Communication Framework, it must call the ReplicatorConnection.close method.

      replicatorConnection.close(null);

      Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

      /* implementation of MessageEndpointConnection */
      @Override
      public void close(Exception error, @NonNull MessagingCloseCompletion completion) {
          /* disconnect with communications framework */
          /* ... */
          /* call completion handler */
          completion.complete();
      }

      Initiated by Passive Peer

      dis passive

      Passive Peer

      When the passive disconnects, it must class the MessageEndpointListener.closeAll method.

      messageEndpointListener.closeAll();

      Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

      /* implementation of MessageEndpointConnection */
      @Override
      public void close(Exception error, @NonNull MessagingCloseCompletion completion) {
          /* disconnect with communications framework */
          /* ... */
          /* call completion handler */
          completion.complete();
      }

      Active Peer

      When the active peer receives the corresponding disconnect notification from the Communication Framework, it must call the ReplicatorConnection.close method.

      replicatorConnection.close(null);

      Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

      @Override
      public void close(Exception error, @NonNull MessagingCloseCompletion completion) {
          /* disconnect with communications framework */
          /* ... */
          /* call completion handler */
          completion.complete();
      }

      Thread Safety

      The Couchbase Lite API is thread safe except for calls to mutable objects: MutableDocument, MutableDictionary and MutableArray.

      UPGRADING

      Database

      Databases created using Couchbase Lite 1.2 or later can still be used with Couchbase Lite 2.x; but will be automatically updated to the current 2.x version. This feature is only available for the default storage type (i.e., not a ForestDB database). Additionally, the automatic migration feature does not support encrypted databases, so if the 1.x database is encrypted you will first need to disable encryption using the Couchbase Lite 1.x API (see the 1.x Database Guide).

      Handling of Existing Conflicts

      If there are existing conflicts in the 1.x database, the automatic upgrade process copies the default winning revision to the new database and does NOT copy any conflicting revisions.

      Handling of Existing Attachments

      Attachments persisted in a 1.x database are copied to the new database. NOTE: The relevant Couchbase Lite API is now called the Blob API not the Attachments API.

      The functionally is identical but the internal schema for attachments has changed. Blobs are stored anywhere in the document, just like other value types, whereas in 1.x they were stored under the _attachments field. The automatic upgrade functionality does not update the internal schema for attachments, so they remain accessible under the _attachments field. The following example shows how to retrieve an attachment that was created in a 1.x database with a 2.x API.

      Dictionary attachments = document.getDictionary("_attachments");
      Blob blob = attachments != null ? attachments.getBlob("avatar") : null;
      byte[] content = blob != null ? blob.getContent() : null;

      Replication Compatibility

      The current replication protocol is not backwards compatible with the 1.x replication protocol. Therefore, to use replication with Couchbase Lite 2.x, the target Sync Gateway instance must also be upgraded to 2.x.

      Sync Gateway 2.x will continue to accept clients that connect through the 1.x protocol. It will automatically use the 1.x replication protocol when a Couchbase Lite 1.x client connects through http://localhost:4984/db and the 2.x replication protocol when a Couchbase Lite 2.x client connects through ws://localhost:4984/db. This allows for a smoother transition to get all your user base onto a version of your application built with Couchbase Lite 2.x.

      SUPPORTED VERSIONS

      Couchbase Lite Java will be supported on x86 64-bit platforms. These are the targeted OS versions

      Table 1. Table Supported OS Versions
      OS Version Type

      RHEL

      6.10 [DEPRECATED]

      Desktop & Web Service/Servlet (Tomcat)

      7

      Desktop & Web Service/Servlet (Tomcat)

      centOS

      6.10 [DEPRECATED]

      Desktop & Web Service/Servlet (Tomcat)

      7

      Desktop & Web Service/Servlet (Tomcat)

      Ubuntu

      16.0

      Desktop & Web Service/Servlet (Tomcat)

      Debian

      GNU/Linux 8

      Desktop & Web Service/Servlet (Tomcat)

      Microsoft Server

      Windows Server 2012

      Web Service/Servlet (Tomcat)

      Microsoft

      Windows 10

      Desktop

      Apple

      macOS 10.12.6 (High Sierra)

      Desktop & Web Service/Servlet (Tomcat)

      Support for centOS 6 and RHEL 6 is deprecated in this release. Deprecated versions will be removed in a future release and we recommend that you plan to migrate away from the deprecated OS versions.

      SUPPLEMENTARY INFORMATION

      Preparatory Steps

      Install Couchbase server and Sync Gateway

      Before you can run any Couchbase Lite Java app you will need to have runnable instances of Couchbase server and Sync Gateway. If you have not already done so:

      Install Couchbase server and Sync Gateway following steps outlined in Installing Sync Gateway →

      Create the Getting Started App Database

      1. Within Couchbase server, create a bucket named getting-started.cblite2

        Follow the instructions here

      2. Create a RBAC user for Sync Gateway

        See instructions here

        • Set username to sync-gateway

        • Set password to password

      Using Downloaded Binaries

      Package Contents

      The download package contains a license file, jar libraries for the appropriate edition of the Couchbase Lite and a set of Linux shared libraries.

      Get the download package from the Couchbase Downloads page.

      • Community Edition — couchbase-lite-java-2.7.0

      • Enterprise Edition — couchbase-lite-java-ee-2.7.0

      When unpacked the package contains the following:

      • A lib folder containing all necessary JAR files:

        • couchbase-lite-java-2.7.0 or for EE couchbase-lite-java-ee-2.7.0

        • json-20180813.jar

        • okhttp-3.9.1.jar

        • okio-1.13.0.jar

      • A support folder containing the Linux native libraries:

        This means you do not need to download and-or build compatible versions of system libraries for the Linux platform of choice.
        • libz (requires zlib v.1.2.11+)

        • libC++ requires libc++ v.3.6.0+)

        • libicu (requires ICU4C v.5.4.1+)

      Go back to GET STARTED

      Steps

      1. Download the zip file from the Couchbase Downloads page.

      2. Unpack the downloaded file to a location accessible to — and usable by — your chosen build environment.

        We’ll refer to that location — <your dir>/couchbase-lite-java-2.7.0 — as the <pathToCbl>.

      3. Include the following dependency in your build.gradle file, you can remove any Couchbase Lite Maven references:

        Dependencies {
          implementation fileTree(include: ['*.jar'], dir: <pathToCbl>/lib>
        }

      Where <pathToCbl> is the location of the downloaded Couchbase Lite library.

      Sample build gradle
      apply plugin: 'java'
      apply plugin: 'jar'
      // apply plugin: 'war'
      sourceCompatibility = 1.8
      repositories {
        jcenter()
      }
      dependencies {
          implementation fileTree(dir: 'libs', include: '*.jar')
          compileOnly "javax.servlet:javax.servlet-api:4.0.1"
      }

      Additional Steps for Linux

      In addition to setting-up your build environment, you also need to make the supplied native libraries (<pathToCbl/support) available:

      These libraries are provided only in the .zip distributable.
      1. Copy the native libraries (<pathToCbl>/support) to a location accessible to — and usable by — your build and runtime environments.

      2. Add the following libraries to the LD_LIBRARY_PATH in `$CATALINA_BASE/bin/setenv.sh:

        If the setenv.sh file doesn’t exist, you will need to create a new file.
        export LD_LIBRARY_PATH=<pathToCbl>/support/linux/x86_64/libicu:$LD_LIBRARY_PATH
        export LD_LIBRARY_PATH=<pathToCbl>/support/linux/x86_64/libz:$LD_LIBRARY_PATH
        export LD_LIBRARY_PATH=<pathToCbl>/support/linux/x86_64/libc++:$LD_LIBRARY_PATH

        Please refer to the Linux Platform Support section for the minimum version requirements of those libraries.

      Using the Gradle WebApp Plugin

      The simplest way to build and deploy your Couchbase Lite Java web app is to use a gradle plugin that provides a simple Tomcat harness.

      Our examples are based on the com.bmuschko.tomcat plugin — see com.bmuschko.tomcat on Github.

      Including the plugin in your build.gradle file make a number of tomcat tasks available to you. View them using:

      ./gradlew tasks

      This shows that the following web application tasks are now available:

      • tomcatJasper - Runs the JSP compiler and turns JSP pages into Java source.

      • tomcatRun - Uses your files as and where they are and deploys them to Tomcat.

      • tomcatRunWar - Assembles the web app into a war and deploys it to Tomcat.

      • tomcatStop - Stops Tomcat.

      So, to run the app use:

      tomcatRun

      About the Getting Started App

      The GettingStarted app will:

      • Create a database

        The app creates its database in the /getting-started.cblite2 directory relative to its root location when run (See: Finding a Database File).

      Explicitly state your required database location when creating your database (see: Finding a Database File for how to do this)
      • Add content to the DB

      • Run a simple query counting the DB rows

      • Start a one-shot, bi-directional replication using Sync Gateway and Couchbase server

      • Produce a simple report on the db Contents

        cblOutput
      Before starting your app ensure you have started both your Couchbase server and Sync Gateway instances.

      Deploying a WAR file to tomcat

      If you want to deploy your web app to a local Tomcat instance then you will need to generate a WAR file. However, you should note that when creating a war file, if you use the implementation dependency type then your Couchbase Lite jar files will be bundled into WEB-INF/lib of the web application. To exclude Couchbase Lite jar files from getting bundled and to use Couchbase Lite in multiple web applications, change the dependency type from implementation to providedCompile

      1. You can do this using the Gradle command below from within your project folder:

        ./gradlew war
        The generated war file will be at <PROJECT ROOT>/build/libs.
      2. Deploy the war file to Tomcat, by copying it to $CATALINA_BASE/webapps

        You can also use Tomcat’s Manager App to deploy the war file — see tomcat’s Manager App documentation for more detail.
      3. To use common class loader approach to load Couchbase Lite libraries, copy all of the Couchbase Lite jar files in $CATALINA_BASE/lib.

        For linux platform see also — Additional Steps for Linux

      RELEASE NOTES

      2.7.1

      Fixed at this Release

      This maintenance release fixes the following issues:

      • CBL-799 Crash calling pull validator callback

      • CBL-789 Crash when accessing connection→name()

      • CBL-701 Pending Document IDs not working correctly

      • CBL-698 Wrong query evaluation using expression values instead of expression properties

      • CBL-657 LIKE and CONTAINS are much slower in 2.7

      • CBL-581 Native memory leak when save document repeatedly​

      • CBL-579 SDK fails for opening Database files over 2GB

      Known issues

      The following issues document known errors:

      • CBL-637 Java Console app doesn’t exit

      • CBL-370 370 Broken API, Kotlin: Unable to import ReplicatorType

      • CBL-216 Ordering null values inconsistent with N1QL expectations

      • CBL-95 Pending conflicts could be resolved by a wrong replicator

      • CBL-49 Need a way to distinguish boolean types

      2.7.0

      New Features

      New at this release is the Java Platform, which enables development of Java apps on any platform that supports the JVM model

      Known issues

      None at this time

      Fixed at this Release

      None at this time


      1. from version 2.0