Java tutorial
/** * Copyright (c) 2016 Couchbase, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ package com.couchbase.lite; import com.couchbase.lite.auth.Authorizer; import com.couchbase.lite.auth.FacebookAuthorizer; import com.couchbase.lite.auth.PersonaAuthorizer; import com.couchbase.lite.internal.InterfaceAudience; import com.couchbase.lite.replicator.Replication; import com.couchbase.lite.support.FileDirUtils; import com.couchbase.lite.support.HttpClientFactory; import com.couchbase.lite.support.Version; import com.couchbase.lite.util.Log; import com.couchbase.lite.util.StreamUtils; import com.couchbase.lite.util.Utils; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Top-level CouchbaseLite object; manages a collection of databases as a CouchDB server does. */ public final class Manager { public static final String PRODUCT_NAME = "CouchbaseLite"; protected static final String kV1DBExtension = ".cblite"; // Couchbase Lite 1.0 protected static final String kDBExtension = ".cblite2"; // Couchbase Lite 1.2 or later (for iOS 1.1 or later) public static final ManagerOptions DEFAULT_OPTIONS = new ManagerOptions(); public static final String LEGAL_CHARACTERS = "[^a-z]{1,}[^a-z0-9_$()/+-]*$"; public static String USER_AGENT = null; public static final String SQLITE_STORAGE = "SQLite"; public static final String FORESTDB_STORAGE = "ForestDB"; // NOTE: Jackson is thread-safe http://wiki.fasterxml.com/JacksonFAQThreadSafety private static final ObjectMapper mapper = new ObjectMapper(); private ManagerOptions options; private File directoryFile; private Map<String, Database> databases; private Map<String, Object> encryptionKeys; private List<Replication> replications; private ScheduledExecutorService workExecutor; private HttpClientFactory defaultHttpClientFactory; private Context context; private String storageType; /////////////////////////////////////////////////////////////////////////// // APIs // https://github.com/couchbaselabs/couchbase-lite-api/blob/master/gen/md/Database.md /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Constructors /////////////////////////////////////////////////////////////////////////// /** * Constructor * * @throws UnsupportedOperationException - not currently supported * @exclude */ @InterfaceAudience.Public public Manager() { final String detailMessage = "Parameterless constructor is not a valid API call on Android. " + " Pure java version coming soon."; throw new UnsupportedOperationException(detailMessage); } /** * Constructor * * @throws java.lang.SecurityException - Runtime exception that can be thrown by File.mkdirs() */ @InterfaceAudience.Public public Manager(Context context, ManagerOptions options) throws IOException { Log.d(Database.TAG, "Starting Manager version: %s", Manager.VERSION); this.context = context; this.directoryFile = context.getFilesDir(); this.options = (options != null) ? options : DEFAULT_OPTIONS; this.databases = new HashMap<String, Database>(); this.encryptionKeys = new HashMap<String, Object>(); this.replications = new ArrayList<Replication>(); if (!directoryFile.exists()) { directoryFile.mkdirs(); } if (!directoryFile.isDirectory()) { throw new IOException( String.format(Locale.ENGLISH, "Unable to create directory for: %s", directoryFile)); } upgradeOldDatabaseFiles(directoryFile); // this must be a single threaded executor due to contract w/ Replication object // which must run on either: // - a shared single threaded executor // - its own single threaded executor workExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "CBLManagerWorkExecutor"); } }); } /////////////////////////////////////////////////////////////////////////// // Constants /////////////////////////////////////////////////////////////////////////// public static final String VERSION = Version.VERSION; /////////////////////////////////////////////////////////////////////////// // Class Members - Properties /////////////////////////////////////////////////////////////////////////// /** * Get shared instance * * @throws UnsupportedOperationException - not currently supported * @exclude */ @InterfaceAudience.Public public static Manager getSharedInstance() { final String detailMessage = "getSharedInstance() is not a valid API call on Android. " + " Pure java version coming soon"; throw new UnsupportedOperationException(detailMessage); } /** * Get the default storage type. * @return Storage type. */ @InterfaceAudience.Public public String getStorageType() { return storageType; } /** * Set default storage engine type for newly-created databases. * There are two options, "SQLite" (the default) or "ForestDB". * @param storageType */ @InterfaceAudience.Public public void setStorageType(String storageType) { this.storageType = storageType; } /////////////////////////////////////////////////////////////////////////// // Class Members - Methods /////////////////////////////////////////////////////////////////////////// /** * Enable logging for a particular tag / loglevel combo * * @param tag Used to identify the source of a log message. It usually identifies * the class or activity where the log call occurs. * @param logLevel The loglevel to enable. Anything matching this loglevel * or having a more urgent loglevel will be emitted. Eg, Log.VERBOSE. */ public static void enableLogging(String tag, int logLevel) { Log.enableLogging(tag, logLevel); } /** * Returns YES if the given name is a valid database name. * (Only the characters in "abcdefghijklmnopqrstuvwxyz0123456789_$()+-/" are allowed.) */ @InterfaceAudience.Public public static boolean isValidDatabaseName(String databaseName) { if (databaseName.length() > 0 && databaseName.length() < 240 && containsOnlyLegalCharacters(databaseName) && Character.isLowerCase(databaseName.charAt(0))) { return true; } return databaseName.equals(Replication.REPLICATOR_DATABASE_NAME); } /////////////////////////////////////////////////////////////////////////// // Instance Members - Properties /////////////////////////////////////////////////////////////////////////// /** * An array of the names of all existing databases. */ @InterfaceAudience.Public public List<String> getAllDatabaseNames() { String[] databaseFiles = directoryFile.list(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { if (filename.endsWith(Manager.kDBExtension)) { return true; } return false; } }); List<String> result = new ArrayList<String>(); for (String databaseFile : databaseFiles) { String trimmed = databaseFile.substring(0, databaseFile.length() - Manager.kDBExtension.length()); String replaced = trimmed.replace(':', '/'); result.add(replaced); } Collections.sort(result); return Collections.unmodifiableList(result); } /** * The root directory of this manager (as specified at initialization time.) */ @InterfaceAudience.Public public File getDirectory() { return directoryFile; } /////////////////////////////////////////////////////////////////////////// // Instance Members - Methods /////////////////////////////////////////////////////////////////////////// /** * Releases all resources used by the Manager instance and closes all its databases. */ @InterfaceAudience.Public public void close() { Log.d(Database.TAG, "Closing " + this); // Close all database: // Snapshot of the current open database to avoid concurrent modification as // the database will be forgotten (removed from the databases map) when it is closed: Database[] openDbs = databases.values().toArray(new Database[databases.size()]); for (Database database : openDbs) { database.close(); } databases.clear(); // Stop reachability: context.getNetworkReachabilityManager().stopListening(); // Shutdown ScheduledExecutorService: if (workExecutor != null && !workExecutor.isShutdown()) { Utils.shutdownAndAwaitTermination(workExecutor); } Log.d(Database.TAG, "Closed " + this); } /** * <p> * Returns the database with the given name, or creates it if it doesn't exist. * Multiple calls with the same name will return the same {@link Database} instance. * <p/> * <p> * This is equivalent to calling {@link #openDatabase(String, DatabaseOptions)} * with a default set of options with the `Create` flag set. * </p> * <p> * NOTE: Database names may not contain capital letters. * </p> */ @InterfaceAudience.Public public Database getDatabase(String name) throws CouchbaseLiteException { DatabaseOptions options = getDefaultOptions(name); options.setCreate(true); return openDatabase(name, options); } /** * <p> * Returns the database with the given name, or null if it doesn't exist. * Multiple calls with the same name will return the same {@link Database} instance. * <p/> * <p> * This is equivalent to calling {@link #openDatabase(String, DatabaseOptions)} * with a default set of options. * </p> */ @InterfaceAudience.Public public Database getExistingDatabase(String name) throws CouchbaseLiteException { DatabaseOptions options = getDefaultOptions(name); return openDatabase(name, options); } /** * Returns the database with the given name. If the database is not yet open, the options given * will be applied; if it's already open, the options are ignored. * Multiple calls with the same name will return the same {@link Database} instance. * @param name The name of the database. May NOT contain capital letters! * @param options Options to use when opening, such as the encryption key; if null, a default * set of options will be used. * @return The database instance. * @throws CouchbaseLiteException thrown when there is an error. */ @InterfaceAudience.Public public Database openDatabase(String name, DatabaseOptions options) throws CouchbaseLiteException { if (options == null) options = getDefaultOptions(name); Database db = getDatabase(name, !options.isCreate()); if (db != null && !db.isOpen()) { db.open(options); registerEncryptionKey(options.getEncryptionKey(), name); } return db; } /** * This method has been superseded by {@link #openDatabase(String, DatabaseOptions)}. * * Registers an encryption key for a database. This must be called _before_ opening an encrypted * database, or before creating a database that's to be encrypted. * If the key is incorrect (or no key is given for an encrypted database), the subsequent call * to open the database will fail with an error with code 401. * To use this API, the database storage engine must support encryption, and the * ManagerOptions.EnableStorageEncryption property must be set to true. Otherwise opening * the database will fail with an error. * @param keyOrPassword The encryption key in the form of an String (a password) or an * byte[] object exactly 32 bytes in length (a raw AES key.) * If a string is given, it will be internally converted to a raw key * using 64,000 rounds of PBKDF2 hashing. * A null value is legal, and clears a previously-registered key. * @param databaseName The name of the database. * @return True if the key can be used, False if it's not in a legal form * (e.g. key as a byte[] is not 32 bytes in length.) */ @InterfaceAudience.Public public boolean registerEncryptionKey(Object keyOrPassword, String databaseName) { if (databaseName == null) return false; if (keyOrPassword != null) { encryptionKeys.put(databaseName, keyOrPassword); } else encryptionKeys.remove(databaseName); return true; } /** * Replaces or installs a database from a file. * <p/> * This is primarily used to install a canned database * on first launch of an app, in which case you should first check .exists to avoid replacing the * database if it exists already. The canned database would have been copied into your app bundle * at build time. This property is deprecated for the new .cblite2 database file. If the database * file is a directory and has the .cblite2 extension, * use -replaceDatabaseNamed:withDatabaseDir:error: instead. * * @param databaseName The name of the target Database to replace or create. * @param databaseStream InputStream on the source Database file. * @param attachmentStreams Map of the associated source Attachments, or null if there are no attachments. * The Map key is the name of the attachment, the map value is an InputStream for * the attachment contents. If you wish to control the order that the attachments * will be processed, use a LinkedHashMap, SortedMap or similar and the iteration order * will be honoured. */ @InterfaceAudience.Public public void replaceDatabase(String databaseName, InputStream databaseStream, Map<String, InputStream> attachmentStreams) throws CouchbaseLiteException { replaceDatabase(databaseName, databaseStream, attachmentStreams == null ? null : attachmentStreams.entrySet().iterator()); } /** * Replaces or installs a database from a file. * * This is primarily used to install a canned database * on first launch of an app, in which case you should first check .exists to avoid replacing the * database if it exists already. The canned database would have been copied into your app bundle * at build time. If the database file is not a directory and has the .cblite extension, * use -replaceDatabaseNamed:withDatabaseFile:withAttachments:error: instead. * * @param databaseName The name of the database to replace. * @param databaseDir Path of the database directory that should replace it. * @return YES if the database was copied, NO if an error occurred. */ @InterfaceAudience.Public public boolean replaceDatabase(String databaseName, String databaseDir) { Database db = getDatabase(databaseName, false); if (db == null) return false; File dir = new File(databaseDir); if (!dir.exists()) { Log.w(Database.TAG, "Database file doesn't exist at path : %s", databaseDir); return false; } if (!dir.isDirectory()) { Log.w(Database.TAG, "Database file is not a directory. " + "Use -replaceDatabaseNamed:withDatabaseFilewithAttachments:error: instead."); return false; } File destDir = new File(db.getPath()); File srcDir = new File(databaseDir); if (destDir.exists()) { if (!FileDirUtils.deleteRecursive(destDir)) { Log.w(Database.TAG, "Failed to delete file/directly: " + destDir); return false; } } try { FileDirUtils.copyFolder(srcDir, destDir); } catch (IOException e) { Log.w(Database.TAG, "Failed to copy directly from " + srcDir + " to " + destDir, e); return false; } try { db.open(); } catch (CouchbaseLiteException e) { Log.w(Database.TAG, "Failed to open database", e); return false; } /* TODO: Currently Java implementation is different from iOS, needs to catch up. if(!db.saveLocalUUIDInLocalCheckpointDocument()){ Log.w(Database.TAG, "Failed to replace UUIDs"); return false; } */ if (!db.replaceUUIDs()) { Log.w(Database.TAG, "Failed to replace UUIDs"); db.close(); return false; } // close so app can (re)open db with its preferred options: db.close(); return true; } /////////////////////////////////////////////////////////////////////////// // End of APIs /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Public but Not API /////////////////////////////////////////////////////////////////////////// @InterfaceAudience.Private DatabaseOptions getDefaultOptions(String databaseName) { DatabaseOptions options = new DatabaseOptions(); options.setEncryptionKey(encryptionKeys.get(databaseName)); return options; } /** * @exclude */ @InterfaceAudience.Private public static ObjectMapper getObjectMapper() { return mapper; } /** * @exclude */ @InterfaceAudience.Private public HttpClientFactory getDefaultHttpClientFactory() { return defaultHttpClientFactory; } /** * @exclude */ @InterfaceAudience.Private public void setDefaultHttpClientFactory(HttpClientFactory defaultHttpClientFactory) { this.defaultHttpClientFactory = defaultHttpClientFactory; } /** * @exclude */ @InterfaceAudience.Private public Collection<Database> allOpenDatabases() { return databases.values(); } /** * @exclude */ @InterfaceAudience.Private public Map<String, Object> getEncryptionKeys() { return Collections.unmodifiableMap(encryptionKeys); } /** * Asynchronously dispatches a callback to run on a background thread. The callback will be passed * Database instance. There is not currently a known reason to use it, it may not make * sense on the Android API, but it was added for the purpose of having a consistent API with iOS. * * @exclude */ @InterfaceAudience.Private public Future runAsync(String databaseName, final AsyncTask function) throws CouchbaseLiteException { final Database database = getDatabase(databaseName); return runAsync(new Runnable() { @Override public void run() { function.run(database); } }); } /** * Instantiates a database but doesn't open the file yet. * in CBLManager.m * - (CBLDatabase*) _databaseNamed: (NSString*)name * mustExist: (BOOL)mustExist * error: (NSError**)outError * * @exclude */ @InterfaceAudience.Private public synchronized Database getDatabase(String name, boolean mustExist) { if (options.isReadOnly()) mustExist = true; Database db = databases.get(name); if (db == null) { if (!isValidDatabaseName(name)) throw new IllegalArgumentException("Invalid database name: " + name); String path = pathForDatabaseNamed(name); if (path == null) return null; db = new Database(path, name, this, options.isReadOnly()); if (mustExist && !db.exists()) { Log.i(Database.TAG, "mustExist is true and db (%s) does not exist", name); return null; } db.setName(name); databases.put(name, db); } return db; } /** * @exclude */ @InterfaceAudience.Private public Replication getReplicator(Map<String, Object> properties) throws CouchbaseLiteException { // TODO: in the iOS equivalent of this code, there is: {@"doc_ids", _documentIDs}) - write unit test that detects this bug // TODO: ditto for "headers" Authorizer authorizer = null; Replication repl = null; URL remote = null; Map<String, Object> remoteMap; Map<String, Object> sourceMap = parseSourceOrTarget(properties, "source"); Map<String, Object> targetMap = parseSourceOrTarget(properties, "target"); String source = (String) sourceMap.get("url"); String target = (String) targetMap.get("url"); Boolean createTargetBoolean = (Boolean) properties.get("create_target"); boolean createTarget = (createTargetBoolean != null && createTargetBoolean.booleanValue()); Boolean continuousBoolean = (Boolean) properties.get("continuous"); boolean continuous = (continuousBoolean != null && continuousBoolean.booleanValue()); Boolean cancelBoolean = (Boolean) properties.get("cancel"); boolean cancel = (cancelBoolean != null && cancelBoolean.booleanValue()); // Map the 'source' and 'target' JSON params to a local database and remote URL: if (source == null || target == null) { throw new CouchbaseLiteException("source and target are both null", new Status(Status.BAD_REQUEST)); } boolean push = false; Database db = null; String remoteStr = null; if (Manager.isValidDatabaseName(source)) { db = getExistingDatabase(source); remoteStr = target; push = true; remoteMap = targetMap; } else { remoteStr = source; if (createTarget && !cancel) { boolean mustExist = false; db = getDatabase(target, mustExist); db.open(); } else { db = getExistingDatabase(target); } if (db == null) { throw new CouchbaseLiteException("database is null", new Status(Status.NOT_FOUND)); } remoteMap = sourceMap; } // Can't specify both a filter and doc IDs if (properties.get("filter") != null && properties.get("doc_ids") != null) throw new CouchbaseLiteException("Can't specify both a filter and doc IDs", new Status(Status.BAD_REQUEST)); try { remote = new URL(remoteStr); } catch (MalformedURLException e) { throw new CouchbaseLiteException("malformed remote url: " + remoteStr, new Status(Status.BAD_REQUEST)); } if (remote == null) { throw new CouchbaseLiteException("remote URL is null: " + remoteStr, new Status(Status.BAD_REQUEST)); } Map<String, Object> authMap = (Map<String, Object>) remoteMap.get("auth"); if (authMap != null) { Map<String, Object> persona = (Map<String, Object>) authMap.get("persona"); if (persona != null) { String email = (String) persona.get("email"); authorizer = new PersonaAuthorizer(email); } Map<String, Object> facebook = (Map<String, Object>) authMap.get("facebook"); if (facebook != null) { String email = (String) facebook.get("email"); authorizer = new FacebookAuthorizer(email); } authorizer.setRemoteURL(remote); authorizer.setLocalUUID(db.publicUUID()); } if (!cancel) { repl = db.getReplicator(remote, getDefaultHttpClientFactory(), push, continuous); if (repl == null) { throw new CouchbaseLiteException("unable to create replicator with remote: " + remote, new Status(Status.INTERNAL_SERVER_ERROR)); } if (authorizer != null) { repl.setAuthenticator(authorizer); } Map<String, Object> headers = null; if (remoteMap != null) { headers = (Map) remoteMap.get("headers"); } if (headers != null && !headers.isEmpty()) { repl.setHeaders(headers); } String filterName = (String) properties.get("filter"); if (filterName != null) { repl.setFilter(filterName); Map<String, Object> filterParams = (Map<String, Object>) properties.get("query_params"); if (filterParams != null) { repl.setFilterParams(filterParams); } } // docIDs if (properties.get("doc_ids") != null) { if (properties.get("doc_ids") instanceof List) { List<String> docIds = (List<String>) properties.get("doc_ids"); repl.setDocIds(docIds); } } String remoteUUID = (String) properties.get("remoteUUID"); if (remoteUUID != null) { repl.setRemoteUUID(remoteUUID); } if (push) { repl.setCreateTarget(createTarget); } } else { // Cancel replication: repl = db.getActiveReplicator(remote, push); if (repl == null) { throw new CouchbaseLiteException("unable to lookup replicator with remote: " + remote, new Status(Status.NOT_FOUND)); } } return repl; } /** * @exclude */ @InterfaceAudience.Private public ScheduledExecutorService getWorkExecutor() { return workExecutor; } /** * @exclude */ @InterfaceAudience.Private public Context getContext() { return context; } /** * @exclude */ @InterfaceAudience.Private public int getExecutorThreadPoolSize() { return this.options.getExecutorThreadPoolSize(); } /////////////////////////////////////////////////////////////////////////// // Internal (protected or private) Methods /////////////////////////////////////////////////////////////////////////// @InterfaceAudience.Private private void replaceDatabase(String databaseName, InputStream databaseStream, Iterator<Map.Entry<String, InputStream>> attachmentStreams) throws CouchbaseLiteException { try { Database db = getDatabase(databaseName, false); String dstDbPath = FileDirUtils.getPathWithoutExt(db.getPath()) + kV1DBExtension; String dstAttsPath = FileDirUtils.getPathWithoutExt(dstDbPath) + " attachments"; OutputStream destStream = new FileOutputStream(new File(dstDbPath)); StreamUtils.copyStream(databaseStream, destStream); File attachmentsFile = new File(dstAttsPath); FileDirUtils.deleteRecursive(attachmentsFile); if (!attachmentsFile.exists()) { attachmentsFile.mkdirs(); } if (attachmentStreams != null) { StreamUtils.copyStreamsToFolder(attachmentStreams, attachmentsFile); } if (!upgradeV1Database(databaseName, dstDbPath)) { throw new CouchbaseLiteException(Status.INTERNAL_SERVER_ERROR); } db.open(); db.replaceUUIDs(); } catch (FileNotFoundException e) { Log.e(Database.TAG, "Error replacing the database: %s", e, databaseName); throw new CouchbaseLiteException(Status.INTERNAL_SERVER_ERROR); } catch (IOException e) { Log.e(Database.TAG, "Error replacing the database: %s", e, databaseName); throw new CouchbaseLiteException(Status.INTERNAL_SERVER_ERROR); } } /** * @exclude */ @InterfaceAudience.Private private static boolean containsOnlyLegalCharacters(String databaseName) { Pattern p = Pattern.compile("^[abcdefghijklmnopqrstuvwxyz0123456789_$()+-/]+$"); Matcher matcher = p.matcher(databaseName); return matcher.matches(); } /** * Scan my dir for SQLite-based databases from Couchbase Lite 1.0 and upgrade them: * <p/> * in CBLManager.m * - (void) upgradeOldDatabaseFiles * * @exclude */ @InterfaceAudience.Private private void upgradeOldDatabaseFiles(File directory) { File[] files = directory.listFiles(new FilenameFilter() { @Override public boolean accept(File file, String name) { return name.endsWith(kV1DBExtension); } }); for (File file : files) { String filename = file.getName(); String name = nameOfDatabaseAtPath(filename); String oldDbPath = new File(directory, filename).getAbsolutePath(); upgradeDatabase(name, oldDbPath, true); } } /** * in CBLManager.m * - (BOOL) upgradeDatabaseNamed: (NSString*)name * atPath: (NSString*)dbPath * error: (NSError**)outError */ private boolean upgradeDatabase(String name, String dbPath, boolean close) { Log.v(Log.TAG_DATABASE, "CouchbaseLite: Upgrading database at %s ...", dbPath); if (!name.equals("_replicator")) { // Create and open new CBLDatabase: Database db = getDatabase(name, false); if (db == null) { Log.w(Log.TAG_DATABASE, "Upgrade failed: Creating new db failed"); return false; } if (!db.exists()) { // Upgrade the old database into the new one: DatabaseUpgrade upgrader = new DatabaseUpgrade(this, db, dbPath); if (!upgrader.importData()) { upgrader.backOut(); return false; } } if (close) db.close(); } // Remove old database file and its SQLite side files: moveSQLiteDbFiles(dbPath, null); if (dbPath.endsWith(kV1DBExtension)) { String oldAttachmentsName = FileDirUtils.getDatabaseNameFromPath(dbPath) + " attachments"; File oldAttachmentsDir = new File(directoryFile, oldAttachmentsName); if (oldAttachmentsDir.exists()) FileDirUtils.deleteRecursive(oldAttachmentsDir); } Log.v(Log.TAG_DATABASE, " ...success!"); return true; } private boolean upgradeV1Database(String name, String dbPath) { if (dbPath.endsWith(kV1DBExtension)) { return upgradeDatabase(name, dbPath, false); } else { // Gracefully skipping the upgrade: Log.w(Log.TAG_DATABASE, "Upgrade skipped: Database file extension is not %s", kDBExtension); return true; } } private static void moveSQLiteDbFiles(String oldDbPath, String newDbPath) { for (String suffix : Arrays.asList("", "-wal", "-shm", "-journal")) { File oldFile = new File(oldDbPath + suffix); if (!oldFile.exists()) continue; if (newDbPath != null) oldFile.renameTo(new File(newDbPath + suffix)); else oldFile.delete(); } } /** * @exclude */ @InterfaceAudience.Private protected Future runAsync(Runnable runnable) { synchronized (workExecutor) { if (!workExecutor.isShutdown()) { return workExecutor.submit(runnable); } else { return null; } } } /** * in CBLManager.m * - (NSString*) pathForDatabaseNamed: (NSString*)name * * @exclude */ @InterfaceAudience.Private private static String nameOfDatabaseAtPath(String path) { String name = FileDirUtils.getDatabaseNameFromPath(path); return isWindows() ? name.replace('/', '.') : name.replace('/', ':'); } /** * in CBLManager.m * - (NSString*) pathForDatabaseNamed: (NSString*)name * * @exclude */ @InterfaceAudience.Private private String pathForDatabaseNamed(String name) { if ((name == null) || (name.length() == 0) || Pattern.matches(LEGAL_CHARACTERS, name)) return null; // NOTE: CouchDB allows forward slash as part of database name. // However, ':' is illegal character on Windows platform. // For Windows, substitute with period '.' name = isWindows() ? name.replace('/', '.') : name.replace('/', ':'); String result = directoryFile.getPath() + File.separator + name + Manager.kDBExtension; return result; } /** * @exclude */ @InterfaceAudience.Private private static Map<String, Object> parseSourceOrTarget(Map<String, Object> properties, String key) { Map<String, Object> result = new HashMap<String, Object>(); Object value = properties.get(key); if (value instanceof String) { result.put("url", value); } else if (value instanceof Map) { result = (Map<String, Object>) value; } return result; } /** * @exclude */ @InterfaceAudience.Private protected void forgetDatabase(Database db) { // remove from cached list of dbs databases.remove(db.getName()); // remove from list of replications // TODO: should there be something that actually stops the replication(s) first? Iterator<Replication> replicationIterator = this.replications.iterator(); while (replicationIterator.hasNext()) { Replication replication = replicationIterator.next(); if (replication.getLocalDatabase().getName().equals(db.getName())) { replicationIterator.remove(); } } // Remove registered encryption key if available: encryptionKeys.remove(db.getName()); } /** * @exclude */ @InterfaceAudience.Private protected boolean isAutoMigrateBlobStoreFilename() { return this.options.isAutoMigrateBlobStoreFilename(); } private static String OS = System.getProperty("os.name").toLowerCase(); /** * Check if platform is Windows */ @InterfaceAudience.Private private static boolean isWindows() { return (OS.indexOf("win") >= 0); } /** * Return User-Agent value * Format: ex: CouchbaseLite/1.2 (Java Linux/MIPS 1.2.1/3382EFA) */ public static String getUserAgent() { if (USER_AGENT == null) { USER_AGENT = String.format(Locale.ENGLISH, "%s/%s (%s/%s)", PRODUCT_NAME, Version.SYNC_PROTOCOL_VERSION, Version.getVersionName(), Version.getCommitHash()); } return USER_AGENT; } }