com.couchbase.lite.Manager Maven / Gradle / Ivy
/**
*
* Copyright (c) 2012 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.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 databases;
private Map encryptionKeys;
private List 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();
this.encryptionKeys = new HashMap();
this.replications = new ArrayList();
if (!directoryFile.exists()) {
directoryFile.mkdirs();
}
if (!directoryFile.isDirectory()) {
throw new IOException(String.format("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 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 result = new ArrayList();
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);
}
/**
*
* 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.
*
*
* This is equivalent to calling {@link #openDatabase(String, DatabaseOptions)}
* with a default set of options with the `Create` flag set.
*
*
* NOTE: Database names may not contain capital letters.
*
*/
@InterfaceAudience.Public
public Database getDatabase(String name) throws CouchbaseLiteException {
DatabaseOptions options = getDefaultOptions(name);
options.setCreate(true);
return openDatabase(name, options);
}
/**
*
* 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.
*
*
* This is equivalent to calling {@link #openDatabase(String, DatabaseOptions)}
* with a default set of options.
*
*/
@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.
*
* 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 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 allOpenDatabases() {
return databases.values();
}
/**
* @exclude
*/
@InterfaceAudience.Private
public Map 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.w(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 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 remoteMap;
Map sourceMap = parseSourceOrTarget(properties, "source");
Map 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;
}
Map authMap = (Map) remoteMap.get("auth");
if (authMap != null) {
Map persona = (Map) authMap.get("persona");
if (persona != null) {
String email = (String) persona.get("email");
authorizer = new PersonaAuthorizer(email);
}
Map facebook = (Map) authMap.get("facebook");
if (facebook != null) {
String email = (String) facebook.get("email");
authorizer = new FacebookAuthorizer(email);
}
}
// 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));
}
if (!cancel) {
repl = db.getReplicator(remote, getDefaultHttpClientFactory(), push, continuous, getWorkExecutor());
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 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 filterParams = (Map) 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 docIds = (List)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> 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:
*
* 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 parseSourceOrTarget(Map properties, String key) {
Map result = new HashMap();
Object value = properties.get(key);
if (value instanceof String) {
result.put("url", value);
} else if (value instanceof Map) {
result = (Map) 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 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("%s/%s (%s/%s)",
PRODUCT_NAME,
Version.SYNC_PROTOCOL_VERSION,
Version.getVersionName(),
Version.getCommitHash());
}
return USER_AGENT;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy