package com.twasyl.slideshowfx.hosting.connector.dropbox; import com.dropbox.core.*; import com.dropbox.core.v2.DbxClientV2; import com.dropbox.core.v2.files.*; import com.twasyl.slideshowfx.engine.presentation.PresentationEngine; import com.twasyl.slideshowfx.global.configuration.GlobalConfiguration; import com.twasyl.slideshowfx.hosting.connector.AbstractHostingConnector; import com.twasyl.slideshowfx.hosting.connector.BasicHostingConnectorOptions; import com.twasyl.slideshowfx.hosting.connector.exceptions.HostingConnectorException; import com.twasyl.slideshowfx.hosting.connector.io.RemoteFile; import javafx.concurrent.Worker; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.web.WebView; import javafx.stage.Stage; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import static com.twasyl.slideshowfx.engine.presentation.PresentationEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION; /** * This connector allows to interact with Dropbox. * * @author Thierry Wasylczenko * @version 1.0 * @since SlideshowFX 1.0 */ public class DropboxHostingConnector extends AbstractHostingConnector<BasicHostingConnectorOptions> { private static final Logger LOGGER = Logger.getLogger(DropboxHostingConnector.class.getName()); private DbxAppInfo appInfo; private final DbxRequestConfig dropboxConfiguration = new DbxRequestConfig("SlideshowFX", Locale.getDefault().toString()); public DropboxHostingConnector() { super("dropbox", "Dropbox", new RemoteFile(null)); this.setOptions(new BasicHostingConnectorOptions()); String configuration = GlobalConfiguration.getProperty(getConfigurationBaseName().concat(CONSUMER_KEY_PROPERTY_SUFFIX)); if(configuration != null && !configuration.trim().isEmpty()) { this.getOptions().setConsumerKey(configuration.trim()); } configuration = GlobalConfiguration.getProperty(getConfigurationBaseName().concat(CONSUMER_SECRET_PROPERTY_SUFFIX)); if(configuration != null && !configuration.trim().isEmpty()) { this.getOptions().setConsumerSecret(configuration.trim()); } configuration = GlobalConfiguration.getProperty(getConfigurationBaseName().concat(REDIRECT_URI_PROPERTY_SUFFIX)); if(configuration != null && !configuration.trim().isEmpty()) { this.getOptions().setRedirectUri(configuration.trim()); } configuration = GlobalConfiguration.getProperty(getConfigurationBaseName().concat(ACCESS_TOKEN_PROPERTY_SUFFIX)); if(configuration != null && !configuration.trim().isEmpty()) { this.accessToken = configuration; } if(this.getOptions().getConsumerKey() != null && this.getOptions().getConsumerSecret() != null) { this.appInfo = new DbxAppInfo(this.getOptions().getConsumerKey(), this.getOptions().getConsumerSecret()); } } @Override public Node getConfigurationUI() { this.newOptions = new BasicHostingConnectorOptions(); this.newOptions.setConsumerKey(this.getOptions().getConsumerKey()); this.newOptions.setConsumerSecret(this.getOptions().getConsumerSecret()); this.newOptions.setRedirectUri(this.getOptions().getRedirectUri()); final Label consumerKeyLabel = new Label("Consumer key:"); final Label consumerSecretLabel = new Label("Consumer secret:"); final Label redirectUriLabel = new Label("Redirect URI:"); final TextField consumerKeyTextField = new TextField(); consumerKeyTextField.textProperty().bindBidirectional(this.newOptions.consumerKeyProperty()); consumerKeyTextField.setPrefColumnCount(20); final TextField consumerSecretTextField = new TextField(); consumerSecretTextField.textProperty().bindBidirectional(this.newOptions.consumerSecretProperty()); consumerSecretTextField.setPrefColumnCount(20); final TextField redirectUriTextField = new TextField(); redirectUriTextField.textProperty().bindBidirectional(this.newOptions.redirectUriProperty()); redirectUriTextField.setPrefColumnCount(20); final HBox consumerKeyBox = new HBox(5, consumerKeyLabel, consumerKeyTextField); consumerKeyBox.setAlignment(Pos.BASELINE_LEFT); final HBox consumerSecretBox = new HBox(5, consumerSecretLabel, consumerSecretTextField); consumerSecretBox.setAlignment(Pos.BASELINE_LEFT); final HBox redirectUriBox = new HBox(5, redirectUriLabel, redirectUriTextField); redirectUriBox.setAlignment(Pos.BASELINE_LEFT); final VBox container = new VBox(5, consumerKeyBox, consumerSecretBox, redirectUriBox); return container; } @Override public void saveNewOptions() { if(this.getNewOptions() != null) { this.setOptions(this.getNewOptions()); if (this.getOptions().getConsumerKey() != null) { GlobalConfiguration.setProperty(getConfigurationBaseName().concat(CONSUMER_KEY_PROPERTY_SUFFIX), this.getOptions().getConsumerKey()); } if (this.getOptions().getConsumerSecret() != null) { GlobalConfiguration.setProperty(getConfigurationBaseName().concat(CONSUMER_SECRET_PROPERTY_SUFFIX), this.getOptions().getConsumerSecret()); } if (this.getOptions().getRedirectUri() != null) { GlobalConfiguration.setProperty(getConfigurationBaseName().concat(REDIRECT_URI_PROPERTY_SUFFIX), this.getOptions().getRedirectUri()); } if(this.getOptions().getConsumerKey() != null && this.getOptions().getConsumerSecret() != null) { this.appInfo = new DbxAppInfo(this.getOptions().getConsumerKey(), this.getOptions().getConsumerSecret()); } } } @Override public void authenticate() throws HostingConnectorException { if(this.appInfo == null) throw new HostingConnectorException(HostingConnectorException.MISSING_CONFIGURATION); // Prepare the request final DbxWebAuthNoRedirect authentication = new DbxWebAuthNoRedirect(this.dropboxConfiguration, this.appInfo); final WebView browser = new WebView(); final Scene scene = new Scene(browser); final Stage stage = new Stage(); browser.setPrefSize(500, 500); // Listening for the div containing the access code to be displayed browser.getEngine().getLoadWorker().stateProperty().addListener((stateValue, oldState, newState) -> { if (newState == Worker.State.SUCCEEDED) { final Element authCode = browser.getEngine().getDocument().getElementById("auth-code"); if (authCode != null && authCode.hasChildNodes()) { final NamedNodeMap attributes = authCode.getFirstChild().getAttributes(); String dataToken = null; if (attributes != null && (dataToken = attributes.getNamedItem("data-token").getTextContent()) != null) { try { final DbxAuthFinish authenticationFinish = authentication.finish(dataToken); this.accessToken = authenticationFinish.getAccessToken(); } catch (DbxException e) { LOGGER.log(Level.SEVERE, "Can not finish authentication", e); this.accessToken = null; } finally { if (this.accessToken != null) { GlobalConfiguration.setProperty(getConfigurationBaseName().concat(ACCESS_TOKEN_PROPERTY_SUFFIX), this.accessToken); } stage.close(); } } } } }); browser.getEngine().load(authentication.start()); stage.setScene(scene); stage.setTitle("Authorize SlideshowFX in Dropbox"); stage.showAndWait(); if(!this.isAuthenticated()) throw new HostingConnectorException(HostingConnectorException.AUTHENTICATION_FAILURE); } @Override public boolean checkAccessToken() { boolean valid = false; final DbxClientV2 client = new DbxClientV2(this.dropboxConfiguration, this.accessToken); try { client.users().getCurrentAccount(); valid = true; } catch (DbxException e) { LOGGER.log(Level.WARNING, "Can not determine if access token is valid", e); } return valid; } @Override public void disconnect() { } @Override public void upload(PresentationEngine engine, RemoteFile folder, boolean overwrite) throws HostingConnectorException, FileNotFoundException { if(engine == null) throw new NullPointerException("The engine can not be null"); if(engine.getArchive() == null) throw new NullPointerException("The archive to upload can not be null"); if(!engine.getArchive().exists()) throw new FileNotFoundException("The archive to upload does not exist"); if(this.isAuthenticated()) { final String computedName = folder.toString().concat("/".concat(engine.getArchive().getName())); final DbxClientV2 client = new DbxClientV2(this.dropboxConfiguration, this.accessToken); WriteMode writeMode; final StringBuilder fileName = new StringBuilder(); final UploadBuilder uploader = client.files().uploadBuilder(computedName); if(overwrite) { try { final Metadata metadata = client.files().getMetadata(computedName); // Ensure the file has been found. if(metadata != null) { writeMode = WriteMode.OVERWRITE; uploader.withAutorename(true); } else { writeMode = WriteMode.ADD; } } catch (DbxException e) { LOGGER.log(Level.SEVERE, "Can not get file metadata"); writeMode = WriteMode.ADD; } } else { writeMode = WriteMode.ADD; uploader.withAutorename(this.fileExists(engine, folder)); } uploader.withMode(writeMode); try(final InputStream archiveStream = new FileInputStream(engine.getArchive())) { uploader.start().uploadAndFinish(archiveStream); } catch (DbxException | IOException e) { LOGGER.log(Level.SEVERE, "Error while trying to upload the presentation", e); } } else { throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED); } } @Override public File download(File destination, RemoteFile file) throws HostingConnectorException { if(destination == null) throw new NullPointerException("The destination can not be null"); if(file == null) throw new NullPointerException("The file to download can not be null"); if(!destination.isDirectory()) throw new IllegalArgumentException("The destination is not a folder"); File result; if(this.isAuthenticated()) { result = new File(destination, file.getName()); try(final OutputStream out = new FileOutputStream(result)) { final DbxClientV2 client = new DbxClientV2(this.dropboxConfiguration, this.accessToken); client.files().download(file.toString()).download(out); } catch (IOException | DbxException e) { LOGGER.log(Level.SEVERE, "Can not download the file", e); result = null; } } else { throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED); } return result; } @Override public List<RemoteFile> list(RemoteFile parent, boolean includeFolders, boolean includePresentations) throws HostingConnectorException { if(parent == null) throw new NullPointerException("The parent can not be null"); final List<RemoteFile> folders = new ArrayList<>(); if(this.isAuthenticated()) { final DbxClientV2 client = new DbxClientV2(this.dropboxConfiguration, this.accessToken); final ListFolderResult listing; try { listing = client.files().listFolderBuilder(parent.isRoot() ? "" : parent.toString()) .withRecursive(false) .withIncludeDeleted(false) .start(); listing.getEntries() .stream() .filter(entry -> { if(includeFolders && includePresentations) { return isFolder(entry) || (isFile(entry) && isNameEndingWithSuffix(entry, DEFAULT_DOTTED_ARCHIVE_EXTENSION)); } else if(includeFolders && !includePresentations) { return isFolder(entry); } else if(!includeFolders && includePresentations) { return isFolder(entry) && isNameEndingWithSuffix(entry, DEFAULT_DOTTED_ARCHIVE_EXTENSION); } else return false; }) .forEach(entry -> folders.add(this.createRemoteFile(entry, parent))); } catch (DbxException e) { LOGGER.log(Level.SEVERE, "Error while retrieving the folders", e); } } else { throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED); } return folders; } /** * Creates an instance of {@link RemoteFile} from a given {@link Metadata} and a given parent. * @param metadata The metadata to create the remote file for. * @param parent The optional parent of the file. * @return A well created {@link RemoteFile} instance. */ protected RemoteFile createRemoteFile(final Metadata metadata, final RemoteFile parent) { final RemoteFile file = new RemoteFile(parent, metadata.getName()); if(isFile(metadata)) { file.setFile(true); file.setFolder(false); } else { file.setFile(false); file.setFolder(true); } return file; } @Override public boolean fileExists(PresentationEngine engine, RemoteFile destination) throws HostingConnectorException { if(engine == null) throw new NullPointerException("The engine can not be null"); if(engine.getArchive() == null) throw new NullPointerException("The archive file can not be null"); if(destination == null) throw new NullPointerException("The destination can not be null"); boolean exist; if(this.isAuthenticated()) { final DbxClientV2 client = new DbxClientV2(this.dropboxConfiguration, this.accessToken); final RemoteFile remotePresentation = new RemoteFile(destination, engine.getArchive().getName()); try { client.files().getMetadata(remotePresentation.toString()); exist = true; } catch (DbxException e) { LOGGER.log(Level.FINE, "The presentation hasn't been found remotely", e); exist = false; } } else { throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED); } return exist; } /** * Check if a given metadata is considered as a folder or not. * @param metadata The metadata to check. * @return {@code true} if the metadata is a folder, {@code false} otherwise. */ protected boolean isFolder(final Metadata metadata) { return metadata instanceof FolderMetadata; } /** * Check if a given metadata is considered as a file or not. * @param metadata The metadata to check. * @return {@code true} if the metadata is a file, {@code false} otherwise. */ protected boolean isFile(final Metadata metadata) { return metadata instanceof FileMetadata; } /** * Check if the name of a metada is ending with a given suffix. * @param metadata The metadata to check the name for. * @param suffix The suffix expected at the end of the metadata's name. * @return {@code true} if the metadata is ending with the suffix, {@code false} otherwise. */ protected boolean isNameEndingWithSuffix(final Metadata metadata, final String suffix) { return metadata.getName().endsWith(suffix); } /** * Check if the name of the metadata is equal to another name. The check is case sensitive. * @param metadata The metadata to check the name. * @param name The expected name to be considered equal. * @return {@code true} if the names are equal, {@code false} otherwise. */ protected boolean isNameEqual(final Metadata metadata, final String name) { return metadata.getName().equals(name); } }