package com.twasyl.slideshowfx.hosting.connector.box;
import com.box.sdk.*;
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.box.io.BoxFile;
import com.twasyl.slideshowfx.hosting.connector.exceptions.HostingConnectorException;
import com.twasyl.slideshowfx.hosting.connector.io.RemoteFile;
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 java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
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 Box.
*
* @author Thierry Wasylczenko
* @version 1.1
* @since SlideshowFX 1.1
*/
public class BoxHostingConnector extends AbstractHostingConnector<BasicHostingConnectorOptions> {
private static final Logger LOGGER = Logger.getLogger(BoxHostingConnector.class.getName());
protected static final String REFRESH_TOKEN_PROPERTY_SUFFIX = ".refreshtoken";
private BoxAPIConnection boxApi;
private String refreshToken;
public BoxHostingConnector() {
super("box", "Box", new BoxFile());
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;
}
configuration = GlobalConfiguration.getProperty(getConfigurationBaseName().concat(REFRESH_TOKEN_PROPERTY_SUFFIX));
if(configuration != null && !configuration.trim().isEmpty()) {
this.refreshToken = configuration;
}
if(this.getOptions().getConsumerKey() != null && this.getOptions().getConsumerSecret() != null) {
this.boxApi = new BoxAPIConnection(this.getOptions().getConsumerKey(), this.getOptions().getConsumerSecret());
if(this.accessToken != null && !this.accessToken.isEmpty()) {
this.boxApi.setAccessToken(this.accessToken);
}
if(this.refreshToken != null && !this.refreshToken.isEmpty()) {
this.boxApi.setRefreshToken(this.refreshToken);
}
}
}
@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.boxApi = new BoxAPIConnection(this.getOptions().getConsumerKey(), this.getOptions().getConsumerSecret());
}
}
}
@Override
public void authenticate() throws HostingConnectorException {
if(this.boxApi == null) throw new HostingConnectorException(HostingConnectorException.MISSING_CONFIGURATION);
final WebView browser = new WebView();
final Scene scene = new Scene(browser);
final Stage stage = new Stage();
browser.setPrefSize(500, 500);
browser.getEngine().locationProperty().addListener((locationProperty, oldLocation, newLocation) -> {
if(newLocation != null && newLocation.startsWith(this.getOptions().getRedirectUri())) {
try {
final Map<String, String> uriParameters = getURIParameters(new URI(newLocation));
if(uriParameters.containsKey("code")) {
this.boxApi.authenticate(uriParameters.get("code"));
this.accessToken = this.boxApi.getAccessToken();
this.refreshToken = this.boxApi.getRefreshToken();
}
} catch (URISyntaxException e) {
LOGGER.log(Level.SEVERE, "Error when parsing the redirect URI", e);
} finally {
if (this.accessToken != null) {
GlobalConfiguration.setProperty(getConfigurationBaseName().concat(ACCESS_TOKEN_PROPERTY_SUFFIX), this.accessToken);
}
if (this.refreshToken != null) {
GlobalConfiguration.setProperty(getConfigurationBaseName().concat(REFRESH_TOKEN_PROPERTY_SUFFIX), this.refreshToken);
}
stage.close();
}
}
});
browser.getEngine().load(getAuthenticationURL());
stage.setScene(scene);
stage.setTitle("Authorize SlideshowFX in Box");
stage.showAndWait();
if(!this.isAuthenticated()) throw new HostingConnectorException(HostingConnectorException.AUTHENTICATION_FAILURE);
}
/**
* Get the URL allowing to ask the user the authorization for SlideshowFX to Box.
* @return The authorization URL.
*/
protected String getAuthenticationURL() {
final StringBuilder url = new StringBuilder("https://account.box.com/api/oauth2/authorize")
.append("?response_type=code")
.append("&client_id=").append(this.getOptions().getConsumerKey())
.append("&redirect_uri=").append(this.getOptions().getRedirectUri())
.append("&state=").append(System.currentTimeMillis());
return url.toString();
}
@Override
public boolean checkAccessToken() {
boolean valid = false;
if(this.boxApi != null) {
try {
BoxUser.getCurrentUser(this.boxApi);
valid = true;
} catch(Exception e) {
LOGGER.log(Level.FINE, "Error when trying to check the access token", 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(!(folder instanceof BoxFile)) throw new IllegalArgumentException("The given folder must be a BoxFile");
if(this.isAuthenticated()) {
BoxFolder destination = folder.isRoot() ? BoxFolder.getRootFolder(this.boxApi) : new BoxFolder(this.boxApi, ((BoxFile) folder).getId());
final FileUploadParams parameters = new FileUploadParams();
if(!overwrite && this.fileExists(engine, folder)) {
final String nameWithoutExtension = engine.getArchive().getName().substring(0, engine.getArchive().getName().lastIndexOf("."));
final Calendar calendar = Calendar.getInstance();
parameters.setName(String.format("%1$s %2$tF %2$tT.%3$s", nameWithoutExtension, calendar, engine.getArchiveExtension()));
} else {
parameters.setName(engine.getArchive().getName());
}
parameters.setSize(engine.getArchive().length());
parameters.setModified(new Date(System.currentTimeMillis()));
try(final FileInputStream input = new FileInputStream(engine.getArchive())) {
parameters.setContent(input);
destination.uploadFile(parameters);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not 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(!(file instanceof BoxFile)) throw new IllegalArgumentException("The given file must be a BoxFile");
if(!destination.isDirectory()) throw new IllegalArgumentException("The destination is not a folder");
if(this.isAuthenticated()) {
final com.box.sdk.BoxFile fileToDownload = new com.box.sdk.BoxFile(this.boxApi, ((BoxFile) file).getId());
try(final FileOutputStream output = new FileOutputStream(new File(destination, file.getName()))) {
fileToDownload.download(output);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not download the file", e);
}
} else {
throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED);
}
return null;
}
@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");
if(!(parent instanceof BoxFile)) throw new IllegalArgumentException("The given parent must be a BoxFile");
final List<RemoteFile> folders = new ArrayList<>();
if(this.isAuthenticated()) {
final BoxFolder folder = parent.isRoot() ? BoxFolder.getRootFolder(this.boxApi)
: new BoxFolder(this.boxApi, ((BoxFile) parent).getId());
folder.forEach(child -> {
if(canFileBeLister(child, includeFolders, includePresentations)) {
folders.add(this.createRemoteFile(child, (BoxFile) parent));
}
});
} else {
throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED);
}
return folders;
}
@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");
if(!(destination instanceof BoxFile)) throw new IllegalArgumentException("The given destination must be a BoxFile");
boolean exist = false;
if(this.isAuthenticated()) {
BoxFolder destinationFolder = destination.isRoot() ? BoxFolder.getRootFolder(this.boxApi) : new BoxFolder(this.boxApi, ((BoxFile) destination).getId());
final Iterator<BoxItem.Info> children = destinationFolder.getChildren().iterator();
while(!exist && children.hasNext()) {
final BoxItem.Info child = children.next();
exist = isFile(child) && isNameEqual(child, engine.getArchive().getName());
}
} else {
throw new HostingConnectorException(HostingConnectorException.NOT_AUTHENTICATED);
}
return exist;
}
/**
* Creates an instance of {@link RemoteFile} from a given {@link BoxItem.Info} and a given parent.
* @param info The info 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 BoxItem.Info info, final BoxFile parent) {
final BoxFile file = new BoxFile(parent, info.getName(), info.getID());
if(isFile(info)) {
file.setFile(true);
file.setFolder(false);
} else {
file.setFile(false);
file.setFolder(true);
}
return file;
}
/**
* Check if a given {@link BoxItem.Info} can be listed in the UI.
* @param child The info to check.
* @param includeFolders Indicates if the folders are allowed to be listed.
* @param includePresentations Indicates if the presentations are allowed to be listed.
* @return {@code true} if the child can be listed, {@code false} otherwise.
*/
protected boolean canFileBeLister(BoxItem.Info child, boolean includeFolders, boolean includePresentations) {
boolean canBeListed = false;
if(includeFolders && includePresentations) {
canBeListed = isFolder(child) || (isFile(child) && isNameEndingWithSuffix(child, DEFAULT_DOTTED_ARCHIVE_EXTENSION));
} else if(includeFolders && !includePresentations) {
canBeListed = isFolder(child);
} else if(!includeFolders && includePresentations) {
canBeListed = isFolder(child) && isNameEndingWithSuffix(child, DEFAULT_DOTTED_ARCHIVE_EXTENSION);
}
return canBeListed;
}
/**
* Check if a given info is considered as a folder or not.
* @param info The info to check.
* @return {@code true} if the info is a folder, {@code false} otherwise.
*/
protected boolean isFolder(final BoxItem.Info info) {
return info instanceof BoxFolder.Info;
}
/**
* Check if a given info is considered as a file or not.
* @param info The info to check.
* @return {@code true} if the info is a file, {@code false} otherwise.
*/
protected boolean isFile(final BoxItem.Info info) {
return info instanceof com.box.sdk.BoxFile.Info;
}
/**
* Check if the name of an info is ending with a given suffix.
* @param info The info to check the name for.
* @param suffix The suffix expected at the end of the info's name.
* @return {@code true} if the info is ending with the suffix, {@code false} otherwise.
*/
protected boolean isNameEndingWithSuffix(final BoxItem.Info info, final String suffix) {
return info.getName().endsWith(suffix);
}
/**
* Check if the name of the info is equal to another name. The check is case sensitive.
* @param info The info 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 BoxItem.Info info, final String name) {
return info.getName().equalsIgnoreCase(name);
}
}