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 * Florent Guillaume 016 * Nelson Silva 017 */ 018package org.nuxeo.ecm.liveconnect.google.drive; 019 020import static java.lang.Boolean.TRUE; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.StringReader; 025import java.net.URI; 026import java.net.URISyntaxException; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032 033import javax.servlet.http.HttpServletRequest; 034 035import com.google.api.client.http.HttpResponseException; 036import com.google.api.client.http.HttpStatusCodes; 037import org.apache.commons.lang.StringUtils; 038import org.apache.commons.logging.Log; 039import org.apache.commons.logging.LogFactory; 040import org.nuxeo.ecm.core.api.Blob; 041import org.nuxeo.ecm.core.api.Blobs; 042import org.nuxeo.ecm.core.api.DocumentModel; 043import org.nuxeo.ecm.core.api.NuxeoException; 044import org.nuxeo.ecm.core.api.model.impl.ListProperty; 045import org.nuxeo.ecm.core.blob.BlobManager.BlobInfo; 046import org.nuxeo.ecm.core.blob.BlobManager.UsageHint; 047import org.nuxeo.ecm.core.blob.BlobProvider; 048import org.nuxeo.ecm.core.blob.ManagedBlob; 049import org.nuxeo.ecm.core.blob.SimpleManagedBlob; 050import org.nuxeo.ecm.core.blob.apps.AppLink; 051import org.nuxeo.ecm.core.cache.Cache; 052import org.nuxeo.ecm.core.cache.CacheService; 053import org.nuxeo.ecm.core.model.Document; 054import org.nuxeo.ecm.liveconnect.google.drive.credential.CredentialFactory; 055import org.nuxeo.ecm.liveconnect.google.drive.credential.OAuth2CredentialFactory; 056import org.nuxeo.ecm.liveconnect.google.drive.credential.ServiceAccountCredentialFactory; 057import org.nuxeo.ecm.liveconnect.update.BatchUpdateBlobProvider; 058import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider; 059import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProviderRegistry; 060import org.nuxeo.ecm.platform.usermanager.UserManager; 061import org.nuxeo.runtime.api.Framework; 062 063import com.google.api.client.auth.oauth2.Credential; 064import com.google.api.client.http.GenericUrl; 065import com.google.api.client.http.HttpResponse; 066import com.google.api.client.http.HttpTransport; 067import com.google.api.client.json.JsonFactory; 068import com.google.api.client.json.JsonObjectParser; 069import com.google.api.client.json.jackson2.JacksonFactory; 070import com.google.api.client.util.ObjectParser; 071import com.google.api.services.drive.Drive; 072import com.google.api.services.drive.DriveRequest; 073import com.google.api.services.drive.model.App; 074import com.google.api.services.drive.model.File; 075import com.google.api.services.drive.model.Revision; 076import com.google.api.services.drive.model.RevisionList; 077import org.nuxeo.runtime.transaction.TransactionHelper; 078 079/** 080 * Provider for blobs getting information from Google Drive. 081 * 082 * @since 7.3 083 */ 084public class GoogleDriveBlobProvider implements BlobProvider, BatchUpdateBlobProvider { 085 086 private static final String GOOGLEDRIVE_DOCUMENT_TO_BE_UPDATED_PP = "googledrive_document_to_be_updated"; 087 088 public static final int PREFERRED_ICON_SIZE = 16; 089 090 private static final Log log = LogFactory.getLog(GoogleDriveBlobProvider.class); 091 092 /** 093 * Information about a file stored in Google Drive. 094 */ 095 public static class FileInfo { 096 public final String user; 097 098 public final String fileId; 099 100 public final String revisionId; 101 102 public FileInfo(String user, String fileId, String revisionId) { 103 this.user = user; 104 this.fileId = fileId; 105 this.revisionId = revisionId; 106 } 107 } 108 109 public static final String PREFIX = "googledrive"; 110 111 private static final String APPLICATION_NAME = "Nuxeo/0"; 112 113 private static final String FILE_CACHE_NAME = "googleDrive"; 114 115 // Service account details 116 public static final String SERVICE_ACCOUNT_ID_PROP = "serviceAccountId"; 117 118 public static final String SERVICE_ACCOUNT_P12_PATH_PROP = "serviceAccountP12Path"; 119 120 // ClientId for the file picker auth 121 public static final String CLIENT_ID_PROP = "clientId"; 122 123 public static final String DEFAULT_EXPORT_MIMETYPE = "application/pdf"; 124 125 // Blob conversion constants 126 protected static final String BLOB_CONVERSIONS_FACET = "BlobConversions"; 127 128 protected static final String BLOB_CONVERSIONS_PROPERTY = "blobconversions:conversions"; 129 130 protected static final String BLOB_CONVERSION_KEY = "key"; 131 132 protected static final String BLOB_CONVERSION_BLOB = "blob"; 133 134 protected static final ObjectParser JSON_PARSER = new JsonObjectParser(JacksonFactory.getDefaultInstance()); 135 136 private String serviceAccountId; 137 138 private java.io.File serviceAccountP12File; 139 140 private String clientId; 141 142 /** resource cache */ 143 private Cache cache; 144 145 @Override 146 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 147 if (!PREFIX.equals(blobProviderId)) { 148 // TODO avoid this by passing a parameter to the GoogleDriveBlobUploader when constructed 149 throw new IllegalArgumentException("Must be registered for name: " + PREFIX + ", not: " + blobProviderId); 150 } 151 // Validate service account configuration 152 serviceAccountId = properties.get(SERVICE_ACCOUNT_ID_PROP); 153 if (StringUtils.isBlank(serviceAccountId)) { 154 return; 155 } 156 String p12 = properties.get(SERVICE_ACCOUNT_P12_PATH_PROP); 157 if (StringUtils.isBlank(p12)) { 158 throw new NuxeoException("Missing value for property: " + SERVICE_ACCOUNT_P12_PATH_PROP); 159 } 160 serviceAccountP12File = new java.io.File(p12); 161 if (!serviceAccountP12File.exists()) { 162 throw new NuxeoException("No such file: " + p12 + " for property: " + SERVICE_ACCOUNT_P12_PATH_PROP); 163 } 164 165 clientId = properties.get(CLIENT_ID_PROP); 166 if (StringUtils.isBlank(clientId)) { 167 throw new NuxeoException("Missing value for property: " + CLIENT_ID_PROP); 168 } 169 } 170 171 @Override 172 public void close() { 173 } 174 175 @Override 176 public Blob readBlob(BlobInfo blobInfo) { 177 return new SimpleManagedBlob(blobInfo); 178 } 179 180 @Override 181 public boolean supportsWrite() { 182 return false; 183 } 184 185 @Override 186 public String writeBlob(Blob blob, Document doc) { 187 throw new UnsupportedOperationException("Writing a blob to Google Drive is not supported"); 188 } 189 190 @Override 191 public URI getURI(ManagedBlob blob, UsageHint usage, HttpServletRequest servletRequest) throws IOException { 192 String url = null; 193 switch (usage) { 194 case STREAM: 195 url = getStreamUrl(blob); 196 break; 197 case DOWNLOAD: 198 url = getDownloadUrl(blob); 199 break; 200 case VIEW: 201 case EDIT: 202 url = getAlternateUrl(blob); 203 break; 204 case EMBED: 205 url = getEmbedUrl(blob); 206 break; 207 } 208 return url == null ? null : asURI(url); 209 } 210 211 // TODO remove unused hint from signature 212 @Override 213 public Map<String, URI> getAvailableConversions(ManagedBlob blob, UsageHint hint) throws IOException { 214 Map<String, String> exportLinks = getExportLinks(blob); 215 if (exportLinks == null) { 216 return Collections.emptyMap(); 217 } 218 Map<String, URI> conversions = new HashMap<>(); 219 for (String mimeType : exportLinks.keySet()) { 220 conversions.put(mimeType, asURI(exportLinks.get(mimeType))); 221 } 222 return conversions; 223 } 224 225 @Override 226 public InputStream getThumbnail(ManagedBlob blob) throws IOException { 227 String url = getThumbnailUrl(blob); 228 return getStream(blob, asURI(url)); 229 } 230 231 /** 232 * Gets the URL from which we can stream the content of the file. 233 * <p> 234 * Will return {@code null} if this is a native Google document. 235 */ 236 protected String getStreamUrl(ManagedBlob blob) throws IOException { 237 FileInfo fileInfo = getFileInfo(blob); 238 if (fileInfo.revisionId == null) { 239 File file = getFile(fileInfo); 240 return file.getDownloadUrl(); 241 } else { 242 Revision revision = getRevision(fileInfo); 243 return revision != null ? revision.getDownloadUrl() : null; 244 } 245 } 246 247 /** 248 * Gets the URL to which we can redirect to let the user download the file. 249 */ 250 protected String getDownloadUrl(ManagedBlob blob) throws IOException { 251 FileInfo fileInfo = getFileInfo(blob); 252 String url = null; 253 if (fileInfo.revisionId == null) { 254 File file = getFile(fileInfo); 255 url = file.getWebContentLink(); 256 if (url == null) { 257 // native Google document 258 url = file.getAlternateLink(); 259 } 260 } else { 261 Revision revision = getRevision(fileInfo); 262 if (revision != null) { 263 url = revision.getDownloadUrl(); 264 if (StringUtils.isBlank(url)) { 265 url = revision.getExportLinks().get(DEFAULT_EXPORT_MIMETYPE); 266 } 267 // hack, without this we get a 401 on the returned URL... 268 if (url.endsWith("&gd=true")) { 269 url = url.substring(0, url.length() - "&gd=true".length()); 270 } 271 } 272 } 273 return url; 274 } 275 276 // TODO remove 277 protected String getAlternateUrl(ManagedBlob blob) throws IOException { 278 FileInfo fileInfo = getFileInfo(blob); 279 // ignore revisionId 280 File file = getFile(fileInfo); 281 return file.getAlternateLink(); 282 } 283 284 /** 285 * Gets the URL to which we can redirect to let the user see a preview of the file. 286 */ 287 protected String getEmbedUrl(ManagedBlob blob) throws IOException { 288 FileInfo fileInfo = getFileInfo(blob); 289 // ignore revisionId 290 File file = getFile(fileInfo); 291 String url = file.getEmbedLink(); 292 if (url == null) { 293 // uploaded file, switch to preview 294 url = file.getAlternateLink(); 295 url = asURI(url).resolve("./preview").toString(); 296 } 297 return url; 298 } 299 300 /** 301 * Gets the URL from which we can stream a thumbnail. 302 */ 303 protected String getThumbnailUrl(ManagedBlob blob) throws IOException { 304 FileInfo fileInfo = getFileInfo(blob); 305 // ignore revisionId 306 File file = getFile(fileInfo); 307 return file.getThumbnailLink(); 308 } 309 310 /** 311 * Gets the export link. 312 */ 313 protected Map<String, String> getExportLinks(ManagedBlob blob) throws IOException { 314 FileInfo fileInfo = getFileInfo(blob); 315 if (fileInfo.revisionId == null) { 316 File file = getFile(fileInfo); 317 return file.getExportLinks(); 318 } else { 319 Revision revision = getRevision(fileInfo); 320 return revision != null && TRUE.equals(revision.getPinned()) ? 321 revision.getExportLinks() : Collections.emptyMap(); 322 } 323 } 324 325 @Override 326 public InputStream getStream(ManagedBlob blob) throws IOException { 327 URI uri = getURI(blob, UsageHint.STREAM, null); 328 return uri == null ? null : getStream(blob, uri); 329 } 330 331 @Override 332 public InputStream getConvertedStream(ManagedBlob blob, String mimeType, DocumentModel doc) throws IOException { 333 Blob conversion = retrieveBlobConversion(blob, mimeType, doc); 334 if (conversion != null) { 335 return conversion.getStream(); 336 } 337 338 Map<String, URI> conversions = getAvailableConversions(blob, UsageHint.STREAM); 339 URI uri = conversions.get(mimeType); 340 if (uri == null) { 341 return null; 342 } 343 return getStream(blob, uri); 344 } 345 346 protected InputStream getStream(ManagedBlob blob, URI uri) throws IOException { 347 FileInfo fileInfo = getFileInfo(blob); 348 return doGet(fileInfo.user, uri); 349 } 350 351 @Override 352 public List<AppLink> getAppLinks(String username, ManagedBlob blob) throws IOException { 353 List<AppLink> appLinks = new ArrayList<>(); 354 355 FileInfo fileInfo = getFileInfo(blob); 356 357 // application links do not work with revisions 358 if (fileInfo.revisionId != null) { 359 return appLinks; 360 } 361 362 // retrieve the service's user (email in this case) for this username 363 String user = getServiceUser(username); 364 365 // fetch a partial file response 366 File file = getPartialFile(user, fileInfo.fileId, "openWithLinks", "defaultOpenWithLink"); 367 if (file.isEmpty()) { 368 return appLinks; 369 } 370 371 // build the list of AppLinks 372 String defaultLink = file.getDefaultOpenWithLink(); 373 for (Map.Entry<String, String> entry : file.getOpenWithLinks().entrySet()) { 374 // build the AppLink 375 App app = getApp(user, entry.getKey()); 376 AppLink appLink = new AppLink(); 377 appLink.setAppName(app.getName()); 378 appLink.setLink(entry.getValue()); 379 380 // pick an application icon 381 for (com.google.api.services.drive.model.App.Icons icon : app.getIcons()) { 382 if ("application".equals(icon.getCategory())) { 383 appLink.setIcon(icon.getIconUrl()); 384 // break if we've got one with our preferred size 385 if (icon.getSize() == PREFERRED_ICON_SIZE) { 386 break; 387 } 388 } 389 } 390 391 // add the default link first 392 if (defaultLink != null && defaultLink.equals(entry.getValue())) { 393 appLinks.add(0, appLink); 394 } else { 395 appLinks.add(appLink); 396 } 397 } 398 return appLinks; 399 } 400 401 protected String getServiceUser(String username) { 402 CredentialFactory credentialFactory = getCredentialFactory(); 403 if (credentialFactory instanceof OAuth2CredentialFactory) { 404 OAuth2ServiceProvider provider = ((OAuth2CredentialFactory) credentialFactory).getProvider(); 405 return ((GoogleOAuth2ServiceProvider) provider).getServiceUser(username); 406 } else { 407 UserManager userManager = Framework.getLocalService(UserManager.class); 408 DocumentModel user = userManager.getUserModel(username); 409 if (user == null) { 410 return null; 411 } 412 return (String) user.getPropertyValue(userManager.getUserEmailField()); 413 } 414 } 415 416 protected App getApp(String user, String appId) throws IOException { 417 String cacheKey = "app_" + appId; 418 return executeAndCache(cacheKey, getService(user).apps().get(appId), App.class); 419 } 420 421 @Override 422 public ManagedBlob freezeVersion(ManagedBlob blob, Document doc) throws IOException { 423 FileInfo fileInfo = getFileInfo(blob); 424 if (fileInfo.revisionId != null) { 425 // already frozen 426 return null; 427 } 428 String user = fileInfo.user; 429 String fileId = fileInfo.fileId; 430 // find current revision for that doc 431 File file = getFile(fileInfo); 432 String revisionId = file.getHeadRevisionId(); 433 if (revisionId != null) { 434 // uploaded file, there is a head revision 435 fileInfo = new FileInfo(user, fileId, revisionId); 436 Revision revision = getRevision(fileInfo); 437 if (!TRUE.equals(revision.getPinned())) { 438 // pin the revision 439 Revision pinRevision = new Revision(); 440 pinRevision.setPinned(TRUE); 441 getService(user).revisions().patch(fileId, revisionId, pinRevision).executeUnparsed().ignore(); 442 } 443 } else { 444 // native Google document 445 // find last revision 446 List<Revision> list = getRevisionList(fileInfo).getItems(); 447 if (list.isEmpty()) { 448 return null; 449 } 450 Revision revision = list.get(list.size() - 1); 451 452 // native Google document revision cannot be pinned so we store a conversion of the blob 453 URI uri = asURI(revision.getExportLinks().get(DEFAULT_EXPORT_MIMETYPE)); 454 455 InputStream is = doGet(user, uri); 456 Blob conversion = Blobs.createBlob(is); 457 conversion.setFilename(blob.getFilename()); 458 conversion.setMimeType(DEFAULT_EXPORT_MIMETYPE); 459 460 fileInfo = new FileInfo(user, fileId, revision.getId()); 461 462 // store a conversion of this revision 463 storeBlobConversion(doc, getKey(fileInfo), conversion); 464 } 465 return getBlob(fileInfo); 466 } 467 468 /** 469 * Store a conversion of the given blob 470 */ 471 @SuppressWarnings("unchecked") 472 protected void storeBlobConversion(Document doc, String blobKey, Blob blob) { 473 if (!doc.hasFacet(BLOB_CONVERSIONS_FACET)) { 474 doc.addFacet(BLOB_CONVERSIONS_FACET); 475 } 476 477 List<Map<String, Object>> conversions = (List<Map<String, Object>>) doc.getValue(BLOB_CONVERSIONS_PROPERTY); 478 Map<String, Object> conversion = new HashMap<>(); 479 conversion.put(BLOB_CONVERSION_KEY, blobKey); 480 conversion.put(BLOB_CONVERSION_BLOB, blob); 481 conversions.add(conversion); 482 doc.setValue(BLOB_CONVERSIONS_PROPERTY, conversions); 483 } 484 485 /** 486 * Retrieve a stored conversion of the given blob 487 */ 488 protected Blob retrieveBlobConversion(ManagedBlob blob, String mimeType, DocumentModel doc) { 489 if (doc == null || !doc.hasFacet(BLOB_CONVERSIONS_FACET)) { 490 return null; 491 } 492 493 boolean txWasActive = TransactionHelper.isTransactionActiveOrMarkedRollback(); 494 try { 495 if (!txWasActive) { 496 TransactionHelper.startTransaction(); 497 } 498 ListProperty conversions = (ListProperty) doc.getProperty(BLOB_CONVERSIONS_PROPERTY); 499 for (int i = 0; i < conversions.size(); i++) { 500 if (blob.getKey().equals(conversions.get(i).getValue(BLOB_CONVERSION_KEY))) { 501 String conversionXPath = String.format("%s/%d/%s", BLOB_CONVERSIONS_PROPERTY, i, BLOB_CONVERSION_BLOB); 502 Blob conversion = (Blob) doc.getPropertyValue(conversionXPath); 503 if (conversion.getMimeType().equals(mimeType)) { 504 return conversion; 505 } 506 } 507 } 508 } finally { 509 if (!txWasActive) { 510 TransactionHelper.commitOrRollbackTransaction(); 511 } 512 } 513 return null; 514 } 515 516 /** 517 * Gets the blob for a Google Drive file. 518 * 519 * @param fileInfo the file info 520 * @return the blob 521 */ 522 protected ManagedBlob getBlob(FileInfo fileInfo) throws IOException { 523 String key = getKey(fileInfo); 524 File file = getFile(fileInfo); 525 String filename = file.getTitle().replace("/", "-"); 526 BlobInfo blobInfo = new BlobInfo(); 527 blobInfo.key = key; 528 blobInfo.mimeType = file.getMimeType(); 529 blobInfo.encoding = null; // TODO extract from mimeType 530 blobInfo.filename = filename; 531 blobInfo.length = file.getFileSize(); 532 // etag for native Google documents and md5 for everything else 533 String digest = getDigest(file); 534 blobInfo.digest = digest; 535 return new SimpleManagedBlob(blobInfo); 536 } 537 538 protected String getDigest(File file) { 539 String digest = file.getMd5Checksum(); 540 if (digest == null) { 541 digest = file.getEtag(); 542 } 543 return digest; 544 } 545 546 protected boolean isDigestChanged(Blob blob, File file) { 547 final String digest = blob.getDigest(); 548 String md5CheckSum = file.getMd5Checksum(); 549 String eTag = file.getEtag(); 550 if (md5CheckSum != null) { 551 return !md5CheckSum.equals(digest); 552 } else { 553 return eTag != null && !eTag.equals(digest); 554 } 555 } 556 557 protected boolean isFilenameChanged(Blob blob, File file) { 558 return !file.getTitle().replace("/", "-").equals(blob.getFilename()); 559 } 560 561 protected boolean isChanged(Blob blob, File file) { 562 return isFilenameChanged(blob, file) || isDigestChanged(blob, file); 563 } 564 565 /** Adds the prefix to the key. */ 566 protected String getKey(FileInfo fileInfo) { 567 return PREFIX + ':' + fileInfo.user + ':' + fileInfo.fileId 568 + (fileInfo.revisionId == null ? "" : ':' + fileInfo.revisionId); 569 } 570 571 /** Removes the prefix from the key. */ 572 protected FileInfo getFileInfo(ManagedBlob blob) { 573 String key = blob.getKey(); 574 int colon = key.indexOf(':'); 575 if (colon < 0) { 576 throw new IllegalArgumentException(key); 577 } 578 String suffix = key.substring(colon + 1); 579 String[] parts = suffix.split(":"); 580 if (parts.length < 2 || parts.length > 3) { 581 throw new IllegalArgumentException(key); 582 } 583 return new FileInfo(parts[0], parts[1], parts.length < 3 ? null : parts[2]); 584 } 585 586 protected Credential getCredential(String user) throws IOException { 587 return getCredentialFactory().build(user); 588 } 589 590 protected CredentialFactory getCredentialFactory() { 591 OAuth2ServiceProvider provider = Framework.getLocalService(OAuth2ServiceProviderRegistry.class).getProvider( 592 PREFIX); 593 if (provider != null && provider.isEnabled()) { 594 // Web application configuration 595 return new OAuth2CredentialFactory(provider); 596 } else { 597 // Service account configuration 598 return new ServiceAccountCredentialFactory(serviceAccountId, serviceAccountP12File); 599 } 600 } 601 602 protected Drive getService(String user) throws IOException { 603 Credential credential = getCredential(user); 604 if (credential == null) { 605 throw new IOException("No credentials found for user " + user); 606 } 607 HttpTransport httpTransport = credential.getTransport(); 608 JsonFactory jsonFactory = credential.getJsonFactory(); 609 return new Drive.Builder(httpTransport, jsonFactory, credential) // 610 .setApplicationName(APPLICATION_NAME) // set application name to avoid a WARN 611 .build(); 612 } 613 614 /** 615 * Retrieve a partial {@link File} resource. 616 */ 617 protected File getPartialFile(String user, String fileId, String... fields) throws IOException { 618 return getService(user).files().get(fileId).setFields(StringUtils.join(fields, ",")).execute(); 619 } 620 621 /** 622 * Retrieves a {@link File} resource and caches the unparsed response. 623 * 624 * @return a {@link File} resource 625 */ 626 // subclassed for mock 627 protected File getFile(FileInfo fileInfo) throws IOException { 628 // ignore revisionId 629 String cacheKey = "file_" + fileInfo.fileId; 630 DriveRequest<File> request = getService(fileInfo.user).files().get(fileInfo.fileId); 631 return executeAndCache(cacheKey, request, File.class); 632 } 633 634 /** 635 * Retrieves a {@link Revision} resource and caches the unparsed response. 636 * 637 * @return a {@link Revision} resource 638 */ 639 // subclassed for mock 640 protected Revision getRevision(FileInfo fileInfo) throws IOException { 641 if (fileInfo.revisionId == null) { 642 throw new NullPointerException("null revisionId for " + fileInfo.fileId); 643 } 644 String cacheKey = "rev_" + fileInfo.fileId + "_" + fileInfo.revisionId; 645 DriveRequest<Revision> request = getService(fileInfo.user).revisions().get(fileInfo.fileId, 646 fileInfo.revisionId); 647 try { 648 return executeAndCache(cacheKey, request, Revision.class); 649 } catch (HttpResponseException e) { 650 // return null if revision is not found 651 if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { 652 return null; 653 } 654 throw e; 655 } 656 } 657 658 /** 659 * Executes a {@link DriveRequest} and caches the unparsed response. 660 */ 661 protected <T> T executeAndCache(String cacheKey, DriveRequest<T> request, Class<T> aClass) throws IOException { 662 String resource = (String) getCache().get(cacheKey); 663 664 if (resource == null) { 665 HttpResponse response = request.executeUnparsed(); 666 if (!response.isSuccessStatusCode()) { 667 return null; 668 } 669 resource = response.parseAsString(); 670 if (cacheKey != null) { 671 getCache().put(cacheKey, resource); 672 } 673 } 674 return JSON_PARSER.parseAndClose(new StringReader(resource), aClass); 675 } 676 677 /** 678 * Retrieves the list of {@link Revision} resources for a file. 679 * 680 * @return a list of {@link Revision} resources 681 */ 682 // subclassed for mock 683 protected RevisionList getRevisionList(FileInfo fileInfo) throws IOException { 684 return getService(fileInfo.user).revisions().list(fileInfo.fileId).execute(); 685 } 686 687 /** 688 * Executes a GET request with the user's credentials. 689 */ 690 protected InputStream doGet(String user, URI url) throws IOException { 691 HttpResponse response = getService(user).getRequestFactory().buildGetRequest(new GenericUrl(url)).execute(); 692 return response.getContent(); 693 } 694 695 /** 696 * Parse a {@link URI}. 697 * 698 * @return the {@link URI} or null if it fails 699 */ 700 protected static URI asURI(String link) { 701 try { 702 return new URI(link); 703 } catch (URISyntaxException e) { 704 log.error("Invalid URI: " + link, e); 705 return null; 706 } 707 } 708 709 protected Cache getCache() { 710 if (cache == null) { 711 cache = Framework.getService(CacheService.class).getCache(FILE_CACHE_NAME); 712 } 713 return cache; 714 } 715 716 public String getClientId() { 717 OAuth2ServiceProvider provider = getOAuth2Provider(); 718 return (provider != null && provider.isEnabled()) ? provider.getClientId() : clientId; 719 } 720 721 protected OAuth2ServiceProvider getOAuth2Provider() { 722 return Framework.getLocalService(OAuth2ServiceProviderRegistry.class).getProvider(PREFIX); 723 } 724 725 @Override 726 public List<DocumentModel> checkChangesAndUpdateBlob(List<DocumentModel> docs) { 727 List<DocumentModel> changedDocuments = new ArrayList<>(); 728 // TODO use google batch request here 729 for (DocumentModel doc : docs) { 730 final SimpleManagedBlob blob = (SimpleManagedBlob) doc.getProperty("content").getValue(); 731 FileInfo fileInfo = getFileInfo(blob); 732 if (isVersion(blob)) { 733 // assume that revisions never change 734 continue; 735 } 736 try { 737 File remote = getPartialFile(fileInfo.user, fileInfo.fileId, "id", "title", "etag", "md5Checksum"); 738 if (isChanged(blob, remote)) { 739 doc.setPropertyValue("content", (SimpleManagedBlob) getBlob(getFileInfo(blob))); 740 String cacheKey = "file_" + fileInfo.fileId; 741 getCache().invalidate(cacheKey); 742 changedDocuments.add(doc); 743 } 744 } catch (IOException e) { 745 log.error("Could not update google drive document " + doc.getTitle(), e); 746 } 747 748 } 749 return changedDocuments; 750 } 751 752 @Override 753 public String getPageProviderNameForUpdate() { 754 return GOOGLEDRIVE_DOCUMENT_TO_BE_UPDATED_PP; 755 } 756 757 @Override 758 public String getBlobPrefix() { 759 return PREFIX; 760 } 761 762 @Override 763 public boolean isVersion(ManagedBlob blob) { 764 FileInfo fileInfo = getFileInfo(blob); 765 return fileInfo.revisionId != null; 766 } 767 768}