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.BlobManager.BlobInfo;
036import org.nuxeo.ecm.core.blob.BlobManager.UsageHint;
037import org.nuxeo.ecm.core.blob.BlobProvider;
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 implements BlobProvider, BatchUpdateBlobProvider {
068
069    private static final Log log = LogFactory.getLog(DropboxBlobProvider.class);
070
071    public static final String PREFIX = "dropbox";
072
073    private static final String APPLICATION_NAME = "Nuxeo/0";
074
075    private static final String FILE_CACHE_NAME = "dropbox";
076
077    private static final String DROPBOX_DOCUMENT_TO_BE_UPDATED_PP = "dropbox_document_to_be_updated";
078
079    /**
080     * {@link DbxEntry.File} resource cache
081     */
082    private Cache fileCache;
083
084    @Override
085    public void initialize(String blobProviderId, Map<String, String> properties) throws IOException {
086    }
087
088    @Override
089    public void close() {
090    }
091
092    @Override
093    public Blob readBlob(BlobInfo blobInfo) throws IOException {
094        return new SimpleManagedBlob(blobInfo);
095    }
096
097    @Override
098    public boolean supportsWrite() {
099        return false;
100    }
101
102    @Override
103    public String writeBlob(Blob blob, Document doc) {
104        throw new UnsupportedOperationException("Writing a blob to Dropbox is not supported");
105    }
106
107    @Override
108    public URI getURI(ManagedBlob blob, UsageHint usage, HttpServletRequest servletRequest) throws IOException {
109        String url = null;
110        String fileInfo = getFileInfo(blob.getKey());
111        String user = getUser(fileInfo);
112        String filePath = getFilePath(fileInfo);
113        DbxClient client = getDropboxClient(getCredential(user));
114        try {
115            switch (usage) {
116            case STREAM:
117                url = client.createTemporaryDirectUrl(filePath).url;
118                break;
119            case DOWNLOAD:
120                url = client.createShareableUrl(filePath);
121                url = url.replace("dl=0", "dl=1"); // enable download flag in url
122                break;
123            case VIEW:
124                url = client.createShareableUrl(filePath);
125                break;
126            }
127        } catch (DbxException e) {
128            throw new IOException("Failed to get Dropbox file URI " + e);
129        }
130        return url != null ? asURI(url) : null;
131    }
132
133    protected InputStream getStream(URI uri) throws IOException {
134        return doGet(uri);
135    }
136
137    @Override
138    public Map<String, URI> getAvailableConversions(ManagedBlob blob, UsageHint hint) throws IOException {
139        return Collections.emptyMap();
140    }
141
142    @Override
143    public InputStream getThumbnail(ManagedBlob blob) throws IOException {
144        String fileInfo = getFileInfo(blob.getKey());
145        String user = getUser(fileInfo);
146        String filePath = getFilePath(fileInfo);
147        try {
148            DbxClient.Downloader downloader = getDropboxClient(getCredential(user)).startGetThumbnail(DbxThumbnailSize.w64h64,
149                DbxThumbnailFormat.bestForFileName(filePath, DbxThumbnailFormat.JPEG), filePath, null);
150
151            if (downloader == null) {
152                return null;
153            }
154            return downloader.body;
155        } catch (DbxException e) {
156            throw new IOException("Failed to get Dropbox file thumbnail " + e);
157        }
158    }
159
160    @Override
161    public InputStream getStream(ManagedBlob blob) throws IOException {
162        URI uri = getURI(blob, UsageHint.STREAM, null);
163        return uri == null ? null : getStream(uri);
164    }
165
166    @Override
167    public InputStream getConvertedStream(ManagedBlob blob, String mimeType, DocumentModel doc) throws IOException {
168        Map<String, URI> conversions = getAvailableConversions(blob, UsageHint.STREAM);
169        URI uri = conversions.get(mimeType);
170        if (uri == null) {
171            return null;
172        }
173        return getStream(uri);
174    }
175
176    @Override
177    public ManagedBlob freezeVersion(ManagedBlob blob, Document doc) throws IOException {
178        return null;
179    }
180
181    /**
182     * Gets the blob for a Dropbox file.
183     *
184     * @param fileInfo the file info ({email}:{filePath})
185     * @return the blob
186     */
187    protected Blob getBlob(String fileInfo) throws IOException {
188        String user = getUser(fileInfo);
189        String filePath = getFilePath(fileInfo);
190        DbxEntry.File file = getFile(user, filePath);
191        String key = String.format("%s:%s:%s", PREFIX, user, filePath);
192        BlobInfo blobInfo = new BlobInfo();
193        blobInfo.key = key;
194        blobInfo.filename = file.name;
195        blobInfo.length = file.numBytes;
196        blobInfo.mimeType = getMimetypeFromFilename(file.name);
197        blobInfo.encoding = null;
198        blobInfo.digest = file.rev;
199        return new SimpleManagedBlob(blobInfo);
200    }
201
202    /**
203     * Removes the prefix from the key.
204     */
205    protected String getFileInfo(String key) {
206        int colon = key.indexOf(':');
207        if (colon < 0) {
208            throw new IllegalArgumentException(key);
209        }
210        String fileInfo = key.substring(colon + 1);
211        return fileInfo;
212    }
213
214    protected String getUser(String fileInfo) {
215        return getFileInfoParts(fileInfo)[0];
216    }
217
218    protected String getFilePath(String fileInfo) {
219        return getFileInfoParts(fileInfo)[1];
220    }
221
222    protected String[] getFileInfoParts(String fileInfo) {
223        String[] parts = fileInfo.split(":");
224        if (parts.length != 2) {
225            throw new IllegalArgumentException(fileInfo);
226        }
227        return parts;
228    }
229
230    protected Credential getCredential(String user) throws IOException {
231        return getCredentialFactory().build(user);
232    }
233
234    protected OAuthCredentialFactory getCredentialFactory() {
235        return new OAuthCredentialFactory(getOAuth2Provider());
236    }
237
238    protected DbxClient getDropboxClient(Credential credential) throws IOException {
239        return getDropboxClient(credential.getAccessToken());
240    }
241
242    protected DbxClient getDropboxClient(String accessToken) throws IOException {
243        DbxRequestConfig config = new DbxRequestConfig(APPLICATION_NAME, Locale.getDefault().toString());
244        return new DbxClient(config, accessToken);
245    }
246
247    /**
248     * Retrieves and caches a {@link DbxEntry.File} resource.
249     *
250     * @return a {@link DbxEntry.File} resource.
251     */
252    protected DbxEntry.File getFile(String user, String filePath) throws IOException {
253        DbxEntry.File fileResource = (DbxEntry.File) getFileCache().get(filePath);
254        if (fileResource == null) {
255            try {
256                DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath);
257                if (fileMetadata == null) {
258                    return null;
259                }
260                fileResource = fileMetadata.asFile();
261                getFileCache().put(filePath, fileResource);
262            } catch (DbxException e) {
263                throw new IOException("Failed to get Dropbox file metadata " + e);
264            }
265        }
266        return fileResource;
267    }
268
269    /**
270     * Executes a GET request
271     */
272    protected InputStream doGet(URI url) throws IOException {
273        HttpRequestFactory requestFactory = getOAuth2Provider().getRequestFactory();
274        HttpResponse response = requestFactory.buildGetRequest(new GenericUrl(url)).execute();
275        return response.getContent();
276    }
277
278    /**
279     * Parse a {@link URI}.
280     *
281     * @return the {@link URI} or null if it fails
282     */
283    protected static URI asURI(String link) {
284        try {
285            return new URI(link);
286        } catch (URISyntaxException e) {
287            log.error("Invalid URI: " + link, e);
288            return null;
289        }
290    }
291
292    protected String getClientId() {
293        OAuth2ServiceProvider provider = getOAuth2Provider();
294        return provider != null ? provider.getClientId() : null;
295    }
296
297    private Cache getFileCache() {
298        if (fileCache == null) {
299            fileCache = Framework.getService(CacheService.class).getCache(FILE_CACHE_NAME);
300        }
301        return fileCache;
302    }
303
304    protected DropboxOAuth2ServiceProvider getOAuth2Provider() {
305        return (DropboxOAuth2ServiceProvider) Framework.getLocalService(
306            OAuth2ServiceProviderRegistry.class).getProvider(PREFIX);
307    }
308
309    private String getMimetypeFromFilename(String filename) {
310        MimetypeRegistryService mimetypeRegistryService = (MimetypeRegistryService) Framework.getLocalService(
311            MimetypeRegistry.class);
312        return mimetypeRegistryService.getMimetypeFromFilename(filename);
313    }
314
315    @Override
316    public List<DocumentModel> checkChangesAndUpdateBlob(List<DocumentModel> docs) {
317        List<DocumentModel> changedDocuments = new ArrayList<>();
318        for (DocumentModel doc : docs) {
319            final SimpleManagedBlob blob = (SimpleManagedBlob) doc.getProperty("content").getValue();
320            if (blob == null) {
321                continue;
322            }
323            if (isVersion(blob)) {
324                continue;
325            }
326            String fileInfo = getFileInfo(blob.key);
327            String user = getUser(fileInfo);
328            String filePath = getFilePath(fileInfo);
329            try {
330                DbxEntry.File file = getFileNoCache(user, filePath);
331                if (StringUtils.isBlank(blob.getDigest()) || !blob.getDigest().equals(file.rev)) {
332                    log.trace("Updating " + blob.key);
333                    getFileCache().invalidate(filePath);
334                    doc.setPropertyValue("content", (SimpleManagedBlob) getBlob(fileInfo));
335                    changedDocuments.add(doc);
336                }
337
338            } catch (DbxException | IOException e) {
339                log.error("Could not update dropbox document " + filePath, e);
340            }
341        }
342        return changedDocuments;
343    }
344
345    protected DbxEntry.File getFileNoCache(String user, String filePath) throws DbxException, IOException {
346        DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath);
347        if (fileMetadata == null) {
348            return null;
349        }
350        DbxEntry.File file = fileMetadata.asFile();
351        return file;
352    }
353
354    @Override
355    public String getPageProviderNameForUpdate() {
356        return DROPBOX_DOCUMENT_TO_BE_UPDATED_PP;
357    }
358
359    @Override
360    public String getBlobPrefix() {
361        return PREFIX;
362    }
363
364}