org.exoplatform.clouddrive.box.BoxAPI.java Source code

Java tutorial

Introduction

Here is the source code for org.exoplatform.clouddrive.box.BoxAPI.java

Source

/*
 * Copyright (C) 2003-2013 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.clouddrive.box;

import com.box.boxjavalibv2.BoxClient;
import com.box.boxjavalibv2.BoxConfigBuilder;
import com.box.boxjavalibv2.BoxRESTClient;
import com.box.boxjavalibv2.authorization.IAuthSecureStorage;
import com.box.boxjavalibv2.authorization.OAuthDataController.OAuthTokenState;
import com.box.boxjavalibv2.authorization.OAuthRefreshListener;
import com.box.boxjavalibv2.dao.BoxCollection;
import com.box.boxjavalibv2.dao.BoxEnterprise;
import com.box.boxjavalibv2.dao.BoxEvent;
import com.box.boxjavalibv2.dao.BoxEventCollection;
import com.box.boxjavalibv2.dao.BoxFile;
import com.box.boxjavalibv2.dao.BoxFolder;
import com.box.boxjavalibv2.dao.BoxItem;
import com.box.boxjavalibv2.dao.BoxOAuthToken;
import com.box.boxjavalibv2.dao.BoxServerError;
import com.box.boxjavalibv2.dao.BoxSharedLink;
import com.box.boxjavalibv2.dao.BoxTypedObject;
import com.box.boxjavalibv2.dao.IAuthData;
import com.box.boxjavalibv2.exceptions.AuthFatalFailureException;
import com.box.boxjavalibv2.exceptions.BoxJSONException;
import com.box.boxjavalibv2.exceptions.BoxServerException;
import com.box.boxjavalibv2.jsonparsing.BoxJSONParser;
import com.box.boxjavalibv2.jsonparsing.BoxResourceHub;
import com.box.boxjavalibv2.requests.requestobjects.BoxEventRequestObject;
import com.box.boxjavalibv2.requests.requestobjects.BoxFileRequestObject;
import com.box.boxjavalibv2.requests.requestobjects.BoxFolderDeleteRequestObject;
import com.box.boxjavalibv2.requests.requestobjects.BoxFolderRequestObject;
import com.box.boxjavalibv2.requests.requestobjects.BoxItemCopyRequestObject;
import com.box.boxjavalibv2.requests.requestobjects.BoxItemRestoreRequestObject;
import com.box.boxjavalibv2.requests.requestobjects.BoxRequestExtras;
import com.box.boxjavalibv2.utils.ISO8601DateParser;
import com.box.boxjavalibv2.utils.Utils;
import com.box.restclientv2.exceptions.BoxRestException;
import com.box.restclientv2.requestsbase.BoxDefaultRequestObject;
import com.box.restclientv2.requestsbase.BoxFileUploadRequestObject;

import org.apache.http.client.HttpClient;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.exoplatform.clouddrive.CloudDriveException;
import org.exoplatform.clouddrive.ConflictException;
import org.exoplatform.clouddrive.FileTrashRemovedException;
import org.exoplatform.clouddrive.NotFoundException;
import org.exoplatform.clouddrive.RefreshAccessException;
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.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

/**
 * All calls to Box API here.
 * 
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:pnedonosko@exoplatform.com">Peter Nedonosko</a>
 * @version $Id: BoxAPI.java 00000 Aug 30, 2013 pnedonosko $
 * 
 */
public class BoxAPI {

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

    /**
     * Pagination size used within Box API.
     */
    public static final int BOX_PAGE_SIZE = 100;

    public static final String NO_STATE = "__no_state_set__";

    /**
     * Id of root folder on Box.
     */
    public static final String BOX_ROOT_ID = "0";

    /**
     * Id of Trash folder on Box.
     */
    public static final String BOX_TRASH_ID = "1";

    /**
     * Box item_status for active items.
     */
    public static final String BOX_ITEM_STATE_ACTIVE = "active";

    /**
     * Box item_status for trashed items.
     */
    public static final String BOX_ITEM_STATE_TRASHED = "trashed";

    /**
     * URL of Box app.
     */
    public static final String BOX_APP_URL = "https://app.box.com/";

    /**
     * Not official part of the path used in file services with Box API.
     */
    protected static final String BOX_FILES_PATH = "files/0/f/";

    /**
     * URL prefix for Box files' UI.
     */
    public static final String BOX_FILE_URL = BOX_APP_URL + BOX_FILES_PATH;

    /**
     * Extension for Box's webdoc files.
     */
    public static final String BOX_WEBDOCUMENT_EXT = "webdoc";

    /**
     * Custom mimetype for Box's webdoc files.
     */
    public static final String BOX_WEBDOCUMENT_MIMETYPE = "application/x-exo.box.webdoc";

    /**
     * Extension for Box's webdoc files.
     */
    public static final String BOX_NOTE_EXT = "boxnote";

    /**
     * Custom mimetype for Box's note files.
     */
    public static final String BOX_NOTE_MIMETYPE = "application/x-exo.box.note";

    /**
     * URL patter for Embedded UI of Box file. Based on:<br>
     * http://stackoverflow.com/questions/12816239/box-com-embedded-file-folder-viewer-code-via-api
     * http://developers.box.com/box-embed/<br>
     */
    public static final String BOX_EMBED_URL = "https://%sapp.box.com/embed_widget/000000000000/%s?"
            + "view=list&sort=date&theme=gray&show_parent_path=no&show_item_feed_actions=no&session_expired=true";

    public static final String BOX_EMBED_URL_SSO = "https://app.box.com/login/auto_initiate_sso?enterprise_id=%s&redirect_url=%s";

    public static final Pattern BOX_URL_CUSTOM_PATTERN = Pattern
            .compile("^https://([\\p{ASCII}]*){1}?\\.app\\.box\\.com/.*\\z");

    public static final Pattern BOX_URL_MAKE_CUSTOM_PATTERN = Pattern.compile("^(https)://(app\\.box\\.com/.*)\\z");

    public static final Set<String> BOX_EVENTS = new HashSet<String>();

    static {
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_CREATE);
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_UPLOAD);
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_MOVE);
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_COPY);
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_TRASH);
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_UNDELETE_VIA_TRASH);
        BOX_EVENTS.add(BoxEvent.EVENT_TYPE_ITEM_RENAME);
    }

    class StoredToken extends UserToken implements OAuthRefreshListener, IAuthSecureStorage {

        void store(BoxOAuthToken btoken) throws CloudDriveException {
            this.store(btoken.getAccessToken(), btoken.getRefreshToken(), btoken.getExpiresIn());
        }

        /**
         * @param newAuthData
         */
        public void onRefresh(IAuthData newAuthData) {
            // save the auth data.
            BoxOAuthToken newToken = (BoxOAuthToken) newAuthData;
            try {
                store(newToken);
            } catch (CloudDriveException e) {
                LOG.error("Error storing refreshed access token", e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void saveAuth(IAuthData auth) {
            try {
                store((BoxOAuthToken) auth);
            } catch (CloudDriveException e) {
                LOG.error("Error saving access token", e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public IAuthData getAuth() {
            Map<String, Object> data = new HashMap<String, Object>();
            data.put(BoxOAuthToken.FIELD_ACCESS_TOKEN, getAccessToken());
            data.put(BoxOAuthToken.FIELD_REFRESH_TOKEN, getRefreshToken());
            data.put(BoxOAuthToken.FIELD_EXPIRES_IN, getExpirationTime());
            data.put(BoxOAuthToken.FIELD_TOKEN_TYPE, "bearer");
            return new BoxOAuthToken(data);
        }
    }

    /**
     * Iterator over whole set of items from Box service. This iterator hides next-chunk logic on
     * request to the service. <br>
     * Iterator methods can throw {@link BoxException} in case of remote or communication errors.
     */
    class ItemsIterator extends ChunkIterator<BoxItem> {
        final String folderId;

        int offset = 0, total = 0;

        /**
         * Parent folder.
         */
        BoxFolder parent;

        ItemsIterator(String folderId) throws CloudDriveException {
            this.folderId = folderId;

            // fetch first
            this.iter = nextChunk();
        }

        protected Iterator<BoxItem> nextChunk() throws CloudDriveException {
            BoxDefaultRequestObject obj = new BoxDefaultRequestObject();
            obj.setPage(BOX_PAGE_SIZE, offset);

            BoxRequestExtras ext = obj.getRequestExtras();
            ext.addField(BoxItem.FIELD_ID);
            ext.addField(BoxItem.FIELD_PARENT);
            ext.addField(BoxItem.FIELD_NAME);
            ext.addField(BoxItem.FIELD_TYPE);
            ext.addField(BoxItem.FIELD_ETAG);
            ext.addField(BoxItem.FIELD_SEQUENCE_ID);
            ext.addField(BoxItem.FIELD_CREATED_AT);
            ext.addField(BoxItem.FIELD_MODIFIED_AT);
            ext.addField(BoxItem.FIELD_DESCRIPTION);
            ext.addField(BoxItem.FIELD_SIZE);
            ext.addField(BoxItem.FIELD_CREATED_BY);
            ext.addField(BoxItem.FIELD_MODIFIED_BY);
            ext.addField(BoxItem.FIELD_OWNED_BY);
            ext.addField(BoxItem.FIELD_SHARED_LINK);
            ext.addField(BoxItem.FIELD_ITEM_STATUS);
            ext.addField(BoxItem.FIELD_PATH_COLLECTION);
            ext.addField(BoxFolder.FIELD_ITEM_COLLECTION);

            try {
                parent = client.getFoldersManager().getFolder(folderId, obj);

                BoxCollection items = parent.getItemCollection();
                // total number of files in the folder
                total = items.getTotalCount();
                if (offset == 0) {
                    available(total);
                }

                offset += items.getEntries().size();

                ArrayList<BoxItem> oitems = new ArrayList<BoxItem>();
                // put folders first, then files
                oitems.addAll(Utils.getTypedObjects(items, BoxFolder.class));
                oitems.addAll(Utils.getTypedObjects(items, BoxFile.class));
                return oitems.iterator();
            } catch (BoxRestException e) {
                throw new BoxException("Error getting folder items: " + e.getMessage(), e);
            } catch (BoxServerException e) {
                int status = getErrorStatus(e);
                if (status == 404 || status == 412) {
                    // not_found or precondition_failed - then folder not found
                    throw new NotFoundException("Folder not found " + folderId, e);
                }
                throw new BoxException("Error reading folder items: " + e.getMessage(), e);
            } catch (AuthFatalFailureException e) {
                checkTokenState();
                throw new BoxException("Authentication error on folder items: " + e.getMessage(), e);
            }
        }

        protected boolean hasNextChunk() {
            return total > offset;
        }
    }

    /**
     * Iterator over set of events from Box service. This iterator hides next-chunk logic on
     * request to the service. <br>
     * Iterator methods can throw {@link BoxException} in case of remote or communication errors.
     */
    class EventsIterator extends ChunkIterator<BoxEvent> {
        /**
         * Set of already fetched event Ids. Used to ignore duplicates from different requests.
         */
        final Set<String> eventIds = new HashSet<String>();

        Long streamPosition;

        Integer offset = 0, chunkSize = 0;

        EventsIterator(long streamPosition) throws BoxException, RefreshAccessException {
            this.streamPosition = streamPosition <= -1 ? BoxEventRequestObject.STREAM_POSITION_NOW : streamPosition;

            // fetch first
            this.iter = nextChunk();
        }

        protected Iterator<BoxEvent> nextChunk() throws BoxException, RefreshAccessException {
            try {
                BoxEventRequestObject request = BoxEventRequestObject.getEventsRequestObject(streamPosition);

                // interest to tree changes only
                request.setStreamType(BoxEventRequestObject.STREAM_TYPE_CHANGES);
                request.setLimit(BOX_PAGE_SIZE);

                BoxEventCollection ec = client.getEventsManager().getEvents(request);

                // for next chunk and next iterators
                streamPosition = ec.getNextStreamPosition();

                ArrayList<BoxEvent> events = new ArrayList<BoxEvent>();
                for (BoxTypedObject eobj : ec.getEntries()) {
                    BoxEvent event = (BoxEvent) eobj;
                    if (BOX_EVENTS.contains(event.getEventType())) {
                        String id = event.getId();
                        if (!eventIds.contains(id)) {
                            eventIds.add(id);
                            events.add(event);
                        }
                    }
                }

                this.chunkSize = events.size();
                return events.iterator();
            } catch (BoxRestException e) {
                throw new BoxException("Error requesting Events service: " + e.getMessage(), e);
            } catch (BoxServerException e) {
                throw new BoxException("Error reading Events service: " + e.getMessage(), e);
            } catch (AuthFatalFailureException e) {
                checkTokenState();
                throw new BoxException("Authentication error for Events service: " + e.getMessage(), e);
            }
        }

        /**
         * {@inheritDoc}
         */
        protected boolean hasNextChunk() {
            // if something was read in previous chunk, then we may have a next chunk
            return chunkSize > 0;
        }

        long getNextStreamPosition() {
            return streamPosition;
        }
    }

    public static class ChangesLink {
        final String type;

        final String url;

        final long maxRetries, retryTimeout, ttl, outdatedTimeout, created;

        ChangesLink(String type, String url, long ttl, long maxRetries, long retryTimeout) {
            this.type = type;
            this.url = url;
            this.ttl = ttl;
            this.maxRetries = maxRetries;
            this.retryTimeout = retryTimeout;

            // link will be outdated in 95% of retry timeout
            this.outdatedTimeout = retryTimeout - Math.round(retryTimeout * 0.05f);
            this.created = System.currentTimeMillis();
        }

        /**
         * @return the type
         */
        public String getType() {
            return type;
        }

        /**
         * @return the url
         */
        public String getUrl() {
            return url;
        }

        /**
         * @return the ttl
         */
        public long getTtl() {
            return ttl;
        }

        /**
         * @return the maxRetries
         */
        public long getMaxRetries() {
            return maxRetries;
        }

        /**
         * @return the retryTimeout
         */
        public long getRetryTimeout() {
            return retryTimeout;
        }

        /**
         * @return the outdatedTimeout
         */
        public long getOutdatedTimeout() {
            return outdatedTimeout;
        }

        /**
         * @return the created
         */
        public long getCreated() {
            return created;
        }

        public boolean isOutdated() {
            return (System.currentTimeMillis() - created) > outdatedTimeout;
        }
    }

    /**
     * Box REST client adopted to Apache HTTP 4.1 (from Platform 4.0) and using allow-all hostname validator.
     * Purpose of this client is to workaround <a href=
     * "http://stackoverflow.com/questions/23529852/multiple-files-upload-to-box-fails-with-http-client-error-connection-still-allo"
     * >multiple files upload problem</a>.
     */
    class RESTClient extends BoxRESTClient {

        final HttpClient httpClient;

        RESTClient() {
            super();

            SchemeRegistry schemeReg = new SchemeRegistry();
            schemeReg.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
            SSLSocketFactory socketFactory;
            try {
                SSLContext sslContext = SSLContext.getInstance(SSLSocketFactory.TLS);
                KeyManagerFactory kmfactory = KeyManagerFactory
                        .getInstance(KeyManagerFactory.getDefaultAlgorithm());
                kmfactory.init(null, null);
                KeyManager[] keymanagers = kmfactory.getKeyManagers();
                TrustManagerFactory tmfactory = TrustManagerFactory
                        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
                tmfactory.init((KeyStore) null);
                TrustManager[] trustmanagers = tmfactory.getTrustManagers();
                sslContext.init(keymanagers, trustmanagers, null);
                socketFactory = new SSLSocketFactory(sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
            } catch (Exception ex) {
                throw new IllegalStateException("Failure initializing default SSL context for Box REST client", ex);
            }
            schemeReg.register(new Scheme("https", 443, socketFactory));

            ThreadSafeClientConnManager connectionManager = new ThreadSafeClientConnManager(schemeReg);
            // XXX 2 recommended by RFC 2616 sec 8.1.4, we make it bigger for quicker // upload
            connectionManager.setDefaultMaxPerRoute(4);
            // 20 by default, we twice it also
            connectionManager.setMaxTotal(40);

            this.httpClient = new DefaultHttpClient(connectionManager);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public HttpClient getRawHttpClient() {
            return httpClient;
        }
    }

    private StoredToken token;

    private BoxClient client;

    private ChangesLink changesLink;

    private String enterpriseId, enterpriseName, customDomain;

    /**
     * Create Box API from OAuth2 authentication code.
     * 
     * @param key {@link String} Box API key the same also as OAuth2 client_id
     * @param clientSecret {@link String}
     * @param authCode {@link String}
     * @throws BoxException if authentication failed for any reason.
     * @throws CloudDriveException if credentials store exception happen
     */
    BoxAPI(String key, String clientSecret, String authCode, String redirectUri)
            throws BoxException, CloudDriveException {

        BoxResourceHub hub = new BoxResourceHub();
        BoxJSONParser parser = new BoxJSONParser(hub);
        this.client = new BoxClient(key, clientSecret, hub, parser, new RESTClient(),
                new BoxConfigBuilder().build());

        this.token = new StoredToken();
        this.client.addOAuthRefreshListener(token);

        try {
            BoxOAuthToken bt = this.client.getOAuthManager().createOAuth(authCode, key, clientSecret, redirectUri);

            this.token.store(bt);
            this.client.authenticate(bt);
        } catch (BoxRestException e) {
            throw new BoxException("Error submiting authentication code: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            throw new BoxException("Error authenticating user code: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            throw new BoxException("Authentication code error: " + e.getMessage(), e);
        }

        // finally init changes link
        updateChangesLink();

        // init user (enterprise etc.)
        initUser();
    }

    /**
     * Create Box API from existing user credentials.
     * 
     * @param key {@link String} Box API key the same also as OAuth2 client_id
     * @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
     */
    BoxAPI(String key, String clientSecret, String accessToken, String refreshToken, long expirationTime)
            throws CloudDriveException {
        BoxResourceHub hub = new BoxResourceHub();
        BoxJSONParser parser = new BoxJSONParser(hub);
        this.client = new BoxClient(key, clientSecret, hub, parser, new RESTClient(),
                new BoxConfigBuilder().build());

        this.token = new StoredToken();
        this.token.load(accessToken, refreshToken, expirationTime);
        this.client.addOAuthRefreshListener(token);
        this.client.authenticateFromSecureStorage(token);

        // init user (enterprise etc.)
        initUser();
    }

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

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

    /**
     * Currently connected Box user.
     * 
     * @return
     * @throws BoxException
     * @throws RefreshAccessException
     */
    com.box.boxjavalibv2.dao.BoxUser getCurrentUser() throws BoxException, RefreshAccessException {
        BoxDefaultRequestObject obj = new BoxDefaultRequestObject();
        obj.getRequestExtras().addField("id");
        obj.getRequestExtras().addField("type");
        obj.getRequestExtras().addField("name");
        obj.getRequestExtras().addField("login");
        obj.getRequestExtras().addField("created_at");
        obj.getRequestExtras().addField("modified_at");
        obj.getRequestExtras().addField("status");
        obj.getRequestExtras().addField("job_title");
        obj.getRequestExtras().addField("phone");
        obj.getRequestExtras().addField("address");
        obj.getRequestExtras().addField("avatar_url");
        obj.getRequestExtras().addField("role");
        obj.getRequestExtras().addField("enterprise");
        try {
            return client.getUsersManager().getCurrentUser(obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error requesting current user: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            throw new BoxException("Error getting current user: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error for current user: " + e.getMessage(), e);
        }
    }

    /**
     * The Box root folder.
     * 
     * @return {@link BoxFolder}
     * @throws BoxException
     */
    BoxFolder getRootFolder() throws BoxException {
        try {
            BoxFolder root = client.getFoldersManager().getFolder(BOX_ROOT_ID, null);
            return root;
        } catch (BoxRestException e) {
            throw new BoxException("Error getting root folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            throw new BoxException("Error reading root folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            throw new BoxException("Authentication error for root folder: " + e.getMessage(), e);
        }
    }

    ItemsIterator getFolderItems(String folderId) throws CloudDriveException {
        return new ItemsIterator(folderId);
    }

    Calendar parseDate(String dateString) throws ParseException {
        Calendar calendar = Calendar.getInstance();
        Date d = ISO8601DateParser.parse(dateString);
        calendar.setTime(d);
        return calendar;
    }

    String formatDate(Calendar date) {
        return ISO8601DateParser.toString(date.getTime());
    }

    /**
     * Link (URl) to the Box file for opening on Box site (UI).
     * 
     * @param item {@link BoxItem}
     * @return String with the file URL.
     */
    String getLink(BoxItem item) {
        BoxSharedLink shared = item.getSharedLink();
        if (shared != null) {
            String link = shared.getUrl();
            if (link != null) {
                return link(link);
            }
        }

        // XXX This link build not on official documentation, but from observed URLs from Box app site.
        StringBuilder link = new StringBuilder();
        link.append(link(BOX_FILE_URL));
        String id = item.getId();
        if (BOX_ROOT_ID.equals(id)) {
            link.append(id);
        } else if (item instanceof BoxFile) {
            String parentId = item.getParent().getId();
            link.append(parentId);
            link.append("/1/f_");
            link.append(id);
        } else if (item instanceof BoxFolder) {
            link.append(id);
            link.append('/');
            link.append(item.getName());
        } else {
            // for unknown open root folder
            link.append(BOX_ROOT_ID);
        }

        link.append("/");
        return link.toString();
    }

    /**
     * Link (URL) to embed a file onto external app (in PLF).
     * 
     * @param item {@link BoxItem}
     * @return String with the file embed URL.
     */
    String getEmbedLink(BoxItem item) {
        StringBuilder linkValue = new StringBuilder();
        BoxSharedLink shared = item.getSharedLink();
        if (shared != null) {
            String link = shared.getUrl();
            String[] lparts = link.split("/");
            if (lparts.length > 3 && lparts[lparts.length - 2].equals("s")) {
                // XXX unofficial way of linkValue extracting from shared link
                linkValue.append("s/");
                linkValue.append(lparts[lparts.length - 1]);
            }
        }

        if (linkValue.length() == 0) {
            linkValue.append(BOX_FILES_PATH);
            // XXX This link build not on official documentation, but from observed URLs from Box app site.
            String id = item.getId();
            if (BOX_ROOT_ID.equals(id)) {
                linkValue.append(id);
            } else if (item instanceof BoxFile) {
                String parentId = item.getParent().getId();
                linkValue.append(parentId);
                linkValue.append("/1/f_");
                linkValue.append(id);
            } else if (item instanceof BoxFolder) {
                linkValue.append(id);
            } else {
                // for unknown open root folder
                linkValue.append(BOX_ROOT_ID);
            }
        }

        // Take in account custom domain for Enterprise on Box
        if (customDomain != null) {
            return String.format(BOX_EMBED_URL, customDomain + ".", linkValue.toString());
        } else {
            return String.format(BOX_EMBED_URL, "", linkValue.toString());
        }
    }

    /**
     * A link (URL) to the Box file thumbnail image.
     * 
     * @param item {@link BoxItem}
     * @return String with the file URL.
     */
    String getThumbnailLink(BoxItem item) {
        // TODO use real thumbnails from Box
        return getLink(item);
    }

    ChangesLink getChangesLink() throws BoxException, RefreshAccessException {
        if (changesLink == null || changesLink.isOutdated()) {
            updateChangesLink();
        }

        return changesLink;
    }

    /**
     * Update link to the drive's long-polling changes notification service. This kind of service optional and
     * may not be supported. If long-polling changes notification not supported then this method will do
     * nothing.
     * 
     */
    void updateChangesLink() throws BoxException, RefreshAccessException {
        BoxDefaultRequestObject obj = new BoxDefaultRequestObject();
        try {
            BoxCollection changesPoll = client.getEventsManager().getEventOptions(obj);
            ArrayList<BoxTypedObject> ce = changesPoll.getEntries();
            if (ce.size() > 0) {
                BoxTypedObject c = ce.get(0);

                Object urlObj = c.getValue("url");
                String url = urlObj != null ? urlObj.toString() : null;

                Object typeObj = c.getValue("type");
                String type = typeObj != null ? typeObj.toString() : null;

                Object ttlObj = c.getValue("ttl");
                if (ttlObj == null) {
                    ttlObj = c.getExtraData("ttl");
                }
                long ttl;
                try {
                    ttl = ttlObj != null ? Long.parseLong(ttlObj.toString()) : 10;
                } catch (NumberFormatException e) {
                    LOG.warn("Error parsing ttl value in Events response [" + ttlObj + "]: " + e);
                    ttl = 10; // 10? What is it ttl? The number from Box docs.
                }

                Object maxRetriesObj = c.getValue("max_retries");
                if (maxRetriesObj == null) {
                    maxRetriesObj = c.getExtraData("max_retries");
                }
                long maxRetries;
                try {
                    maxRetries = maxRetriesObj != null ? Long.parseLong(maxRetriesObj.toString()) : 0;
                } catch (NumberFormatException e) {
                    LOG.warn("Error parsing max_retries value in Events response [" + maxRetriesObj + "]: " + e);
                    maxRetries = 2; // assume two possible attempts to try use the link
                }

                Object retryTimeoutObj = c.getValue("retry_timeout");
                if (retryTimeoutObj == null) {
                    retryTimeoutObj = c.getExtraData("retry_timeout");
                }
                long retryTimeout; // it is in milliseconds
                try {
                    retryTimeout = retryTimeoutObj != null ? Long.parseLong(retryTimeoutObj.toString()) * 1000
                            : 600000;
                } catch (NumberFormatException e) {
                    LOG.warn(
                            "Error parsing retry_timeout value in Events response [" + retryTimeoutObj + "]: " + e);
                    retryTimeout = 600000; // 600 sec = 10min (as mentioned in Box docs)
                }

                this.changesLink = new ChangesLink(type, url, ttl, maxRetries, retryTimeout);
            } else {
                throw new BoxException("Empty entries from Events service.");
            }
        } catch (BoxRestException e) {
            throw new BoxException("Error requesting changes long poll URL: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            throw new BoxException("Error reading changes long poll URL: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error for changes long poll URL: " + e.getMessage(), e);
        }
    }

    EventsIterator getEvents(long streamPosition) throws BoxException, RefreshAccessException {
        return new EventsIterator(streamPosition);
    }

    BoxFile createFile(String parentId, String name, Calendar created, InputStream data)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            // To speedup the process we check if parent exists first.
            // How this speedups: if parent not found we will not wait for the content upload to the Box side.
            try {
                readFolder(parentId);
            } catch (NotFoundException e) {
                // parent not found
                throw new NotFoundException(
                        "Parent not found " + parentId + ". Cannot start file uploading " + name, e);
            }

            // TODO You can optionally specify a Content-MD5 header with the SHA1 hash of the file to ensure that
            // the file is not corrupted in transit.
            BoxFileUploadRequestObject obj = BoxFileUploadRequestObject.uploadFileRequestObject(parentId, name,
                    data);
            obj.setLocalFileCreatedAt(created.getTime());
            obj.put("created_at", formatDate(created));
            return client.getFilesManager().uploadFile(obj);
        } catch (BoxJSONException e) {
            throw new BoxException("Error uploading file: " + e.getMessage(), e);
        } catch (UnsupportedEncodingException e) {
            throw new BoxException("Error uploading file: " + e.getMessage(), e);
        } catch (BoxRestException e) {
            throw new BoxException("Error uploading file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then parent not found
                throw new NotFoundException(
                        "Parent not found " + parentId + ". File uploading canceled for " + name, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to upload a file " + name, e);
            } else if (status == 409) {
                // conflict - the same name file exists
                throw new ConflictException("File with the same name as creating already exists " + name, e);
            }
            throw new BoxException("Error uploading file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when uploading file: " + e.getMessage(), e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BoxException("File " + name + " uploading interrupted.", e);
        }
    }

    BoxFolder createFolder(String parentId, String name, Calendar created)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            BoxFolderRequestObject obj = BoxFolderRequestObject.createFolderRequestObject(name, parentId);
            obj.put("created_at", formatDate(created));
            return client.getFoldersManager().createFolder(obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error creating folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then parent not found
                throw new NotFoundException("Parent not found " + parentId, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to create a folder " + name, e);
            } else if (status == 409) {
                // conflict - the same name file exists
                throw new ConflictException("File with the same name as creating already exists " + name, e);
            }
            throw new BoxException("Error creating folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when creating folder: " + e.getMessage(), e);
        }
    }

    BoxFolder createSharedFolder(String parentId, String name, Calendar created)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            BoxFolderRequestObject obj = BoxFolderRequestObject.createSharedLinkRequestObject(null);
            obj.put("created_at", formatDate(created));
            return client.getSharedFoldersManager("sharedLink", "password").createFolder(obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error creating folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then parent not found
                throw new NotFoundException("Parent not found " + parentId, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to create a folder " + name, e);
            } else if (status == 409) {
                // conflict - the same name file exists
                throw new ConflictException("File with the same name as creating already exists " + name, e);
            }
            throw new BoxException("Error creating folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when creating folder: " + e.getMessage(), e);
        }
    }

    /**
     * Delete a cloud file by given fileId. Depending on Box enterprise settings for this user, the file will
     * either be actually deleted from Box or moved to the Trash.
     * 
     * @param id {@link String}
     * @throws BoxException
     * @throws NotFoundException
     * @throws RefreshAccessException
     */
    void deleteFile(String id) throws BoxException, NotFoundException, RefreshAccessException {
        try {
            BoxDefaultRequestObject obj = new BoxDefaultRequestObject();
            // obj.setIfMatch(etag); // // TODO use it!
            client.getFilesManager().deleteFile(id, obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error deleting file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to the file " + id, e);
            }
            throw new BoxException("Error deleting file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when deleting file: " + e.getMessage(), e);
        }
    }

    /**
     * Delete a cloud folder by given folderId. Depending on Box enterprise settings for this user, the folder
     * will
     * either be actually deleted from Box or moved to the Trash.
     * 
     * @param id {@link String}
     * @throws BoxException
     * @throws NotFoundException
     * @throws RefreshAccessException
     */
    void deleteFolder(String id) throws BoxException, NotFoundException, RefreshAccessException {
        try {
            BoxFolderDeleteRequestObject obj = BoxFolderDeleteRequestObject.deleteFolderRequestObject(true);
            // obj.setIfMatch(etag); // TODO use it!
            client.getFoldersManager().deleteFolder(id, obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error deleting folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to the folder " + id, e);
            }
            throw new BoxException("Error deleting folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when deleting folder: " + e.getMessage(), e);
        }
    }

    /**
     * Trash a cloud file by given fileId. Depending on Box enterprise settings for this user, the file will
     * either be actually deleted from Box or moved to the Trash. If the file was actually deleted on Box, this
     * method will throw {@link FileTrashRemovedException}, and the caller code should delete the file locally
     * also.
     * 
     * @param id {@link String}
     * @return {@link BoxFile} of the file successfully moved to Box Trash
     * @throws BoxException
     * @throws FileTrashRemovedException if file was permanently removed.
     * @throws NotFoundException
     * @throws RefreshAccessException
     */
    BoxFile trashFile(String id)
            throws BoxException, FileTrashRemovedException, NotFoundException, RefreshAccessException {
        try {
            BoxDefaultRequestObject deleteObj = new BoxDefaultRequestObject();
            // deleteObj.setIfMatch(etag); // TODO use it!
            client.getFilesManager().deleteFile(id, deleteObj);

            // check if file actually removed or in the trash
            try {
                BoxDefaultRequestObject trashObj = new BoxDefaultRequestObject();
                return client.getTrashManager().getTrashFile(id, trashObj);
            } catch (BoxRestException e) {
                throw new BoxException("Error reading trashed file: " + e.getMessage(), e);
            } catch (BoxServerException e) {
                int status = getErrorStatus(e);
                if (status == 404 || status == 412) {
                    // not_found or precondition_failed - then file not found in the Trash
                    // XXX throwing an exception not a best solution, but returning a boolean also can have double
                    // meaning: not trashed at all or deleted instead of trashed
                    throw new FileTrashRemovedException("Trashed file deleted permanently " + id);
                }
                throw new BoxException("Error reading trashed file: " + e.getMessage(), e);
            }
        } catch (BoxRestException e) {
            throw new BoxException("Error trashing file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to the file " + id, e);
            }
            throw new BoxException("Error trashing file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when trashing file: " + e.getMessage(), e);
        }
    }

    /**
     * Trash a cloud folder by given folderId. Depending on Box enterprise settings for this user, the folder
     * will either be actually deleted from Box or moved to the Trash. If the folder was actually deleted in
     * Box, this method will return {@link FileTrashRemovedException}, and the caller code should delete the
     * folder locally also.
     * 
     * @param id {@link String}
     * @return {@link BoxFolder} of the folder successfully moved to Box Trash
     * @throws BoxException
     * @throws FileTrashRemovedException if folder was permanently removed.
     * @throws NotFoundException
     * @throws RefreshAccessException
     */
    BoxFolder trashFolder(String id)
            throws BoxException, FileTrashRemovedException, NotFoundException, RefreshAccessException {
        try {
            BoxFolderDeleteRequestObject deleteObj = BoxFolderDeleteRequestObject.deleteFolderRequestObject(true);
            // deleteObj.setIfMatch(etag); // TODO use it!
            client.getFoldersManager().deleteFolder(id, deleteObj);

            // check if file actually removed or in the trash
            try {
                BoxDefaultRequestObject trashObj = new BoxDefaultRequestObject();
                return client.getTrashManager().getTrashFolder(id, trashObj);
            } catch (BoxRestException e) {
                throw new BoxException("Error reading trashed foler: " + e.getMessage(), e);
            } catch (BoxServerException e) {
                int status = getErrorStatus(e);
                if (status == 404 || status == 412) {
                    // not_found or precondition_failed - then foler not found in the Trash
                    // XXX throwing an exception not a best solution, but returning a boolean also can have double
                    // meaning: not trashed at all or deleted instead of trashed
                    throw new FileTrashRemovedException("Trashed folder deleted permanently " + id);
                }
                throw new BoxException("Error reading trashed foler: " + e.getMessage(), e);
            }
        } catch (BoxRestException e) {
            throw new BoxException("Error trashing foler: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            } else if (status == 403) {
                throw new NotFoundException("The user doesn't have access to the folder " + id, e);
            }
            throw new BoxException("Error trashing foler: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when trashing foler: " + e.getMessage(), e);
        }
    }

    BoxFile untrashFile(String id, String name)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            BoxItemRestoreRequestObject obj = BoxItemRestoreRequestObject.restoreItemRequestObject();
            if (name != null) {
                obj.setNewName(name);
            }
            return client.getTrashManager().restoreTrashFile(id, obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error untrashing file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("Trashed file not found " + id, e);
            } else if (status == 405) {
                // method_not_allowed
                throw new NotFoundException("File not in the trash " + id, e);
            } else if (status == 409) {
                // conflict
                throw new ConflictException("File with the same name as untrashed already exists " + id, e);
            }
            throw new BoxException("Error untrashing file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when untrashing file: " + e.getMessage(), e);
        }
    }

    BoxFolder untrashFolder(String id, String name)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            BoxItemRestoreRequestObject obj = BoxItemRestoreRequestObject.restoreItemRequestObject();
            if (name != null) {
                obj.setNewName(name);
            }
            return client.getTrashManager().restoreTrashFolder(id, obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error untrashing folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("Trashed folder not found " + id, e);
            } else if (status == 405) {
                // method_not_allowed
                throw new NotFoundException("Folder not in the trash " + id, e);
            } else if (status == 409) {
                // conflict
                throw new ConflictException("Folder with the same name as untrashed already exists " + id, e);
            }
            throw new BoxException("Error untrashing folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when untrashing folder: " + e.getMessage(), e);
        }
    }

    /**
     * Update file name or/and parent and set given modified date. If file was actually updated (name or/and
     * parent changed) this method return updated file object or <code>null</code> if file already exists
     * with such name and parent.
     * 
     * 
     * @param parentId {@link String}
     * @param id {@link String}
     * @param name {@link String}
     * @param modified {@link Calendar}
     * @return {@link BoxFile} of actually changed file or <code>null</code> if file already exists with
     *         such name and parent.
     * @throws BoxException
     * @throws NotFoundException
     * @throws RefreshAccessException
     * @throws ConflictException
     */
    BoxFile updateFile(String parentId, String id, String name, Calendar modified)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {

        BoxFile existing = readFile(id);
        int attemts = 0;
        boolean nameChanged = !existing.getName().equals(name);
        boolean parentChanged = !existing.getParent().getId().equals(parentId);
        while ((nameChanged || parentChanged) && attemts < 3) {
            attemts++;
            try {
                // if name or parent changed - we do actual update, we ignore modified date changes
                // otherwise, if name the same, Box service will respond with error 409 (conflict)
                BoxFileRequestObject obj = BoxFileRequestObject.getRequestObject();
                // obj.setIfMatch(etag); // TODO use it
                if (nameChanged) {
                    obj.setName(name);
                }
                if (parentChanged) {
                    obj.setParent(parentId);
                }
                obj.put("modified_at", formatDate(modified));
                return client.getFilesManager().updateFileInfo(id, obj);
            } catch (BoxRestException e) {
                throw new BoxException("Error updating file: " + e.getMessage(), e);
            } catch (BoxServerException e) {
                int status = getErrorStatus(e);
                if (status == 404 || status == 412) {
                    // not_found or precondition_failed - then item not found
                    throw new NotFoundException("File not found " + id, e);
                } else if (status == 409) {
                    // conflict, try again
                    if (attemts < 3) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(
                                    "File with the same name as updated already exists " + id + ". Trying again.");
                        }
                        existing = readFile(id);
                        nameChanged = !existing.getName().equalsIgnoreCase(name);
                        parentChanged = !existing.getParent().getId().equals(parentId);
                    } else {
                        throw new ConflictException("File with the same name as updated already exists " + id);
                    }
                } else {
                    throw new BoxException("Error updating file: " + e.getMessage(), e);
                }
            } catch (UnsupportedEncodingException e) {
                throw new BoxException("Error updating file: " + e.getMessage(), e);
            } catch (AuthFatalFailureException e) {
                checkTokenState();
                throw new BoxException("Authentication error when updating file: " + e.getMessage(), e);
            }
        }
        return existing;
    }

    BoxFile updateFileContent(String parentId, String id, String name, Calendar modified, InputStream data)
            throws BoxException, NotFoundException, RefreshAccessException {
        try {
            BoxFileUploadRequestObject obj = BoxFileUploadRequestObject.uploadFileRequestObject(parentId, name,
                    data);
            obj.setLocalFileLastModifiedAt(modified.getTime());
            obj.put("modified_at", formatDate(modified));
            return client.getFilesManager().uploadNewVersion(id, obj);
        } catch (BoxJSONException e) {
            throw new BoxException("Error uploading new version of file: " + e.getMessage(), e);
        } catch (BoxRestException e) {
            throw new BoxException("Error uploading new version of file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            }
            throw new BoxException("Error uploading new version of file: " + e.getMessage(), e);
        } catch (UnsupportedEncodingException e) {
            throw new BoxException("Error uploading new version of file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when uploading new version of file: " + e.getMessage(), e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BoxException("File " + name + ", new version uploading interrupted.", e);
        }
    }

    /**
     * Update folder name or/and parent and set given modified date. If folder was actually updated (name or/and
     * parent changed) this method return updated folder object or <code>null</code> if folder already exists
     * with such name and parent.
     * 
     * @param parentId {@link String}
     * @param id {@link String}
     * @param name {@link String}
     * @param modified {@link Calendar}
     * @return {@link BoxFolder} of actually changed folder or <code>null</code> if folder already exists with
     *         such name and parent.
     * @throws BoxException
     * @throws NotFoundException
     * @throws RefreshAccessException
     * @throws ConflictException
     */
    BoxFolder updateFolder(String parentId, String id, String name, Calendar modified)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        BoxFolder existing = readFolder(id);
        int attemts = 0;
        boolean nameChanged = !existing.getName().equals(name);
        boolean parentChanged = !existing.getParent().getId().equals(parentId);
        while ((nameChanged || parentChanged) && attemts < 3) {
            attemts++;
            // if name or parent changed - we do actual update, we ignore modified date changes
            // otherwise, if name the same, Box service will respond with error 409 (conflict)
            try {
                BoxFolderRequestObject obj = BoxFolderRequestObject.createFolderRequestObject(name, parentId);
                obj.put("modified_at", formatDate(modified));
                return client.getFoldersManager().updateFolderInfo(id, obj);
            } catch (BoxRestException e) {
                throw new BoxException("Error updating folder: " + e.getMessage(), e);
            } catch (BoxServerException e) {
                int status = getErrorStatus(e);
                if (status == 404 || status == 412) {
                    // not_found or precondition_failed - then item not found
                    throw new NotFoundException("Folder not found " + id, e);
                } else if (status == 409) {
                    // conflict, try again
                    if (attemts < 3) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Folder with the same name as updated already exists " + id
                                    + ". Trying again.");
                        }
                        existing = readFolder(id);
                        nameChanged = !existing.getName().equalsIgnoreCase(name);
                        parentChanged = !existing.getParent().getId().equals(parentId);
                    } else {
                        throw new ConflictException("Folder with the same name as updated already exists " + id);
                    }
                } else {
                    throw new BoxException("Error updating folder: " + e.getMessage(), e);
                }
            } catch (UnsupportedEncodingException e) {
                throw new BoxException("Error updating folder: " + e.getMessage(), e);
            } catch (AuthFatalFailureException e) {
                checkTokenState();
                throw new BoxException("Authentication error when updating folder: " + e.getMessage(), e);
            }
        }
        return existing;
    }

    /**
     * Copy file to a new one. If file was successfully copied this method return new file object.
     * 
     * 
     * @param id {@link String}
     * @param parentId {@link String}
     * @param name {@link String}
     * @param modified {@link Calendar}
     * @return {@link BoxFile} of actually copied file.
     * @throws BoxException
     * @throws NotFoundException
     * @throws RefreshAccessException
     * @throws ConflictException
     */
    BoxFile copyFile(String id, String parentId, String name)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            BoxItemCopyRequestObject obj = BoxItemCopyRequestObject.copyItemRequestObject(parentId);
            // obj.setIfMatch(etag); // TODO use it
            obj.setName(name);
            return client.getFilesManager().copyFile(id, obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error copying file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            } else if (status == 409) {
                // conflict, try again
                if (LOG.isDebugEnabled()) {
                    LOG.debug("File with the same name as copying already exists " + id + ". Trying again.");
                }
                throw new ConflictException("File with the same name as copying already exists " + id);
            }
            throw new BoxException("Error copying file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when copying file: " + e.getMessage(), e);
        }
    }

    /**
     * Copy folder to a new one. If folder was successfully copied this method return new folder object.
     * 
     * @param id {@link String}
     * @param parentId {@link String}
     * @param name {@link String}
     * @return {@link BoxFile} of actually copied folder.
     * @throws BoxException
     * @throws NotFoundException
     * @throws RefreshAccessException
     * @throws ConflictException
     */
    BoxFolder copyFolder(String id, String parentId, String name)
            throws BoxException, NotFoundException, RefreshAccessException, ConflictException {
        try {
            BoxItemCopyRequestObject obj = BoxItemCopyRequestObject.copyItemRequestObject(parentId);
            // obj.setIfMatch(etag); // TODO use it
            obj.setName(name);
            return client.getFoldersManager().copyFolder(id, obj);
        } catch (BoxRestException e) {
            throw new BoxException("Error copying folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("Folder not found " + id, e);
            } else if (status == 409) {
                // conflict, try again
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Folder with the same name as copying already exists " + id + ". Trying again.");
                }
                throw new ConflictException("Folder with the same name as copying already exists " + id);
            }
            throw new BoxException("Error copying folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when copying folder: " + e.getMessage(), e);
        }
    }

    BoxFile readFile(String id) throws BoxException, NotFoundException, RefreshAccessException {
        try {
            return client.getFilesManager().getFile(id, new BoxDefaultRequestObject());
        } catch (BoxRestException e) {
            throw new BoxException("Error reading file: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("File not found " + id, e);
            }
            throw new BoxException("Error reading file: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when reading file: " + e.getMessage(), e);
        }
    }

    BoxFolder readFolder(String id) throws BoxException, NotFoundException, RefreshAccessException {
        try {
            return client.getFoldersManager().getFolder(id, new BoxDefaultRequestObject());
        } catch (BoxRestException e) {
            throw new BoxException("Error reading folder: " + e.getMessage(), e);
        } catch (BoxServerException e) {
            int status = getErrorStatus(e);
            if (status == 404 || status == 412) {
                // not_found or precondition_failed - then item not found
                throw new NotFoundException("Folder not found " + id, e);
            }
            throw new BoxException("Error reading folder: " + e.getMessage(), e);
        } catch (AuthFatalFailureException e) {
            checkTokenState();
            throw new BoxException("Authentication error when reading folder: " + e.getMessage(), e);
        }
    }

    /**
     * Current user's enterprise name. Can be <code>null</code> if user doesn't belong to any enterprise.
     * 
     * @return {@link String} user's enterprise name or <code>null</code>
     */
    String getEnterpriseName() {
        return enterpriseName;
    }

    /**
     * Current user's enterprise ID. Can be <code>null</code> if user doesn't belong to any enterprise.
     * 
     * @return {@link String} user's enterprise ID or <code>null</code>
     */
    String getEnterpriseId() {
        return enterpriseId;
    }

    /**
     * Current user's custom domain (actual for enterprise users). Can be <code>null</code> if user doesn't have
     * a custom domain.
     * 
     * @return {@link String} user's custom domain or <code>null</code>
     */
    String getCustomDomain() {
        return customDomain;
    }

    // ********* internal *********

    /**
     * Find server error status.
     * 
     * @param e {@link BoxServerException}
     * @return int
     */
    private int getErrorStatus(BoxServerException e) {
        BoxServerError se = e.getError();
        if (se != null) {
            int status = se.getStatus();
            if (status != 0) {
                return status;
            }
        }
        return e.getStatusCode();
    }

    /**
     * Check if need new access token from user (refresh token already expired).
     * 
     * @throws RefreshAccessException if client failed to refresh the access token and need new new token
     */
    private void checkTokenState() throws RefreshAccessException {
        if (OAuthTokenState.FAIL.equals(client.getAuthState())) {
            // we need new access token (refresh token already expired here)
            throw new RefreshAccessException("Authentication failure. Reauthenticate.");
        }
    }

    private void initUser() throws BoxException, RefreshAccessException, NotFoundException {
        com.box.boxjavalibv2.dao.BoxUser user = getCurrentUser();
        String avatarUrl = user.getAvatarUrl();

        Matcher m = BOX_URL_CUSTOM_PATTERN.matcher(avatarUrl);
        if (m.matches()) {
            // we have custom domain (actual for Enterprise users)
            customDomain = m.group(1);
        }

        BoxEnterprise enterprise = user.getEnterprise();
        if (enterprise != null) {
            enterpriseName = enterprise.getName();
            enterpriseId = enterprise.getExtraData("id").toString();
        }
    }

    /**
     * Correct file link for enterprise users with the enterprise custom domain (if it present).
     * 
     * @param fileLink {@link String}
     * @return file link optionally with added custom domain.
     */
    private String link(String fileLink) {
        if (customDomain != null) {
            Matcher m = BOX_URL_MAKE_CUSTOM_PATTERN.matcher(fileLink);
            if (m.matches()) {
                // we need add custom domain to the link host name
                return m.replaceFirst("$1://" + customDomain + ".$2");
            } // else, link already starts with custom domain (actual for Enterprise users)
        } // else, custom domain not available

        return fileLink;
    }
}