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