001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Andre Justo
016 */
017package org.nuxeo.ecm.liveconnect.dropbox;
018
019import com.dropbox.core.DbxClient;
020import com.dropbox.core.DbxEntry;
021import com.dropbox.core.DbxException;
022import com.dropbox.core.DbxRequestConfig;
023import com.dropbox.core.DbxThumbnailFormat;
024import com.dropbox.core.DbxThumbnailSize;
025import com.google.api.client.auth.oauth2.Credential;
026import com.google.api.client.http.GenericUrl;
027import com.google.api.client.http.HttpRequestFactory;
028import com.google.api.client.http.HttpResponse;
029
030import org.apache.commons.lang3.StringUtils;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.ecm.core.api.Blob;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.blob.AbstractBlobProvider;
036import org.nuxeo.ecm.core.blob.BlobManager.BlobInfo;
037import org.nuxeo.ecm.core.blob.BlobManager.UsageHint;
038import org.nuxeo.ecm.core.blob.ManagedBlob;
039import org.nuxeo.ecm.core.blob.SimpleManagedBlob;
040import org.nuxeo.ecm.core.cache.Cache;
041import org.nuxeo.ecm.core.cache.CacheService;
042import org.nuxeo.ecm.core.model.Document;
043import org.nuxeo.ecm.liveconnect.update.BatchUpdateBlobProvider;
044import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
045import org.nuxeo.ecm.platform.mimetype.service.MimetypeRegistryService;
046import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider;
047import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProviderRegistry;
048import org.nuxeo.runtime.api.Framework;
049
050import java.io.IOException;
051import java.io.InputStream;
052import java.net.URI;
053import java.net.URISyntaxException;
054import java.util.ArrayList;
055import java.util.Collections;
056import java.util.List;
057import java.util.Locale;
058import java.util.Map;
059
060import javax.servlet.http.HttpServletRequest;
061
062/**
063 * Provider for blobs getting information from Dropbox.
064 *
065 * @since 7.3
066 */
067public class DropboxBlobProvider extends AbstractBlobProvider implements BatchUpdateBlobProvider {
068
069    private static final Log log = LogFactory.getLog(DropboxBlobProvider.class);
070
071    private static final String APPLICATION_NAME = "Nuxeo/0";
072
073    private static final String FILE_CACHE_NAME = "dropbox";
074
075    private static final String DROPBOX_DOCUMENT_TO_BE_UPDATED_PP = "dropbox_document_to_be_updated";
076
077    /**
078     * {@link DbxEntry.File} resource cache
079     */
080    private Cache fileCache;
081
082    @Override
083    public void close() {
084    }
085
086    @Override
087    public Blob readBlob(BlobInfo blobInfo) throws IOException {
088        return new SimpleManagedBlob(blobInfo);
089    }
090
091    @Override
092    public boolean supportsUserUpdate() {
093        return supportsUserUpdateDefaultTrue();
094    }
095
096    @Override
097    public String writeBlob(Blob blob, Document doc) {
098        throw new UnsupportedOperationException("Writing a blob to Dropbox is not supported");
099    }
100
101    @Override
102    public URI getURI(ManagedBlob blob, UsageHint usage, HttpServletRequest servletRequest) throws IOException {
103        String url = null;
104        String fileInfo = getFileInfo(blob.getKey());
105        String user = getUser(fileInfo);
106        String filePath = getFilePath(fileInfo);
107        DbxClient client = getDropboxClient(getCredential(user));
108        try {
109            switch (usage) {
110            case STREAM:
111                url = client.createTemporaryDirectUrl(filePath).url;
112                break;
113            case DOWNLOAD:
114                url = client.createShareableUrl(filePath);
115                url = url.replace("dl=0", "dl=1"); // enable download flag in url
116                break;
117            case VIEW:
118                url = client.createShareableUrl(filePath);
119                break;
120            }
121        } catch (DbxException e) {
122            throw new IOException("Failed to get Dropbox file URI " + e);
123        }
124        return url != null ? asURI(url) : null;
125    }
126
127    protected InputStream getStream(URI uri) throws IOException {
128        return doGet(uri);
129    }
130
131    @Override
132    public Map<String, URI> getAvailableConversions(ManagedBlob blob, UsageHint hint) throws IOException {
133        return Collections.emptyMap();
134    }
135
136    @Override
137    public InputStream getThumbnail(ManagedBlob blob) throws IOException {
138        String fileInfo = getFileInfo(blob.getKey());
139        String user = getUser(fileInfo);
140        String filePath = getFilePath(fileInfo);
141        try {
142            DbxClient.Downloader downloader = getDropboxClient(getCredential(user)).startGetThumbnail(DbxThumbnailSize.w64h64,
143                DbxThumbnailFormat.bestForFileName(filePath, DbxThumbnailFormat.JPEG), filePath, null);
144
145            if (downloader == null) {
146                return null;
147            }
148            return downloader.body;
149        } catch (DbxException e) {
150            throw new IOException("Failed to get Dropbox file thumbnail " + e);
151        }
152    }
153
154    @Override
155    public InputStream getStream(ManagedBlob blob) throws IOException {
156        URI uri = getURI(blob, UsageHint.STREAM, null);
157        return uri == null ? null : getStream(uri);
158    }
159
160    @Override
161    public InputStream getConvertedStream(ManagedBlob blob, String mimeType, DocumentModel doc) throws IOException {
162        Map<String, URI> conversions = getAvailableConversions(blob, UsageHint.STREAM);
163        URI uri = conversions.get(mimeType);
164        if (uri == null) {
165            return null;
166        }
167        return getStream(uri);
168    }
169
170    @Override
171    public ManagedBlob freezeVersion(ManagedBlob blob, Document doc) throws IOException {
172        return null;
173    }
174
175    /**
176     * Gets the blob for a Dropbox file.
177     *
178     * @param fileInfo the file info ({email}:{filePath})
179     * @return the blob
180     */
181    protected Blob getBlob(String fileInfo) throws IOException {
182        String user = getUser(fileInfo);
183        String filePath = getFilePath(fileInfo);
184        DbxEntry.File file = getFile(user, filePath);
185        String key = String.format("%s:%s:%s", blobProviderId, user, filePath);
186        BlobInfo blobInfo = new BlobInfo();
187        blobInfo.key = key;
188        blobInfo.filename = file.name;
189        blobInfo.length = file.numBytes;
190        blobInfo.mimeType = getMimetypeFromFilename(file.name);
191        blobInfo.encoding = null;
192        blobInfo.digest = file.rev;
193        return new SimpleManagedBlob(blobInfo);
194    }
195
196    /**
197     * Removes the prefix from the key.
198     */
199    protected String getFileInfo(String key) {
200        int colon = key.indexOf(':');
201        if (colon < 0) {
202            throw new IllegalArgumentException(key);
203        }
204        String fileInfo = key.substring(colon + 1);
205        return fileInfo;
206    }
207
208    protected String getUser(String fileInfo) {
209        return getFileInfoParts(fileInfo)[0];
210    }
211
212    protected String getFilePath(String fileInfo) {
213        return getFileInfoParts(fileInfo)[1];
214    }
215
216    protected String[] getFileInfoParts(String fileInfo) {
217        String[] parts = fileInfo.split(":");
218        if (parts.length != 2) {
219            throw new IllegalArgumentException(fileInfo);
220        }
221        return parts;
222    }
223
224    protected Credential getCredential(String user) throws IOException {
225        return getCredentialFactory().build(user);
226    }
227
228    protected OAuthCredentialFactory getCredentialFactory() {
229        return new OAuthCredentialFactory(getOAuth2Provider());
230    }
231
232    protected DbxClient getDropboxClient(Credential credential) throws IOException {
233        return getDropboxClient(credential.getAccessToken());
234    }
235
236    protected DbxClient getDropboxClient(String accessToken) throws IOException {
237        DbxRequestConfig config = new DbxRequestConfig(APPLICATION_NAME, Locale.getDefault().toString());
238        return new DbxClient(config, accessToken);
239    }
240
241    /**
242     * Retrieves and caches a {@link DbxEntry.File} resource.
243     *
244     * @return a {@link DbxEntry.File} resource.
245     */
246    protected DbxEntry.File getFile(String user, String filePath) throws IOException {
247        DbxEntry.File fileResource = (DbxEntry.File) getFileCache().get(filePath);
248        if (fileResource == null) {
249            try {
250                DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath);
251                if (fileMetadata == null) {
252                    return null;
253                }
254                fileResource = fileMetadata.asFile();
255                getFileCache().put(filePath, fileResource);
256            } catch (DbxException e) {
257                throw new IOException("Failed to get Dropbox file metadata " + e);
258            }
259        }
260        return fileResource;
261    }
262
263    /**
264     * Executes a GET request
265     */
266    protected InputStream doGet(URI url) throws IOException {
267        HttpRequestFactory requestFactory = getOAuth2Provider().getRequestFactory();
268        HttpResponse response = requestFactory.buildGetRequest(new GenericUrl(url)).execute();
269        return response.getContent();
270    }
271
272    /**
273     * Parse a {@link URI}.
274     *
275     * @return the {@link URI} or null if it fails
276     */
277    protected static URI asURI(String link) {
278        try {
279            return new URI(link);
280        } catch (URISyntaxException e) {
281            log.error("Invalid URI: " + link, e);
282            return null;
283        }
284    }
285
286    protected String getClientId() {
287        OAuth2ServiceProvider provider = getOAuth2Provider();
288        return provider != null ? provider.getClientId() : null;
289    }
290
291    private Cache getFileCache() {
292        if (fileCache == null) {
293            fileCache = Framework.getService(CacheService.class).getCache(FILE_CACHE_NAME);
294        }
295        return fileCache;
296    }
297
298    protected DropboxOAuth2ServiceProvider getOAuth2Provider() {
299        return (DropboxOAuth2ServiceProvider) Framework.getLocalService(
300            OAuth2ServiceProviderRegistry.class).getProvider(blobProviderId);
301    }
302
303    private String getMimetypeFromFilename(String filename) {
304        MimetypeRegistryService mimetypeRegistryService = (MimetypeRegistryService) Framework.getLocalService(
305            MimetypeRegistry.class);
306        return mimetypeRegistryService.getMimetypeFromFilename(filename);
307    }
308
309    @Override
310    public List<DocumentModel> checkChangesAndUpdateBlob(List<DocumentModel> docs) {
311        List<DocumentModel> changedDocuments = new ArrayList<>();
312        for (DocumentModel doc : docs) {
313            final SimpleManagedBlob blob = (SimpleManagedBlob) doc.getProperty("content").getValue();
314            if (blob == null) {
315                continue;
316            }
317            if (isVersion(blob)) {
318                continue;
319            }
320            String fileInfo = getFileInfo(blob.key);
321            String user = getUser(fileInfo);
322            String filePath = getFilePath(fileInfo);
323            try {
324                DbxEntry.File file = getFileNoCache(user, filePath);
325                if (StringUtils.isBlank(blob.getDigest()) || !blob.getDigest().equals(file.rev)) {
326                    log.trace("Updating " + blob.key);
327                    getFileCache().invalidate(filePath);
328                    doc.setPropertyValue("content", (SimpleManagedBlob) getBlob(fileInfo));
329                    changedDocuments.add(doc);
330                }
331
332            } catch (DbxException | IOException e) {
333                log.error("Could not update dropbox document " + filePath, e);
334            }
335        }
336        return changedDocuments;
337    }
338
339    protected DbxEntry.File getFileNoCache(String user, String filePath) throws DbxException, IOException {
340        DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath);
341        if (fileMetadata == null) {
342            return null;
343        }
344        DbxEntry.File file = fileMetadata.asFile();
345        return file;
346    }
347
348    @Override
349    public String getPageProviderNameForUpdate() {
350        return DROPBOX_DOCUMENT_TO_BE_UPDATED_PP;
351    }
352
353    @Override
354    public String getBlobProviderId() {
355        return blobProviderId;
356    }
357
358}