org.exoplatform.clouddrive.gdrive.GoogleDriveAPI.java Source code

Java tutorial

Introduction

Here is the source code for org.exoplatform.clouddrive.gdrive.GoogleDriveAPI.java

Source

/*
 * Copyright (C) 2003-2013 eXo Platform SAS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.exoplatform.clouddrive.gdrive;

import com.google.api.client.auth.oauth2.AuthorizationCodeFlow.CredentialCreatedListener;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.CredentialRefreshListener;
import com.google.api.client.auth.oauth2.StoredCredential;
import com.google.api.client.auth.oauth2.TokenErrorResponse;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.googleapis.json.GoogleJsonError.ErrorInfo;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.AbstractInputStreamContent;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.DataStore;
import com.google.api.client.util.store.DataStoreFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Changes;
import com.google.api.services.drive.Drive.Children;
import com.google.api.services.drive.Drive.Files.Delete;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.About;
import com.google.api.services.drive.model.Change;
import com.google.api.services.drive.model.ChangeList;
import com.google.api.services.drive.model.ChildList;
import com.google.api.services.drive.model.ChildReference;
import com.google.api.services.drive.model.File;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.Oauth2Scopes;
import com.google.api.services.oauth2.model.Userinfoplus;

import org.exoplatform.clouddrive.CloudDriveAccessException;
import org.exoplatform.clouddrive.CloudDriveException;
import org.exoplatform.clouddrive.NotFoundException;
import org.exoplatform.clouddrive.oauth2.UserToken;
import org.exoplatform.clouddrive.utils.ChunkIterator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Covers calls to Google Drive services and handles related exceptions. <br>
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:pnedonosko@exoplatform.com">Peter Nedonosko</a>
 * @version $Id: GoogleDriveAPI.java 00000 Jan 5, 2013 pnedonosko $
 */
class GoogleDriveAPI implements DataStoreFactory {

    public static final String APP_NAME = "eXo Cloud Drive";

    public static final String FOLDER_MIMETYPE = "application/vnd.google-apps.folder";

    public static final List<String> SCOPES = Arrays.asList(DriveScopes.DRIVE, DriveScopes.DRIVE_FILE,
            DriveScopes.DRIVE_APPDATA, DriveScopes.DRIVE_SCRIPTS, DriveScopes.DRIVE_APPS_READONLY,
            Oauth2Scopes.USERINFO_EMAIL, Oauth2Scopes.USERINFO_PROFILE);

    public static final String SCOPES_STRING = scopes();

    public static final String ACCESS_TYPE = "offline";

    public static final String APPOVAl_PROMT = "force";

    public static final String NO_STATE = "__no_state_set__";

    protected static final String USER_ID = "user_id";

    protected static final String USER_EMAIL_ADDRESS = "emailAddress";

    protected static final Log LOG = ExoLogger.getLogger(GoogleDriveAPI.class);

    class AuthToken extends UserToken implements CredentialRefreshListener, CredentialCreatedListener {

        class Store implements DataStore<StoredCredential> {

            /**
             * {@inheritDoc}
             */
            @Override
            public DataStoreFactory getDataStoreFactory() {
                return GoogleDriveAPI.this;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public String getId() {
                return GoogleDriveAPI.class.getSimpleName();
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public int size() throws IOException {
                // Only one item possible - current user token
                return isEmpty() ? 0 : 1;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public boolean isEmpty() throws IOException {
                return getAccessToken() == null || getRefreshToken() == null;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public boolean containsKey(String userId) throws IOException {
                return USER_ID.equals(userId);
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public boolean containsValue(StoredCredential value) throws IOException {
                return value.getAccessToken().equals(getAccessToken())
                        && value.getRefreshToken().equals(getRefreshToken())
                        && value.getExpirationTimeMilliseconds() == getExpirationTime();
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public Set<String> keySet() throws IOException {
                Set<String> keys = new HashSet<String>();
                keys.add(USER_ID);
                return keys;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public Collection<StoredCredential> values() throws IOException {
                StoredCredential[] single = new StoredCredential[] { get(USER_ID) };
                return Arrays.asList(single);
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public StoredCredential get(String userId) throws IOException {
                // Need return StoredCredential...
                StoredCredential stored = new StoredCredential();
                stored.setAccessToken(getAccessToken());
                stored.setRefreshToken(getRefreshToken());
                stored.setExpirationTimeMilliseconds(getExpirationTime());
                return stored;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public DataStore<StoredCredential> set(String userId, StoredCredential value) throws IOException {
                store(value);
                return this;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public DataStore<StoredCredential> clear() throws IOException {
                // TODO clear the token keys
                return this;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public DataStore<StoredCredential> delete(String userId) throws IOException {
                // TODO clear the token keys
                return this;
            }
        }

        /**
         * Facade-implementation of {@link DataStore} to actual value stored in enclosing {@link UserToken}.
         */
        final Store store = new Store();

        void store(StoredCredential credential) {
            try {
                store(credential.getAccessToken(), credential.getRefreshToken(),
                        credential.getExpirationTimeMilliseconds());
            } catch (CloudDriveException e) {
                LOG.error("Error storing credential", e);
            }
        }

        void store(Credential credential) {
            try {
                store(credential.getAccessToken(), credential.getRefreshToken(),
                        credential.getExpirationTimeMilliseconds());
            } catch (CloudDriveException e) {
                LOG.error("Error storing credential", e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onCredentialCreated(Credential credential, TokenResponse tokenResponse) throws IOException {
            store(credential);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onTokenResponse(Credential credential, TokenResponse tokenResponse) throws IOException {
            store(credential);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onTokenErrorResponse(Credential credential, TokenErrorResponse tokenErrorResponse)
                throws IOException {
            // TODO clean token keys to let them be re-requested to the user
            String errDescription = tokenErrorResponse.getErrorDescription();
            String errURI = tokenErrorResponse.getErrorUri();
            LOG.error("Error refreshing credentials: " + tokenErrorResponse.getError()
                    + (errDescription != null ? " " + errDescription : "")
                    + (errURI != null ? ". Error URI: " + errURI : ""));
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void merge(UserToken newToken) throws CloudDriveException {
            super.merge(newToken);

            // explicitly apply token keys to currently used credential
            if (credential != null) {
                credential.setAccessToken(newToken.getAccessToken());
                credential.setExpirationTimeMilliseconds(newToken.getExpirationTime());
                credential.setRefreshToken(newToken.getRefreshToken());
            }
        }
    }

    class ChildIterator extends ChunkIterator<ChildReference> {
        final Children.List request;

        /**
         * @throws GoogleDriveException
         */
        ChildIterator(String fileId) throws GoogleDriveException {
            try {
                this.request = drive.children().list(fileId);
            } catch (IOException e) {
                throw new GoogleDriveException("Error creating request to Children.List service: " + e.getMessage(),
                        e);
            }

            // fetch first page
            iter = nextChunk();
        }

        @Override
        protected Iterator<ChildReference> nextChunk() throws GoogleDriveException {
            try {
                ChildList children = request.execute();
                request.setPageToken(children.getNextPageToken());
                List<ChildReference> items = children.getItems();

                available(items.size());

                return items.iterator();
            } catch (IOException e) {
                throw new GoogleDriveException("Error requesting Children.List service: " + e.getMessage(), e);
            }
        }

        @Override
        protected boolean hasNextChunk() {
            return request.getPageToken() != null && request.getPageToken().length() > 0;
        }
    }

    class ChangesIterator extends ChunkIterator<Change> {
        final Changes.List request;

        long largestChangeId;

        /**
         * @throws GoogleDriveException
         */
        ChangesIterator(long startChangeId) throws GoogleDriveException {
            try {
                this.request = drive.changes().list();
                this.request.setIncludeSubscribed(false); // get changes of files only explicitly added to user drive
                this.request.setIncludeDeleted(true);
                this.request.setStartChangeId(startChangeId);
            } catch (IOException e) {
                throw new GoogleDriveException("Error creating request to Changes.List service: " + e.getMessage(),
                        e);
            }

            // fetch first page
            iter = nextChunk();
        }

        @Override
        protected Iterator<Change> nextChunk() throws GoogleDriveException {
            try {
                ChangeList children = request.execute();
                largestChangeId = children.getLargestChangeId();
                request.setPageToken(children.getNextPageToken());
                List<Change> items = children.getItems();

                available(items.size());

                return items.iterator();
            } catch (IOException e) {
                throw new GoogleDriveException("Error requesting Children.List service: " + e.getMessage(), e);
            }
        }

        @Override
        protected boolean hasNextChunk() {
            return request.getPageToken() != null && request.getPageToken().length() > 0;
        }

        long getLargestChangeId() {
            return largestChangeId;
        }
    }

    /**
     * Credentials for request authentication.
     */
    final Credential credential;

    /**
     * Drive services API.
     */
    final Drive drive;

    final AuthToken token;

    /**
     * User info API.
     */
    final Oauth2 oauth2;

    /**
     * Timezone regexp pattern for adapting Google's date format to SimpleDateFormatter supported.
     */
    // full date pattern: \\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z)
    final Pattern tzPattern = Pattern.compile("([+-][0-2]\\d:[0-5]\\d|Z)$");

    /**
     * Create Google Drive API from OAuth2 authentication code.
     * 
     * @param clientId {@link String}
     * @param clientSecret {@link String}
     * @param authCode {@link String}
     * @throws GoogleDriveException if authentication failed for any reason.
     * @throws CloudDriveException if credentials store exception happen
     */
    GoogleDriveAPI(String clientId, String clientSecret, String authCode, String redirectUri)
            throws GoogleDriveException, CloudDriveException {
        // use clean token, it will be populated with actual credentials as CredentialRefreshListener
        this.token = new AuthToken();

        GoogleAuthorizationCodeFlow authFlow;
        try {
            authFlow = createFlow(clientId, clientSecret, token);
        } catch (IOException e) {
            throw new GoogleDriveException("Error creating authentication flow: " + e.getMessage(), e);
        }

        GoogleTokenResponse response;
        try {
            // Exchange an authorization code for OAuth 2.0 credentials.
            response = authFlow.newTokenRequest(authCode).setRedirectUri(redirectUri).execute();
        } catch (IOException e) {
            throw new GoogleDriveException("Error authenticating user code: " + e.getMessage(), e);
        }

        try {
            this.credential = authFlow.createAndStoreCredential(response, USER_ID);
        } catch (IOException e) {
            throw new CloudDriveException("Error storing user credential: " + e.getMessage(), e);
        }

        // XXX .setHttpRequestInitializer(new RequestInitializer() this causes OAuth2 401 Unauthorized
        this.drive = new Drive.Builder(new NetHttpTransport(), new JacksonFactory(), this.credential)
                .setApplicationName(APP_NAME).build();
        this.oauth2 = new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), this.credential)
                .setApplicationName(APP_NAME).build();
    }

    /**
     * Create Google Drive API from existing user credentials.
     * 
     * @param clientId {@link String}
     * @param clientSecret {@link String}
     * @param accessToken {@link String}
     * @param refreshToken {@link String}
     * @param expirationTime long, token expiration time on milliseconds
     * @throws CloudDriveException if credentials store exception happen
     */
    GoogleDriveAPI(String clientId, String clientSecret, String accessToken, String refreshToken,
            long expirationTime) throws CloudDriveException {
        this.token = new AuthToken();
        this.token.load(accessToken, refreshToken, expirationTime);

        GoogleAuthorizationCodeFlow authFlow;
        try {
            authFlow = createFlow(clientId, clientSecret, token);
        } catch (IOException e) {
            throw new GoogleDriveException("Error creating authentication flow: " + e.getMessage(), e);
        }

        try {
            this.credential = authFlow.loadCredential(USER_ID);
        } catch (IOException e) {
            throw new CloudDriveException("Error loading Google user credentials: " + e.getMessage(), e);
        }

        // XXX .setHttpRequestInitializer(new RequestInitializer() this causes OAuth2 401 Unauthorized
        this.drive = new Drive.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
                .setApplicationName(APP_NAME).build();
        this.oauth2 = new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
                .setApplicationName(APP_NAME).build();
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public DataStore<StoredCredential> getDataStore(String id) throws IOException {
        return token.store;
    }

    private static String scopes() {
        StringBuilder s = new StringBuilder();
        for (String scope : SCOPES) {
            s.append(scope);
            s.append(' ');
        }
        return s.toString();
    }

    /**
     * Build an authorization flow optionally using provided {@link AuthToken}, then store it as a static
     * class attribute.
     * 
     * @param clientId
     * @param clientSecret
     * @param tokenStore
     * @return GoogleAuthorizationCodeFlow instance.
     * @throws IOException
     */
    GoogleAuthorizationCodeFlow createFlow(String clientId, String clientSecret, AuthToken storedToken)
            throws IOException {
        HttpTransport httpTransport = new NetHttpTransport();
        JacksonFactory jsonFactory = new JacksonFactory();

        GoogleAuthorizationCodeFlow.Builder flow = new GoogleAuthorizationCodeFlow.Builder(httpTransport,
                jsonFactory, clientId, clientSecret, SCOPES);
        // (access_type=offline) if application needs to refresh access tokens
        // when the user is not present at the browser.
        // If the value "approval_prompt" is force, then the user sees a
        // consent page even if they have previously given consent to your
        // application for a given set of scopes.
        // was setApprovalPrompt("force")
        flow.setAccessType(ACCESS_TYPE).setApprovalPrompt(APPOVAl_PROMT);

        if (storedToken != null) {
            flow.setCredentialDataStore(storedToken.store);
            flow.setCredentialCreatedListener(storedToken);
            flow.addRefreshListener(storedToken);
        }

        return flow.build();
    }

    /**
     * Return Userinfo service.
     * 
     * @return {@link Userinfoplus}
     * @throws GoogleDriveException on error from OAuth2 service
     * @throws CloudDriveException if no Userinfo found
     */
    Userinfoplus userInfo() throws GoogleDriveException, CloudDriveException {
        Userinfoplus userInfo;
        try {
            userInfo = oauth2.userinfo().get().execute();
        } catch (GoogleJsonResponseException e) {
            GoogleJsonError error = e.getDetails();
            // More error information can be retrieved with error.getErrors().
            throw new GoogleDriveException(
                    "Error getting userinfo: " + error.getMessage() + " (" + error.getCode() + ").", e);
        } catch (HttpResponseException e) {
            // No Json body was returned by the API.
            throw new GoogleDriveException(
                    "Error handling userinfo response: " + e.getMessage() + " (" + e.getStatusCode() + ").", e);
        } catch (IOException e) {
            throw new GoogleDriveException("Error requesting userinfo: " + e.getMessage(), e);
        }
        if (userInfo != null && userInfo.getId() != null) {
            return userInfo;
        } else {
            throw new CloudDriveException("User ID cannot be retrieved.");
        }
    }

    /**
     * Return About service.
     * 
     * @return {@link About}
     * @throws GoogleDriveException
     * @throws CloudDriveAccessException
     */
    About about() throws GoogleDriveException, CloudDriveAccessException {
        try {
            return drive.about().get().execute();
        } catch (GoogleJsonResponseException e) {
            if (e.getStatusCode() == 403) {
                throw new CloudDriveAccessException("Error accessing About service: " + e.getMessage(), e);
            } else {
                throw new GoogleDriveException("Error reading About service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error requesting About service: " + e.getMessage(), e);
        }
    }

    ChildIterator children(String fileId) throws GoogleDriveException {
        return new ChildIterator(fileId);
    }

    ChangesIterator changes(long startChangeId) throws GoogleDriveException {
        return new ChangesIterator(startChangeId);
    }

    /**
     * Read file from Files service.
     * 
     * @param fileId {@link String}
     * @return {@link File}
     * @throws GoogleDriveException
     * @throws NotFoundException
     */
    File file(String fileId) throws GoogleDriveException, NotFoundException {
        try {
            return drive.files().get(fileId).execute();
        } catch (GoogleJsonResponseException e) {
            if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found: " + fileId, e);
            } else {
                throw new GoogleDriveException("Error getting file from Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error requesting file from Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Insert a new file to Files service and upload its content.
     * 
     * @param file {@link File} file metadata
     * @param file {@link AbstractInputStreamContent} file content
     * @return {@link File} resulting file
     * @throws GoogleDriveException
     * @throws CloudDriveAccessException
     */
    File insert(File file, AbstractInputStreamContent content)
            throws GoogleDriveException, CloudDriveAccessException {
        try {
            return drive.files().insert(file, content).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException(
                        "Insufficient permissions to inserting file with content in Files service. "
                                + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else {
                throw new GoogleDriveException(
                        "Error inserting file with content to Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error inserting file with content to Files service: " + e.getMessage(),
                    e);
        }
    }

    /**
     * Insert a new file to Files service. This method will create an empty file or a folder (if given file
     * object has such mimetype).
     * 
     * @param file {@link File} file metadata
     * @return {@link File} resulting file
     * @throws GoogleDriveException
     * @throws CloudDriveAccessException
     */
    File insert(File file) throws GoogleDriveException, CloudDriveAccessException {
        try {
            return drive.files().insert(file).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to insert file to Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else {
                throw new GoogleDriveException("Error inserting file to Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error inserting file to Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Update a file metadata in Files service and upload its new content.
     * 
     * @param file {@link File} file metadata
     * @param file {@link AbstractInputStreamContent} file content
     * @return {@link File} resulting file
     * @throws GoogleDriveException
     * @throws NotFoundException
     * @throws CloudDriveAccessException
     */
    void update(File file, AbstractInputStreamContent content)
            throws GoogleDriveException, NotFoundException, CloudDriveAccessException {
        // TODO use If-Match with local ETag to esnure consistency
        // http://stackoverflow.com/questions/15723284/google-drive-sdk-check-etag-when-uploading-synchronizing
        String fileId = file.getId();
        try {
            // file id update not assumed in this context
            drive.files().update(fileId, file, content).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to update file in Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found for updating: " + fileId, e);
            } else {
                throw new GoogleDriveException("Error updating file in Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error upating file with content in Files service: " + e.getMessage(),
                    e);
        }
    }

    /**
     * Update a file metadata in Files service.
     * 
     * @param file {@link File} file metadata
     * @return {@link File} resulting file
     * @throws GoogleDriveException
     * @throws NotFoundException
     * @throws CloudDriveAccessException
     */
    void update(File file) throws GoogleDriveException, NotFoundException, CloudDriveAccessException {
        // TODO use If-Match with local ETag to esnure consistency
        // http://stackoverflow.com/questions/15723284/google-drive-sdk-check-etag-when-uploading-synchronizing
        String fileId = file.getId();
        try {
            // file id update not assumed in this context
            drive.files().update(fileId, file).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to update file in Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found for updating: " + fileId, e);
            } else {
                throw new GoogleDriveException("Error updating file in Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error upating file metadata in Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Copy a file in Files service.
     * 
     * @param srcFileId {@link String}
     * @param destFile {@link File} destination file metadata
     * @return {@link File} resulting file
     * @throws GoogleDriveException
     * @throws NotFoundException
     * @throws CloudDriveAccessException
     */
    File copy(String srcFileId, File destFile)
            throws GoogleDriveException, NotFoundException, CloudDriveAccessException {
        // TODO use If-Match with local ETag to esnure consistency
        // http://stackoverflow.com/questions/15723284/google-drive-sdk-check-etag-when-uploading-synchronizing
        try {
            return drive.files().copy(srcFileId, destFile).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to copy file in Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found for copying: " + srcFileId, e);
            } else {
                throw new GoogleDriveException("Error copying file in Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error copying file metadata in Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Delete a file in Files service.
     * 
     * @param fileId {@link String} file id
     * @return {@link Delete} resulting object
     * @throws GoogleDriveException
     * @throws NotFoundException
     * @throws CloudDriveAccessException
     */
    void delete(String fileId) throws GoogleDriveException, NotFoundException, CloudDriveAccessException {
        try {
            drive.files().delete(fileId).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to delete file in Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found for deleting: " + fileId, e);
            } else {
                throw new GoogleDriveException("Error deleting file in Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error deleting file in Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Move a file to Trash using Files service.
     * 
     * @param fileId {@link String} file id
     * @return {@link File} resulting object
     * @throws GoogleDriveException
     * @throws NotFoundException
     * @throws CloudDriveAccessException
     */
    File trash(String fileId) throws GoogleDriveException, NotFoundException, CloudDriveAccessException {
        try {
            return drive.files().trash(fileId).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to trash file in Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found for trashing: " + fileId, e);
            } else {
                throw new GoogleDriveException("Error trashing file in Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error trashing file in Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Move a file from Trash to its original place using Files service.
     * 
     * @param fileId {@link String} file id
     * @return {@link File} resulting object
     * @throws GoogleDriveException
     * @throws NotFoundException
     * @throws CloudDriveAccessException
     */
    File untrash(String fileId) throws GoogleDriveException, NotFoundException, CloudDriveAccessException {
        try {
            return drive.files().untrash(fileId).execute();
        } catch (GoogleJsonResponseException e) {
            if (isInsufficientPermissions(e)) {
                throw new CloudDriveAccessException("Insufficient permissions to untrash file in Files service. "
                        + e.getStatusMessage() + " (" + e.getStatusCode() + ")");
            } else if (e.getStatusCode() == 404) {
                throw new NotFoundException("Cloud file not found for untrashing: " + fileId, e);
            } else {
                throw new GoogleDriveException("Error untrashing file in Files service: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GoogleDriveException("Error untrashing file in Files service: " + e.getMessage(), e);
        }
    }

    /**
     * Check credentials isn't expired and refresh them if required.
     * 
     * @throws GoogleDriveException if error during communication with the provider
     */
    void refreshAccess() throws GoogleDriveException {
        Long expirationTime = credential.getExpiresInSeconds();
        if (expirationTime != null && expirationTime < 0) {
            try {
                credential.refreshToken();
            } catch (IOException e) {
                throw new GoogleDriveException("Error refreshing access token: " + e.getMessage(), e);
            }
        }
    }

    /**
     * Update OAuth2 token to a new one.
     * 
     * @param newToken {@link AuthToken}
     * @throws CloudDriveException
     */
    void updateToken(UserToken newToken) throws CloudDriveException {
        this.token.merge(newToken);

    }

    /**
     * Current OAuth2 token associated with this API instance.
     * 
     * @return {@link AuthToken}
     */
    AuthToken getToken() {
        return token;
    }

    // ********** helpers ***********

    boolean isFolder(File file) {
        return file.getMimeType().equals(FOLDER_MIMETYPE);
    }

    /**
     * Parse RFC3339 date format into Calendar type.
     * 
     * @param datestring date in RFC3339 format.
     * @return Calendar.
     */
    Calendar parseDate(String datestring) {
        Date d = new Date();
        Calendar calendar = Calendar.getInstance();
        // if there is no time zone, we don't need to do any special parsing.
        if (datestring.endsWith("Z")) {
            try {
                SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
                d = s.parse(datestring);
            } catch (ParseException pe) {// try again with optional decimals
                SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");
                s.setLenient(true);
                try {
                    d = s.parse(datestring);
                } catch (ParseException e) {
                    LOG.error("An error occurred: ", e);
                }
            }
            calendar.setTime(d);
            return calendar;
        }

        // Google keep dates in form "2014-12-24T13:45:13.620+02:00" - we need convert timezone to RFC 822 form
        Matcher dm = tzPattern.matcher(datestring);
        if (dm.find() && dm.groupCount() >= 1) {
            String tz = dm.group(1);
            datestring = dm.replaceFirst(tz.replace(":", ""));
        }

        SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
        try {
            d = s.parse(datestring);
        } catch (ParseException pe) {
            s = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ");
            s.setLenient(true);
            try {
                d = s.parse(datestring);
            } catch (ParseException e) {
                LOG.error("An error occurred: ", e);
            }
        }
        calendar.setTime(d);
        return calendar;
    }

    // **** internals *****

    private boolean isInsufficientPermissions(GoogleJsonResponseException e) {
        GoogleJsonError details = e.getDetails();
        if (e.getStatusCode() == 403 && details != null) {
            List<ErrorInfo> errors = details.getErrors();
            if (errors != null) {
                for (ErrorInfo ei : errors) {
                    if (ei.getReason().equals("insufficientPermissions")) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}