Java tutorial
package ch.cyberduck.core.gdocs; /* * Copyright (c) 2002-2010 David Kocher. All rights reserved. * * http://cyberduck.ch/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * Bug fixes, suggestions and comments should be sent to: * dkocher@cyberduck.ch */ import ch.cyberduck.core.*; import ch.cyberduck.core.i18n.Locale; import ch.cyberduck.core.io.BandwidthThrottle; import ch.cyberduck.core.serializer.Deserializer; import ch.cyberduck.core.serializer.Serializer; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.google.gdata.client.DocumentQuery; import com.google.gdata.client.GDataProtocol; import com.google.gdata.client.GoogleAuthTokenFactory; import com.google.gdata.client.Service; import com.google.gdata.client.http.HttpGDataRequest; import com.google.gdata.client.spreadsheet.SpreadsheetService; import com.google.gdata.data.*; import com.google.gdata.data.acl.AclEntry; import com.google.gdata.data.acl.AclFeed; import com.google.gdata.data.acl.AclRole; import com.google.gdata.data.acl.AclScope; import com.google.gdata.data.docs.*; import com.google.gdata.data.media.MediaSource; import com.google.gdata.util.ContentType; import com.google.gdata.util.NotImplementedException; import com.google.gdata.util.ServiceException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class GDPath extends Path { private static Logger log = Logger.getLogger(GDPath.class); private static class Factory extends PathFactory<GDSession> { @Override protected Path create(GDSession session, String path, int type) { return new GDPath(session, path, type); } @Override protected Path create(GDSession session, String parent, String name, int type) { return new GDPath(session, parent, name, type); } @Override protected Path create(GDSession session, String parent, Local file) { return new GDPath(session, parent, file); } @Override protected <T> Path create(GDSession session, T dict) { return new GDPath(session, dict); } } public static PathFactory factory() { return new Factory(); } @Override protected void init(Deserializer dict) { String resourceIdObj = dict.stringForKey("ResourceId"); if (resourceIdObj != null) { this.setResourceId(resourceIdObj); } String exportUriObj = dict.stringForKey("ExportUri"); if (exportUriObj != null) { this.setExportUri(exportUriObj); } String documentTypeObj = dict.stringForKey("DocumentType"); if (documentTypeObj != null) { this.setDocumentType(documentTypeObj); } super.init(dict); } @Override protected <S> S getAsDictionary(Serializer dict) { if (resourceId != null) { dict.setStringForKey(resourceId, "ResourceId"); } if (exportUri != null) { dict.setStringForKey(exportUri, "ExportUri"); } if (documentType != null) { dict.setStringForKey(documentType, "DocumentType"); } return super.<S>getAsDictionary(dict); } private final GDSession session; protected GDPath(GDSession s, String parent, String name, int type) { super(parent, name, type); this.session = s; } protected GDPath(GDSession s, String path, int type) { super(path, type); this.session = s; } protected GDPath(GDSession s, String parent, Local file) { super(parent, file); this.session = s; } protected <T> GDPath(GDSession s, T dict) { super(dict); this.session = s; } /** * Arbitrary file type not converted to Google Docs. */ private static final String DOCUMENT_FILE_TYPE = "file"; /** * Kind of document or folder. Type is currently one of: * document * drawing * folder * pdf * presentation * spreadsheet * form */ private String documentType; public String getDocumentType() { if (null == documentType) { if (attributes().isDirectory()) { return FolderEntry.LABEL; } // Arbitrary file type not converted to Google Docs. return DOCUMENT_FILE_TYPE; } return documentType; } public void setDocumentType(String documentType) { this.documentType = documentType; } /** * URL from where the document can be downloaded. */ private String exportUri; /** * @return Download URL without export format. */ public String getExportUri() { if (StringUtils.isBlank(exportUri)) { log.warn("Refetching Export URI for " + this.toString()); AttributedList<AbstractPath> l = this.getParent().children(); if (l.contains(this.getReference())) { exportUri = ((GDPath) l.get(this.getReference())).getExportUri(); } else { log.error("Missing Export URI for " + this.toString()); } } return exportUri; } public void setExportUri(String exportUri) { this.exportUri = exportUri; } /** * Resource ID. Contains both the document type and document ID. * For folders this is <code>folder:0BwoD_34YE1B4ZDFiZmMwNTAtMGFiMy00MmQ1LTg1NTQtNmFiYWFkNTg2MTQ3</code> */ private String resourceId; public String getResourceId() { if (StringUtils.isBlank(resourceId)) { log.warn("Refetching Resource ID for " + this.toString()); AttributedList<AbstractPath> l = this.getParent().children(); if (l.contains(this.getReference())) { resourceId = ((GDPath) l.get(this.getReference())).getResourceId(); } else { log.error("Missing Resource ID for " + this.toString()); } } return resourceId; } public void setResourceId(String resourceId) { this.resourceId = resourceId; } private String documentUri; /** * @return The URL to the document editable in the web browser */ public String getDocumentUri() { return documentUri; } public void setDocumentUri(String documentUri) { this.documentUri = documentUri; } private String getDocumentId() { // Removing document type from resourceId gives us the documentId return StringUtils.removeStart(this.getResourceId(), this.getDocumentType() + ":"); } /** * @return Includes the protocol and hostname only */ protected StringBuilder getPrivateFeed() { final StringBuilder feed = this.getHostUrl(); feed.append("/feeds/default/private/full/"); return feed; } private StringBuilder getHostUrl() { final StringBuilder feed = new StringBuilder(this.getSession().getHost().getProtocol().getScheme()) .append("://"); feed.append(this.getSession().getHost().getHostname()); return feed; } protected String getResourceFeed() throws MalformedURLException { return this.getPrivateFeed().append(this.getResourceId()).toString(); } protected String getMediaFeed() throws MalformedURLException { return this.getHostUrl().append("/feeds/default/media/document%3A").append(this.getDocumentId()).toString(); } protected String getUpdateSessionFeed() throws MalformedURLException { return new StringBuilder(this.getCreateSessionFeed()).append("/document%3A").append(this.getDocumentId()) .toString(); } protected String getCreateSessionFeed() throws MalformedURLException { return this.getHostUrl().append("/feeds/upload/create-session/default/private/full").toString(); } protected String getFolderFeed() throws MalformedURLException { final StringBuilder feed = this.getPrivateFeed(); if (this.isRoot()) { return feed.append("folder%3Aroot/contents").toString(); } return feed.append("folder%3A").append(this.getDocumentId()).append("/contents").toString(); } protected String getAclFeed() throws MalformedURLException { final StringBuilder feed = new StringBuilder(this.getResourceFeed()); return feed.append("/acl").toString(); } public String getRevisionsFeed() throws MalformedURLException { final StringBuilder feed = new StringBuilder(this.getResourceFeed()); return feed.append("/revisions").toString(); } @Override public void readSize() { ; } @Override public void readAcl() { try { this.getSession().check(); this.getSession().message(MessageFormat .format(Locale.localizedString("Getting permission of {0}", "Status"), this.getName())); Acl acl = new Acl(); AclFeed feed = this.getSession().getClient().getFeed(new URL(this.getAclFeed()), AclFeed.class); for (AclEntry entry : feed.getEntries()) { AclScope scope = entry.getScope(); AclScope.Type type = scope.getType(); AclRole role = entry.getRole(); if (type.equals(AclScope.Type.USER)) { // Only editable if not owner of document. Changing owner is not supported. boolean editable = !role.getValue().equals(AclRole.OWNER.getValue()); acl.addAll(new Acl.EmailUser(scope.getValue(), editable), new Acl.Role(role.getValue(), editable)); } else if (type.equals(AclScope.Type.DOMAIN)) { // Google Apps Domain grant. acl.addAll(new Acl.DomainUser(scope.getValue()), new Acl.Role(role.getValue())); } else if (type.equals(AclScope.Type.GROUP)) { // Google Group email grant acl.addAll(new Acl.GroupUser(scope.getValue(), true), new Acl.Role(role.getValue())); } else if (type.equals(AclScope.Type.DEFAULT)) { // Value of scope is null. Default access for non authenticated // users. Publicly shared with all users. acl.addAll(new Acl.CanonicalUser(AclScope.Type.DEFAULT.name(), Locale.localizedString("Public"), false), new Acl.Role(role.getValue())); } else { log.warn("Unsupported scope:" + type); } } this.attributes().setAcl(acl); } catch (IOException e) { this.error("Cannot read file attributes", e); } catch (ServiceException e) { this.error("Cannot read file attributes", e); } } @Override public void writeAcl(Acl acl, boolean recursive) { try { // Delete all previous ACLs before inserting updated set. AclFeed feed = this.getSession().getClient().getFeed(new URL(this.getAclFeed()), AclFeed.class); for (AclEntry entry : feed.getEntries()) { if (entry.getRole().toString().equals(AclRole.OWNER.toString())) { // Do not remove owner of document continue; } entry.delete(); } for (Acl.User user : acl.keySet()) { if (!user.isValid()) { continue; } if (!user.isEditable()) { continue; } // The API supports sharing permissions on multiple levels. These values // correspond to the <gAcl:scope> type attribute AclScope scope = null; if (user instanceof Acl.EmailUser) { // a user's email address. Creating an ACL entry that shares a document or folder with users will notify // relevant users via email that they have new access to the document or folder scope = new AclScope(AclScope.Type.USER, user.getIdentifier()); } else if (user instanceof Acl.GroupUser) { // a Google Group email address scope = new AclScope(AclScope.Type.GROUP, user.getIdentifier()); } else if (user instanceof Acl.DomainUser) { // a Google Apps domain. scope = new AclScope(AclScope.Type.DOMAIN, user.getIdentifier()); } else if (user instanceof Acl.CanonicalUser) { if (user.getIdentifier().equals(AclScope.Type.DEFAULT.name())) { // Publicly shared with all users scope = new AclScope(AclScope.Type.DEFAULT, null); } } if (null == scope) { log.warn("Unsupported scope:" + user); continue; } for (Acl.Role role : acl.get(user)) { if (!role.isValid()) { continue; } AclEntry entry = new AclEntry(); entry.setScope(scope); entry.setRole(new AclRole(role.getName())); // Insert updated ACL entry for scope this.getSession().getClient().insert(new URL(this.getAclFeed()), entry); } } } catch (IOException e) { this.error("Cannot change permissions", e); } catch (ServiceException e) { this.error("Cannot change permissions", e); } finally { this.attributes().clear(false, false, true, false); } if (attributes().isDirectory()) { if (recursive) { // All child objects of the folder reflect will reflect the new // sharing permission regardless. } } } @Override public GDSession getSession() { return session; } @Override protected void download(BandwidthThrottle throttle, StreamListener listener, boolean check) { if (attributes().isFile()) { OutputStream out = null; InputStream in = null; try { if (check) { this.getSession().check(); } MediaContent mc = new MediaContent(); StringBuilder uri = new StringBuilder(this.getExportUri()); final String type = this.getDocumentType(); final GoogleAuthTokenFactory.UserToken token = (GoogleAuthTokenFactory.UserToken) this.getSession() .getClient().getAuthTokenFactory().getAuthToken(); try { if (type.equals(SpreadsheetEntry.LABEL)) { // Authenticate against the Spreadsheets API to obtain an auth token SpreadsheetService spreadsheet = new SpreadsheetService(this.getSession().getUserAgent()); final Credentials credentials = this.getSession().getHost().getCredentials(); spreadsheet.setUserCredentials(credentials.getUsername(), credentials.getPassword()); // Substitute the spreadsheets token for the docs token this.getSession().getClient().setUserToken(((GoogleAuthTokenFactory.UserToken) spreadsheet .getAuthTokenFactory().getAuthToken()).getValue()); } if (StringUtils.isNotEmpty(getExportFormat(type))) { uri.append("&exportFormat=").append(getExportFormat(type)); } mc.setUri(uri.toString()); MediaSource ms = this.getSession().getClient().getMedia(mc); in = ms.getInputStream(); if (null == in) { throw new IOException("Unable opening data stream"); } out = this.getLocal().getOutputStream(this.status().isResume()); this.download(in, out, throttle, listener); } finally { // Restore docs token for our DocList client this.getSession().getClient().setUserToken(token.getValue()); } } catch (IOException e) { this.error("Download failed", e); } catch (ServiceException e) { this.error("Download failed", e); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(out); } } } /** * Google Apps Premier domains can upload files of arbitrary type. Uploading an arbitrary file is * the same as uploading documents (with and without metadata), except there is no * restriction on the file's Content-Type. Unlike normal document uploads, arbitrary * file uploads preserve their original format/extension, meaning there is no loss in * fidelity when the file is stored in Google Docs. * <p/> * By default, uploaded document files will be converted to a native Google Docs format. * For example, an .xls upload will create a Google Spreadsheet. To keep the file as an Excel * spreadsheet (and therefore upload the file as an arbitrary file), specify the convert=false * parameter to preserve the original format. The convert parameter is true by default for * document files. The parameter will be ignored for types that cannot be * converted (e.g. .exe, .mp3, .mov, etc.). * * @param throttle The bandwidth limit * @param listener The stream listener to notify about bytes received and sent * @param check Check for open connection and open if needed before transfer */ @Override protected void upload(BandwidthThrottle throttle, StreamListener listener, boolean check) { try { if (attributes().isFile()) { if (check) { this.getSession().check(); } InputStream in = null; OutputStream out = null; try { final String mime = this.getLocal().getMimeType(); final long size = this.getLocal().attributes().getSize(); DocumentListEntry document; if (this.exists()) { // First, fetch entry using the resourceId URL url = new URL(this.getResourceFeed()); document = this.getSession().getClient().getEntry(url, DocumentListEntry.class); this.setDocumentType(document.getType()); } else { document = new DocumentListEntry(); document.setTitle(new PlainTextConstruct(this.getName())); } StringBuilder feed; if (this.exists()) { // PUT /feeds/upload/create-session/default/private/full/document%3A12345 HTTP/1.1 feed = new StringBuilder(this.getUpdateSessionFeed()); } else { feed = new StringBuilder(this.getCreateSessionFeed()); } // Convertible to Google Docs file type. To create a resumable upload request for an arbitrary // file upload, include the convert=false parameter on this initial upload request feed.append("?convert=").append(this.isConversionSupported() && Preferences.instance().getBoolean("google.docs.upload.convert")); if (this.isOcrSupported()) { // Image file type feed.append("&ocr=").append(Preferences.instance().getProperty("google.docs.upload.ocr")); } // To initiate a resumable upload session, send an HTTP POST request to the resumable-post link. The unique // upload URI will be used to upload the file chunks final Service.GDataRequest session; if (this.exists()) { session = this.getSession().getClient().createUpdateRequest(new URL(feed.toString())); session.setEtag(document.getEtag()); } else { session = this.getSession().getClient().createInsertRequest(new URL(feed.toString())); } // Initialize a resumable media upload request. session.setHeader(GDataProtocol.Header.X_UPLOAD_CONTENT_TYPE, mime); session.setHeader(GDataProtocol.Header.X_UPLOAD_CONTENT_LENGTH, Long.toString(size)); final URL location; try { this.getSession().getClient().writeRequestData(session, document); session.execute(); location = new URL(session.getResponseHeader("Location")); } finally { session.end(); } final Service.GDataRequest request; request = this.getSession().getClient().createRequest(Service.GDataRequest.RequestType.UPDATE, location, new ContentType(mime)); request.setHeader("Content-Length", String.valueOf(size)); if (this.exists()) { if (this.status().isResume()) { // Querying the status of an incomplete upload Service.GDataRequest status = this.getSession().getClient().createRequest( Service.GDataRequest.RequestType.UPDATE, location, new ContentType(mime)); // If your request is terminated prior to receiving an entry response from the server or // if you receive an HTTP 503 response from the server, you can query the // current status of the upload by issuing an empty PUT request on the unique upload URI status.setHeader("Content-Length", String.valueOf(0)); status.setHeader("Content-Range", "bytes" + " " + "*/" + size); try { status.execute(); final String header = status.getResponseHeader("Range"); log.info("Content-Range reported by server:" + header); final long range = getNextByteIndexFromRangeHeader(header); request.setHeader("Content-Range", (this.status().isResume() ? range : 0) + "-" + (size - 1) + "/" + size); } catch (ServiceException e) { log.warn("Resume upload failed:" + e.getMessage()); // Ignore several possible server errors. Reload instead. this.status().setResume(false); } } } if (request instanceof HttpGDataRequest) { // No internal buffering of request with a known content length // Use chunked upload with default chunk size. ((HttpGDataRequest) request).getConnection().setChunkedStreamingMode(0); } in = getLocal().getInputStream(); out = request.getRequestStream(); this.upload(out, in, throttle, listener); try { // Parse response for HTTP error message. request.execute(); } catch (ServiceException e) { this.status().setComplete(false); throw e; } finally { request.end(); } } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(out); } } } catch (ServiceException e) { this.error("Upload failed", e); } catch (IOException e) { this.error("Upload failed", e); } } /** * Returns the next byte index identifying data that the server has not * yet received, obtained from an HTTP Range header (e.g., a header of * "Range: 0-55" would cause 56 to be returned). <code>null</code> or * malformed headers cause 0 to be returned. * * @param rangeHeader in the server response * @return the byte index beginning where the server has yet to receive data */ private long getNextByteIndexFromRangeHeader(String rangeHeader) { if (rangeHeader == null || rangeHeader.indexOf('-') == -1) { // No valid range header, start from the beginning of the file. return 0L; } Matcher rangeMatcher = Pattern.compile("[0-9]+-[0-9]+").matcher(rangeHeader); if (!rangeMatcher.find(1)) { // No valid range header, start from the beginning of the file. return 0L; } try { String[] rangeParts = rangeMatcher.group().split("-"); // Ensure that the start of the range is 0. long firstByteIndex = Long.parseLong(rangeParts[0]); if (firstByteIndex != 0) { return 0L; } // Return the next byte index after the end of the range. long lastByteIndex = Long.parseLong(rangeParts[1]); return lastByteIndex + 1; } catch (NumberFormatException e) { return 0L; } } /** * Supported file formats: application/pdf, image/jpeg, image/png, image/gif * * @return True for image formats supported by OCR */ protected boolean isOcrSupported() { return this.getMimeType().equals("application/pdf") || this.getMimeType().equals("image/png") || this.getMimeType().equals("image/jpeg") || this.getMimeType().endsWith("image/gif"); } /** * @return True if the document, spreadsheet or presentation format is recognized by Google Docs. */ protected boolean isConversionSupported() { // The convert parameter will be ignored for types that cannot be converted. Therefore we // can always return true. return true; } @Override public AttributedList<Path> list(final AttributedList<Path> children) { if (this.attributes().isDirectory()) { try { this.getSession().check(); this.getSession().message(MessageFormat .format(Locale.localizedString("Listing directory {0}", "Status"), this.getName())); children.addAll(this.list(new DocumentQuery(new URL(this.getFolderFeed())))); this.getSession().setWorkdir(this); } catch (ServiceException e) { log.warn("Listing directory failed:" + e.getMessage()); children.attributes().setReadable(false); if (this.cache().isEmpty()) { this.error(e.getMessage(), e); } } catch (IOException e) { log.warn("Listing directory failed:" + e.getMessage()); children.attributes().setReadable(false); if (this.cache().isEmpty()) { this.error(e.getMessage(), e); } } } return children; } /** * @param query * @return * @throws ServiceException * @throws IOException */ private AttributedList<Path> list(DocumentQuery query) throws ServiceException, IOException { final AttributedList<Path> children = new AttributedList<Path>(); DocumentListFeed pager = this.getSession().getClient().getFeed(query, DocumentListFeed.class); do { for (final DocumentListEntry entry : pager.getEntries()) { log.debug("Resource:" + entry.getResourceId()); final String type = entry.getType(); GDPath path = new GDPath(this.getSession(), this.getAbsolute(), entry.getTitle().getPlainText(), FolderEntry.LABEL.equals(type) ? Path.DIRECTORY_TYPE : Path.FILE_TYPE); path.setParent(this); path.setDocumentType(type); // Download URL path.setExportUri(((OutOfLineContent) entry.getContent()).getUri()); // Link to Google Docs Editor path.setDocumentUri(entry.getDocumentLink().getHref()); path.setResourceId(entry.getResourceId()); // Add unique document ID as checksum path.attributes().setChecksum(entry.getEtag()); if (null != entry.getMediaSource()) { path.attributes().setSize(entry.getMediaSource().getContentLength()); } if (entry.getQuotaBytesUsed() > 0) { path.attributes().setSize(entry.getQuotaBytesUsed()); } final DateTime lastViewed = entry.getLastViewed(); if (lastViewed != null) { path.attributes().setAccessedDate(lastViewed.getValue()); } for (Person person : entry.getAuthors()) { path.attributes().setOwner(person.getEmail()); } final DateTime updated = entry.getUpdated(); if (updated != null) { path.attributes().setModificationDate(updated.getValue()); } if (children.contains(path.getReference())) { // Google Docs allows files to be named the same. Not really a duplicate. path.attributes().setDuplicate(true); path.setReference(null); } // Add to listing children.add(path); if (path.attributes().isFile()) { // Fetch revisions if (Preferences.instance().getBoolean("google.docs.revisions.enable")) { try { final List<RevisionEntry> revisions = this.getSession().getClient() .getFeed(new URL(path.getRevisionsFeed()), RevisionFeed.class).getEntries(); Collections.sort(revisions, new Comparator<RevisionEntry>() { public int compare(RevisionEntry o1, RevisionEntry o2) { return o1.getUpdated().compareTo(o2.getUpdated()); } }); int i = 0; for (RevisionEntry revisionEntry : revisions) { GDPath revision = new GDPath(this.getSession(), revisionEntry.getTitle().getPlainText(), FolderEntry.LABEL.equals(type) ? Path.DIRECTORY_TYPE : Path.FILE_TYPE); revision.setParent(this); revision.setDocumentType(type); revision.setExportUri(((OutOfLineContent) revisionEntry.getContent()).getUri()); final long size = ((OutOfLineContent) revisionEntry.getContent()).getLength(); if (size > 0) { revision.attributes().setSize(size); } revision.attributes().setOwner(revisionEntry.getModifyingUser().getName()); revision.attributes().setModificationDate(revisionEntry.getUpdated().getValue()); // Versioning is enabled if non null. revision.attributes().setVersionId(revisionEntry.getVersionId()); revision.attributes().setChecksum(revisionEntry.getEtag()); revision.attributes().setRevision(++i); revision.attributes().setDuplicate(true); // Add to listing children.add(revision); } } catch (NotImplementedException e) { log.error("No revisions available:" + e.getMessage()); } } } } Link next = pager.getNextLink(); if (null == next) { // No link to next page. break; } // More pages available pager = this.getSession().getClient().getFeed(new URL(next.getHref()), DocumentListFeed.class); } while (pager.getEntries().size() > 0); return children; } @Override public String getMimeType() { if (attributes().isFile()) { final String exportFormat = getExportFormat(this.getDocumentType()); if (StringUtils.isNotEmpty(exportFormat)) { return getMimeType(exportFormat); } } return super.getMimeType(); } @Override public String getExtension() { if (attributes().isFile()) { final String exportFormat = getExportFormat(this.getDocumentType()); if (StringUtils.isNotEmpty(exportFormat)) { return exportFormat; } } return super.getExtension(); } @Override public String getName() { if (attributes().isFile()) { final String exportFormat = getExportFormat(this.getDocumentType()); if (StringUtils.isNotEmpty(exportFormat)) { if (!super.getName().endsWith(exportFormat)) { return super.getName() + "." + exportFormat; } } } return super.getName(); } /** * @param type The document type * @return */ protected static String getExportFormat(String type) { if (type.equals(DocumentEntry.LABEL)) { return Preferences.instance().getProperty("google.docs.export.document"); } if (type.equals(PresentationEntry.LABEL)) { return Preferences.instance().getProperty("google.docs.export.presentation"); } if (type.equals(SpreadsheetEntry.LABEL)) { return Preferences.instance().getProperty("google.docs.export.spreadsheet"); } if (type.equals(DOCUMENT_FILE_TYPE)) { // For files not converted to Google Docs. // DOCUMENT_FILE_TYPE log.debug("No output format conversion for document type:" + type); return null; } log.warn("Unknown document type:" + type); return null; } @Override public void mkdir() { if (this.attributes().isDirectory()) { try { this.getSession().check(); this.getSession().message(MessageFormat .format(Locale.localizedString("Making directory {0}", "Status"), this.getName())); DocumentListEntry folder = new FolderEntry(); folder.setTitle(new PlainTextConstruct(this.getName())); try { this.getSession().getClient().insert(new URL(((GDPath) this.getParent()).getFolderFeed()), folder); } catch (ServiceException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } this.cache().put(this.getReference(), AttributedList.<Path>emptyList()); // The directory listing is no more current this.getParent().invalidate(); } catch (IOException e) { this.error("Cannot create folder {0}", e); } } } @Override public void readUnixPermission() { throw new UnsupportedOperationException(); } @Override public void writeUnixPermission(Permission perm, boolean recursive) { throw new UnsupportedOperationException(); } @Override public void readTimestamp() { throw new UnsupportedOperationException(); } @Override public void writeTimestamp(long created, long modified, long accessed) { throw new UnsupportedOperationException(); } @Override public void delete() { try { if (this.attributes().isDuplicate()) { log.warn("Cannot delete revision " + this.attributes().getRevision()); return; } this.getSession().check(); this.getSession().message( MessageFormat.format(Locale.localizedString("Deleting {0}", "Status"), this.getName())); try { this.getSession().getClient().delete(new URL(this.getResourceFeed()), this.attributes().getChecksum()); } catch (ServiceException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } catch (MalformedURLException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } // The directory listing is no more current this.getParent().invalidate(); } catch (IOException e) { this.error("Cannot delete {0}", e); } } @Override public void rename(AbstractPath renamed) { try { this.getSession().check(); this.getSession().message(MessageFormat.format(Locale.localizedString("Renaming {0} to {1}", "Status"), this.getName(), renamed)); DocumentListEntry moved = new DocumentListEntry(); moved.setId("https://docs.google.com/feeds/id/" + this.getResourceId()); if (this.getParent().equals(renamed.getParent())) { // Rename file moved.setTitle(new PlainTextConstruct(renamed.getName())); try { // Move into new folder this.getSession().getClient().update(new URL(this.getResourceFeed()), moved, this.attributes().getChecksum()); } catch (ServiceException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } catch (MalformedURLException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } } else { try { // Move into new folder final DocumentListEntry update = this.getSession().getClient() .insert(new URL(((GDPath) renamed.getParent()).getFolderFeed()), moved); // Move out of previous folder this.getSession().getClient().delete( new URL((((GDPath) this.getParent()).getFolderFeed()) + "/" + this.getResourceId()), update.getEtag()); } catch (ServiceException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } catch (MalformedURLException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } } // The directory listing of the target is no more current renamed.getParent().invalidate(); // The directory listing of the source is no more current this.getParent().invalidate(); } catch (IOException e) { this.error("Cannot rename {0}", e); } } @Override public void touch() { if (this.attributes().isFile()) { try { this.getSession().check(); this.getSession().message( MessageFormat.format(Locale.localizedString("Uploading {0}", "Status"), this.getName())); DocumentListEntry file = new DocumentEntry(); file.setTitle(new PlainTextConstruct(this.getName())); try { this.getSession().getClient().insert(new URL(((GDPath) this.getParent()).getFolderFeed()), file); } catch (ServiceException e) { IOException failure = new IOException(e.getMessage()); failure.initCause(e); throw failure; } // The directory listing is no more current this.getParent().invalidate(); } catch (IOException e) { this.error("Cannot create file {0}", e); } } } @Override public String toHttpURL() { return this.getDocumentUri(); } }