/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.io.dropbox.internal;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dropbox.core.DbxAppInfo;
import com.dropbox.core.DbxClient;
import com.dropbox.core.DbxDelta;
import com.dropbox.core.DbxDelta.Entry;
import com.dropbox.core.DbxEntry;
import com.dropbox.core.DbxEntry.WithChildren;
import com.dropbox.core.DbxException;
import com.dropbox.core.DbxRequestConfig;
import com.dropbox.core.DbxWebAuthNoRedirect;
import com.dropbox.core.DbxWriteMode;
/**
* The {@link DropboxSynchronizer} is able to synchronize contents of your Dropbox
* to the local file system and vice versa. There three synchronization modes
* available: local to dropbox (which is the default mode), dropbox to local and
* bidirectional.
*
* Note: The {@link DropboxSynchronizer} must be authorized against Dropbox one
* time. Watch the logfile for the URL to open in your Browser and allow openHAB
* to connect to a predefined App-Folder (see <a
* href="https://www.dropbox.com/developers/apps">Dropbox Documentation</a> for more information).
*
* @author Thomas.Eichstaedt-Engelen
* @since 1.0.0
*/
public class DropboxSynchronizer implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(DropboxSynchronizer.class);
private static final String DROPBOX_SCHEDULER_GROUP = "Dropbox";
private static final String FIELD_DELIMITER = "@@";
private static final String LINE_DELIMITER = System.getProperty("line.separator");
private static final String DELTA_CURSOR_FILE_NAME = File.separator + "deltacursor.dbx";
private static final String DROPBOX_ENTRIES_FILE_NAME = File.separator + "dropbox-entries.dbx";
private static final String AUTH_FILE_NAME = File.separator + "authfile.dbx";
/** holds the id of the last synchronisation cursor. This is needed to define the delta to download from Dropbox. */
private static String lastCursor = null;
private static String lastHash = null;
/** the configured AppKey (optional, defaults to the official Dropbox-App key 'gbrwwfzvrw6a9uv') */
private static String appKey = "gbrwwfzvrw6a9uv";
/** the configured AppSecret (optional, defaults to official Dropbox-App secret 'gu5v7lp1f5bbs07') */
private static String appSecret = "gu5v7lp1f5bbs07";
/** The default directory to download files from Dropbox to (currently '.') */
private static final String DEFAULT_CONTENT_DIR = ".";
/** the base directory to synchronize with openHAB, configure 'filter' to select files (defaults to DEFAULT_CONTENT_DIR) */
private static String contentDir = DEFAULT_CONTENT_DIR;
/** the configured synchronization mode (defaults to LOCAL_TO_DROPBOX) */
private static DropboxSyncMode syncMode = DropboxSyncMode.LOCAL_TO_DROPBOX;
/** the upload interval as Cron-Expression (optional, defaults to '0 0 2 * * ?' which means once a day at 2am) */
private static String uploadInterval = "0 0 2 * * ?";
/** the download interval as Cron-Expression (optional, defaults to '0 0/5 * * * ?' which means every 5 minutes) */
private static String downloadInterval = "0 0/5 * * * ?";
private static final List<String> DEFAULT_UPLOAD_FILE_FILTER = Arrays.asList("^([^/]*/){1}[^/]*$", "/configurations.*", "/logs/.*", "/etc/.*");
private static final List<String> DEFAULT_DOWNLOAD_FILE_FILTER = Arrays.asList("^([^/]*/){1}[^/]*$", "/configurations.*");
/** defines a comma separated list of regular expressions which matches the filenames to upload to Dropbox (optional, defaults to '/configurations.*, /logs/.*, /etc/.*') */
private static List<String> uploadFilterElements = DEFAULT_UPLOAD_FILE_FILTER;
/** defines a comma separated list of regular expressions which matches the filenames to download from Dropbox (optional, defaults to '/configurations.*') */
private static List<String> downloadFilterElements = DEFAULT_DOWNLOAD_FILE_FILTER;
/** operates the Synchronizer in fake mode which avoids up- or downloading files to and from Dropbox. This is meant as testMode for the filter settings (optional, defaults to false) */
private static boolean fakeMode = false;
private static boolean isProperlyConfigured = false;
private static DropboxSynchronizer instance = null;
private static final DbxAppInfo appInfo =
new DbxAppInfo(DropboxSynchronizer.appKey, DropboxSynchronizer.appSecret);
private final static DbxRequestConfig requestConfig =
new DbxRequestConfig("openHAB/1.0", Locale.getDefault().toString());
public void activate() {
DropboxSynchronizer.instance = this;
}
public void deactivate() {
logger.debug("about to shut down DropboxSynchronizer ...");
cancelAllJobs();
isProperlyConfigured = false;
lastCursor = null;
uploadFilterElements = DEFAULT_UPLOAD_FILE_FILTER;
downloadFilterElements = DEFAULT_DOWNLOAD_FILE_FILTER;
DropboxSynchronizer.instance = null;
}
private void activateSynchronizer() {
if (isAuthenticated()) {
startSynchronizationJobs();
} else {
try {
startAuthentication();
} catch (DbxException e) {
logger.warn("Couldn't start authentication process: {}", e.getMessage());;
}
}
}
/**
* Starts the OAuth authorization process with Dropbox. The authorization
* process is a multi step process which is described in the Wiki in detail.
*
* @throws DbxException if there are technical or application level errors
* in the Dropbox communication
*
* @see <a href="http://code.google.com/p/openhab/wiki/DropboxIOBundle">openHAB Dropbox Wiki</a>
*/
public void startAuthentication() throws DbxException {
DbxWebAuthNoRedirect webAuth = new DbxWebAuthNoRedirect(requestConfig, appInfo);
String authUrl = webAuth.start();
logger.info("#########################################################################################");
logger.info("# Dropbox-Integration: U S E R I N T E R A C T I O N R E Q U I R E D !!");
logger.info("# 1. Open URL '{}'", authUrl);
logger.info("# 2. Allow openHAB to access Dropbox");
logger.info("# 3. Paste the authorisation code here using the command 'finishAuthentication \"<token>\"'");
logger.info("#########################################################################################");
}
/**
* Finishes the OAuth authorization process by taking the given {@code token} and creating
* an accessToken out of it. The authorization process is a multi step process which is
* described in the Wiki in detail.
*
* @throws DbxException if there are technical or application level errors
* in the Dropbox communication
*
* @see <a href="http://code.google.com/p/openhab/wiki/DropboxIOBundle">openHAB Dropbox Wiki</a>
*/
public void finishAuthentication(String code) throws DbxException {
DbxWebAuthNoRedirect webAuth = new DbxWebAuthNoRedirect(requestConfig, appInfo);
String accessToken = webAuth.finish(code).accessToken;
writeAccessToken(accessToken);
logger.info("#########################################################################################");
logger.info("# OAuth2 authentication flow has been finished successfully ");
logger.info("#########################################################################################");
startSynchronizationJobs();
}
/**
* Synchronizes all changes from Dropbox to the local file system. Changes are
* identified by the Dropbox delta mechanism which takes the <code>lastCursor</code>
* field into account. If <code>lastCursor</code> is <code>null</code> it
* tries to recreate it from the file <code>deltacursor.dbx</code>. If
* it is still <code>null</code> all files are downloaded from the specified
* location.
*
* Note: Since we define Dropbox as data master we do not care about local
* changes while downloading files!
*
* @throws DbxException if there are technical or application level
* errors in the Dropbox communication
* @throws IOException
*/
public void syncDropboxToLocal(DbxClient client) throws DbxException, IOException {
logger.debug("Started synchronization from Dropbox to local ...");
lastCursor = readDeltaCursor();
if (StringUtils.isBlank(lastCursor)) {
logger.trace("Last cursor was NULL and has now been recreated from the filesystem '{}'", lastCursor);
}
DbxDelta<DbxEntry> deltaPage = client.getDelta(lastCursor);
if (deltaPage.entries != null && deltaPage.entries.size() == 0) {
logger.debug("There are no deltas to download from Dropbox ...");
} else {
do {
logger.debug("There are '{}' deltas to process ...", deltaPage.entries.size());
int processedDelta = 0;
for (Entry<DbxEntry> entry : deltaPage.entries) {
boolean matches = false;
for (String filter : downloadFilterElements) {
matches |= entry.lcPath.matches(filter);
}
if (matches) {
if (entry.metadata != null) {
downloadFile(client, entry);
} else {
String fqPath = contentDir + entry.lcPath;
deleteLocalFile(fqPath);
}
processedDelta++;
} else {
logger.trace("skipped file '{}' since it doesn't match the given filter arguments.", entry.lcPath);
}
}
logger.debug("'{}' deltas met the given downloadFilter {}", processedDelta, downloadFilterElements);
// query again to check if there more entries to process!
deltaPage = client.getDelta(lastCursor);
} while (deltaPage.hasMore);
}
writeDeltaCursor(deltaPage.cursor);
}
/**
* Synchronizes all changes from the local filesystem into Dropbox. Changes
* are identified by the files' <code>lastModified</code> attribut. If there
* are less files locally the additional files will be deleted from the
* Dropbox. New files will be uploaded or overwritten if they exist already.
*
* @throws DbxException if there are technical or application level
* errors in the Dropbox communication
* @throws IOException
*/
public void syncLocalToDropbox(DbxClient client) throws DbxException, IOException {
logger.debug("Started synchronization from local to Dropbox ...");
Map<String, Long> dropboxEntries = new HashMap<String, Long>();
WithChildren metadata = client.getMetadataWithChildren("/");
File dropboxEntryFile = new File(contentDir + DROPBOX_ENTRIES_FILE_NAME);
if (!dropboxEntryFile.exists() || !metadata.hash.equals(lastHash)) {
collectDropboxEntries(client, dropboxEntries, "/");
serializeDropboxEntries(dropboxEntryFile, dropboxEntries);
lastHash = metadata.hash;
// TODO: TEE: we could think about writing the 'lastHash' to a file?
// let's see what daily use brings whether this a necessary feature!
} else {
logger.trace("Dropbox entry file '{}' exists -> extract content", dropboxEntryFile.getPath());
dropboxEntries = extractDropboxEntries(dropboxEntryFile);
}
Map<String, Long> localEntries = new HashMap<String, Long>();
collectLocalEntries(localEntries, contentDir);
logger.debug("There are '{}' local entries that met the upload filters ...", localEntries.size());
boolean isChanged = false;
for (java.util.Map.Entry<String, Long> entry : localEntries.entrySet()) {
if (dropboxEntries.containsKey(entry.getKey())) {
if (entry.getValue().compareTo(dropboxEntries.get(entry.getKey())) > 0) {
logger.trace("Local file '{}' is newer - upload to Dropbox!", entry.getKey());
if (!fakeMode) {
uploadFile(client, entry.getKey(), true);
}
isChanged = true;
}
} else {
logger.trace("Local file '{}' doesn't exist in Dropbox - upload to Dropbox!", entry.getKey());
if (!fakeMode) {
uploadFile(client, entry.getKey(), false);
}
isChanged = true;
}
dropboxEntries.remove(entry.getKey());
}
// all left dropboxEntries are only present in Dropbox and not locally (anymore)
// so delete them from Dropbox!
for (String path : dropboxEntries.keySet()) {
for (String filter : uploadFilterElements) {
if (path.matches(filter)) {
if (!fakeMode) {
client.delete(path);
}
isChanged = true;
logger.debug("Successfully deleted file '{}' from Dropbox", path);
} else {
logger.trace("skipped file '{}' since it doesn't match the given filter arguments.", path);
}
}
}
// when something changed we will remove the entry file
// which causes a new generation during the next sync
if (isChanged) {
boolean success = FileUtils.deleteQuietly(dropboxEntryFile);
if (!success) {
logger.warn("Couldn't delete file '{}'", dropboxEntryFile.getPath());
} else {
logger.debug("Deleted cache file '{}' since there are changes. It will be recreated while next synchronization loop.", dropboxEntryFile.getPath());
}
// since there are changes we have to update the lastCursor (and
// the corresponding file) to have the right starting point for the
// next synchronization loop
DbxDelta<DbxEntry> delta = client.getDelta(lastCursor);
writeDeltaCursor(delta.cursor);
} else {
logger.debug("No files changed locally > no deltas to upload to Dropbox ...");
}
}
private void downloadFile(DbxClient client, Entry<DbxEntry> entry) throws DbxException, IOException {
String fqPath = contentDir + entry.metadata.path;
File newLocalFile = new File(fqPath);
if (entry.metadata.isFolder()) {
// create intermediary directories
boolean success = newLocalFile.mkdirs();
if (!success) {
logger.debug("Didn't create any intermediary directories for '{}'", fqPath);
}
} else {
// if the parent directory doesn't exist create all intermediary
// directorys ...
if (!newLocalFile.getParentFile().exists()) {
newLocalFile.getParentFile().mkdirs();
}
try {
FileOutputStream os = new FileOutputStream(newLocalFile);
if (!fakeMode) {
client.getFile(entry.metadata.path, null, os);
}
logger.debug("Successfully downloaded file '{}'", fqPath);
} catch (FileNotFoundException fnfe) {
throw new DbxException("Couldn't write file '" + fqPath + "'", fnfe);
}
long lastModified = entry.metadata.asFile().lastModified.getTime();
boolean success = newLocalFile.setLastModified(lastModified);
if (!success) {
logger.debug("Couldn't change attribute 'lastModified' of file '{}'", fqPath);
}
}
}
private Map<String, Long> extractDropboxEntries(File dropboxEntryFile) {
Map<String, Long> dropboxEntries = new HashMap<String, Long>();
try {
List<String> lines = FileUtils.readLines(dropboxEntryFile);
for (String line : lines) {
String[] lineComponents = line.split(FIELD_DELIMITER);
if (lineComponents.length == 2) {
dropboxEntries.put(lineComponents[0], Long.valueOf(lineComponents[1]));
} else {
logger.trace("Couldn't parse line '{}' - it does not contain to elements delimited by '{}'", line, FIELD_DELIMITER);
}
}
} catch (IOException ioe) {
logger.warn("Couldn't read lines from file '{}'", dropboxEntryFile.getPath());
}
return dropboxEntries;
}
private void serializeDropboxEntries(File file, Map<String, Long> dropboxEntries) {
try {
StringBuffer sb = new StringBuffer();
for (java.util.Map.Entry<String, Long> line : dropboxEntries.entrySet()) {
sb.append(line.getKey()).append(FIELD_DELIMITER).append(line.getValue()).append(LINE_DELIMITER);
}
FileUtils.writeStringToFile(file, sb.toString());
} catch (IOException e) {
logger.warn("Couldn't write file '{}'", file.getPath());
}
}
private void collectLocalEntries(Map<String, Long> localEntries, String path) {
File[] files = new File(path).listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
String normalizedPath = StringUtils.substringAfter(file.getPath(), contentDir);
for (String filter : uploadFilterElements) {
if (FilenameUtils.getName(normalizedPath).startsWith(".")) {
return false;
} else if (FilenameUtils.getName(normalizedPath).endsWith(".dbx")) {
return false;
} else if (normalizedPath.matches(filter)) {
return true;
}
}
logger.trace("skipped file '{}' since it doesn't match the given filter arguments.", file.getAbsolutePath());
return false;
}
});
for (File file : files) {
String normalizedPath = StringUtils.substringAfter(file.getPath(), contentDir);
if (file.isDirectory()) {
collectLocalEntries(localEntries, file.getPath());
} else {
localEntries.put(normalizedPath, file.lastModified());
}
}
}
private void collectDropboxEntries(DbxClient client, Map<String, Long> dropboxEntries, String path) throws DbxException {
WithChildren entries = client.getMetadataWithChildren(path);
for (DbxEntry entry : entries.children) {
if (entry.isFolder()) {
collectDropboxEntries(client, dropboxEntries, entry.path);
} else {
dropboxEntries.put(entry.path, entry.asFile().lastModified.getTime());
}
}
}
/*
* TODO: TEE: Currently there is now way to change the attribute
* 'lastModified' of the files to upload via Dropbox API. See the
* discussion below for more details.
*
* Since this is a missing feature (from my point of view) we should
* check the improvements of the API development on regular basis.
*
* @see http://forums.dropbox.com/topic.php?id=22347
*/
private void uploadFile(DbxClient client, String dropboxPath, boolean overwrite) throws DbxException, IOException {
File file = new File(contentDir + File.separator + dropboxPath);
FileInputStream inputStream = new FileInputStream(file);
try {
DbxWriteMode mode = overwrite ? DbxWriteMode.force() : DbxWriteMode.add();
DbxEntry.File uploadedFile =
client.uploadFile(dropboxPath, mode, file.length(), inputStream);
logger.debug("successfully uploaded file '{}'. New revision is '{}'", uploadedFile.toString(), uploadedFile.rev);
} finally {
inputStream.close();
}
}
private void writeAccessToken(String content) {
File tokenFile = new File(contentDir + AUTH_FILE_NAME);
writeLocalFile(tokenFile, content);
}
private String readAccessToken() {
File tokenFile = new File(contentDir + AUTH_FILE_NAME);
return readFile(tokenFile);
}
private boolean isAuthenticated() {
return StringUtils.isNotBlank(readAccessToken());
}
private void writeDeltaCursor(String deltaCursor) {
if (!deltaCursor.equals(lastCursor)) {
logger.trace("Delta-Cursor changed (lastCursor '{}', newCursor '{}')", lastCursor, deltaCursor);
File cursorFile = new File(contentDir + DELTA_CURSOR_FILE_NAME);
writeLocalFile(cursorFile, deltaCursor);
lastCursor = deltaCursor;
}
}
private String readDeltaCursor() {
File cursorFile = new File(contentDir + DELTA_CURSOR_FILE_NAME);
return readFile(cursorFile);
}
private String readFile(File file) {
String content = null;
if (file.exists()) {
try {
List<String> lines = FileUtils.readLines(file);
if (lines.size() > 0) {
content = lines.get(0);
}
} catch (IOException ioe) {
logger.debug("Handling of cursor file throws an Exception", ioe);
}
}
return content;
}
private static void writeLocalFile(File file, String content) {
try {
FileUtils.writeStringToFile(file, content);
logger.debug("Created file '{}' with content '{}'", file.getAbsolutePath(), content);
} catch (IOException e) {
logger.error("Couldn't write to file '" + file.getPath() + "'.", e);
}
}
private static void deleteLocalFile(String fqPath) {
File fileToDelete = new File(fqPath);
if (!fileToDelete.isDirectory()) {
boolean success = true;
if (!fakeMode) {
FileUtils.deleteQuietly(fileToDelete);
}
if (success) {
logger.debug("Successfully deleted local file '{}'", fqPath);
} else {
logger.debug("Local file '{}' couldn't be deleted", fqPath);
}
} else {
logger.trace("Local file '{}' isn't deleted because it is a directory");
}
}
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary config) throws ConfigurationException {
if (config != null) {
isProperlyConfigured = false;
String appKeyString = (String) config.get("appkey");
if (isNotBlank(appKeyString)) {
DropboxSynchronizer.appKey = appKeyString;
}
String appSecretString = (String) config.get("appsecret");
if (isNotBlank(appSecretString)) {
DropboxSynchronizer.appSecret = appSecretString;
}
if (isBlank(DropboxSynchronizer.appKey) || isBlank(DropboxSynchronizer.appSecret)) {
throw new ConfigurationException("dropbox:appkey",
"The parameters 'appkey' or 'appsecret' are missing! Please refer to your 'openhab.cfg'");
}
String fakeModeString = (String) config.get("fakemode");
if (isNotBlank(fakeModeString)) {
DropboxSynchronizer.fakeMode = BooleanUtils.toBoolean(fakeModeString);
}
String contentDirString = (String) config.get("contentdir");
if (isNotBlank(contentDirString)) {
DropboxSynchronizer.contentDir = contentDirString;
}
String uploadIntervalString = (String) config.get("uploadInterval");
if (isNotBlank(uploadIntervalString)) {
DropboxSynchronizer.uploadInterval = uploadIntervalString;
}
String downloadIntervalString = (String) config.get("downloadInterval");
if (isNotBlank(downloadIntervalString)) {
DropboxSynchronizer.downloadInterval = downloadIntervalString;
}
String syncModeString = (String) config.get("syncmode");
if (isNotBlank(syncModeString)) {
try {
DropboxSynchronizer.syncMode = DropboxSyncMode.valueOf(syncModeString.toUpperCase());
} catch (IllegalArgumentException iae) {
throw new ConfigurationException(
"dropbox:syncmode", "Unknown SyncMode '" + syncModeString
+ "'. Valid SyncModes are 'DROPBOX_TO_LOCAL', 'LOCAL_TO_DROPBOX' and 'BIDIRECTIONAL'.");
}
}
String uploadFilterString = (String) config.get("uploadfilter");
if (isNotBlank(uploadFilterString)) {
String[] newFilterElements = uploadFilterString.split(",");
uploadFilterElements = Arrays.asList(newFilterElements);
}
String downloadFilterString = (String) config.get("downloadfilter");
if (isNotBlank(downloadFilterString)) {
String[] newFilterElements = downloadFilterString.split(",");
downloadFilterElements = Arrays.asList(newFilterElements);
}
// we got thus far, so we define this synchronizer as properly configured ...
isProperlyConfigured = true;
activateSynchronizer();
}
}
// ****************************************************************************
// Synchronisation Jobs
// ****************************************************************************
private void startSynchronizationJobs() {
if (isProperlyConfigured) {
cancelAllJobs();
if (isAuthenticated()) {
scheduleJobs();
} else {
logger.debug("Dropbox-Bundle isn't authorized properly, so the synchronization jobs " +
"won't be started! Please re-initiate the authorization process by restarting the " +
"Dropbox-Bundle through OSGi console.");
}
}
}
/**
* Schedules the quartz synchronization according to the synchronization mode
*/
private void scheduleJobs() {
switch (syncMode) {
case DROPBOX_TO_LOCAL:
schedule(DropboxSynchronizer.downloadInterval, false);
break;
case LOCAL_TO_DROPBOX:
schedule(DropboxSynchronizer.uploadInterval, true);
break;
case BIDIRECTIONAL:
schedule(DropboxSynchronizer.downloadInterval, false);
schedule(DropboxSynchronizer.uploadInterval, true);
break;
default:
throw new IllegalArgumentException("Unknown SyncMode '" + syncMode.toString() + "'");
}
}
/**
* Schedules either a job handling the Upload (<code>LOCAL_TO_DROPBOX</code>)
* or Download (<code>DROPBOX_TO_LOCAL</code>) direction depending on
* <code>isUpload</code>.
*
* @param interval the Trigger interval as cron expression
* @param isUpload
*/
private void schedule(String interval, boolean isUpload) {
String direction = isUpload ? "Upload" : "Download";
try {
Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
JobDetail job = newJob(SynchronizationJob.class)
.withIdentity(direction, DROPBOX_SCHEDULER_GROUP)
.build();
CronTrigger trigger = newTrigger()
.withIdentity(direction, DROPBOX_SCHEDULER_GROUP)
.withSchedule(CronScheduleBuilder.cronSchedule(interval))
.build();
sched.scheduleJob(job, trigger);
logger.debug("Scheduled synchronization job (direction={}) with cron expression '{}'", direction, interval);
} catch (SchedulerException e) {
logger.warn("Could not create synchronization job: {}", e.getMessage());
}
}
/**
* Delete all quartz scheduler jobs of the group <code>Dropbox</code>.
*/
private void cancelAllJobs() {
try {
Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
Set<JobKey> jobKeys = sched.getJobKeys(jobGroupEquals(DROPBOX_SCHEDULER_GROUP));
if (jobKeys.size() > 0) {
sched.deleteJobs(new ArrayList<JobKey>(jobKeys));
logger.debug("Found {} synchronization jobs to delete from DefaulScheduler (keys={})", jobKeys.size(), jobKeys);
}
} catch (SchedulerException e) {
logger.warn("Couldn't remove synchronization job: {}", e.getMessage());
}
}
/**
* A quartz scheduler job to execute the synchronization. There can be only
* one instance of a specific job type running at the same time.
*/
@DisallowConcurrentExecution
public static class SynchronizationJob implements Job {
private final static JobKey UPLOAD_JOB_KEY = new JobKey("Upload", DROPBOX_SCHEDULER_GROUP);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
boolean isUpload =
UPLOAD_JOB_KEY.compareTo(context.getJobDetail().getKey()) == 0;
DropboxSynchronizer synchronizer = DropboxSynchronizer.instance;
if (synchronizer != null) {
try {
DbxClient client = getClient(synchronizer);
if (client != null) {
if (isUpload) {
synchronizer.syncLocalToDropbox(client);
} else {
synchronizer.syncDropboxToLocal(client);
}
} else {
logger.info("Couldn't create Dropbox client. Most likely there has been no "
+ "access token found. Please restart authentication process by typing "
+ "'startAuthentication' on the OSGi console");
}
} catch (Exception e) {
logger.warn("Synchronizing data with Dropbox throws an exception: {}", e.getMessage());
}
} else {
logger.debug("DropboxSynchronizer instance hasn't been initialized properly!");
}
}
/**
* Creates and returns a new {@link DbxClient} initialized with the store access token.
* Returns {@code null} if no access token has been found.
*
* @return a new {@link DbxClient} or <code>null</code> if no access token has been found.
*/
private DbxClient getClient(DropboxSynchronizer synchronizer) {
String accessToken = synchronizer.readAccessToken();
if (StringUtils.isNotBlank(accessToken)) {
return new DbxClient(requestConfig, accessToken);
}
return null;
}
}
}