/* Copyright (c) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.gdata.client; import com.google.gdata.util.common.base.Preconditions; import com.google.gdata.util.common.net.UriParameterMap; import com.google.gdata.client.AuthTokenFactory.AuthToken; import com.google.gdata.client.batch.BatchInterruptedException; import com.google.gdata.client.http.HttpGDataRequest; import com.google.gdata.data.AbstractExtension; import com.google.gdata.data.DateTime; import com.google.gdata.data.ExtensionProfile; import com.google.gdata.data.IAtom; import com.google.gdata.data.IEntry; import com.google.gdata.data.IFeed; import com.google.gdata.data.ILink; import com.google.gdata.data.ParseSource; import com.google.gdata.data.introspection.IServiceDocument; import com.google.gdata.model.Element; import com.google.gdata.model.ElementKey; import com.google.gdata.model.ElementMetadata; import com.google.gdata.model.MetadataContext; import com.google.gdata.model.MetadataRegistry; import com.google.gdata.model.Schema; import com.google.gdata.model.atom.Feed; import com.google.gdata.model.batch.BatchUtils; import com.google.gdata.model.transforms.atom.AtomVersionTransforms; import com.google.gdata.model.transforms.atompub.AtompubVersionTransforms; import com.google.gdata.util.ContentType; import com.google.gdata.util.NotModifiedException; import com.google.gdata.util.ParseException; import com.google.gdata.util.PreconditionFailedException; import com.google.gdata.util.ResourceNotFoundException; import com.google.gdata.util.ServiceException; import com.google.gdata.util.Version; import com.google.gdata.util.VersionRegistry; import com.google.gdata.wireformats.AltFormat; import com.google.gdata.wireformats.AltRegistry; import com.google.gdata.wireformats.StreamProperties; import com.google.gdata.wireformats.input.AtomDualParser; import com.google.gdata.wireformats.input.AtomServiceDualParser; import com.google.gdata.wireformats.input.InputParser; import com.google.gdata.wireformats.input.InputProperties; import com.google.gdata.wireformats.output.AtomDualGenerator; import com.google.gdata.wireformats.output.AtomServiceDualGenerator; import com.google.gdata.wireformats.output.OutputGenerator; import com.google.gdata.wireformats.output.OutputProperties; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.util.Collection; import java.util.Map; /** * The Service class represents a client connection to a GData service. It * encapsulates all protocol-level interactions with the GData server and acts * as a helper class for higher level entities (feeds, entries, etc) that invoke * operations on the server and process their results. * <p> * This class provides the base level common functionality required to access * any GData service. It is also designed to act as a base class that can be * customized for specific types of GData services. Examples of supported * customizations include: * <ul> * <li><b>Authentication</b> - implementing a custom authentication * mechanism for services that require authentication and use something other * than HTTP basic or digest authentication. * <li><b>Extensions</b> - define expected extensions for feed, entry, and * other types associated with a the service. * <li><b>Formats</b> - define additional custom resource representations that * might be consumed or produced by the service and client side parsers and * generators to handle them. * </ul> * * */ public class Service { private static final String SERVICE_VERSION = "GData-Java/" + Service.class.getPackage().getImplementationVersion() + "(gzip)"; // Necessary to get GZIP encoded responses /** * The Versions class defines {@link Version} constants representing the set * of active versions of the GData core protocol and common data model * classes. */ public static class Versions { /** * Version 1. GData core protocol released in May 2006 and is still in use * by version 1 of some GData services. */ public static final Version V1 = new Version(Service.class, 1, 0); /** * Version 2. GData core protocol release that brings full alignment with * the now standard Atom Publishing Protocol specification and migrates to * OpenSearch 1.1. */ public static final Version V2 = new Version(Service.class, 2, 0); /** * Version {@code 2.1}. Support new gd:kind attribute on feeds and * entries. */ public static final Version V2_1 = new Version(Service.class, 2, 1); /** * Version {@code 2.2}. Unreleased next minor version of the GData * protocol. */ public static final Version V2_2 = new Version(Service.class, 2, 2); /** * Version 3. Unreleased next major version of the GData protocol that will * default to structured error messages. */ public static final Version V3 = new Version(Service.class, 3, 0); private Versions() {} } /** * Initializes the default client version for the GData core protocol. */ @SuppressWarnings("unused") private static final Version CORE_VERSION = initServiceVersion(Service.class, Versions.V1); /** * The GDataRequest interface represents a streaming connection to a GData * service that can be used either to send request data to the service using * an OutputStream (or XmlWriter for XML content) or to receive response data * from the service as an InputStream (or ParseSource for XML data). The * calling client then has full control of the request data generation and * response data parsing. This can be used to integrate GData services with an * external Atom or RSS parsing library, such as Rome. * <p> * A GDataRequest instance will be returned by the streaming client APIs of * the Service class. The basic usage pattern is: * <p> * * <pre> * GDataRequest request = ... // createXXXRequest API call * try { * OutputStream requestStream = request.getRequestStream(); * // stream request data, if any * request.execute() // execute the request * InputStream responseStream = request.getResponseStream(); * // process the response data, if any * } * catch (IOException ioe) { * // handle errors writing to / reading from server * } catch (ServiceException se) { * // handle service invocation errors * } finally { * request.end(); * } * </pre> * * @see Service#createEntryRequest(URL) * @see Service#createFeedRequest(URL) * @see Service#createInsertRequest(URL) * @see Service#createUpdateRequest(URL) * @see Service#createDeleteRequest(URL) */ public interface GDataRequest { /** * The RequestType enumeration defines the set of expected GData request * types. These correspond to the four operations of the GData protocol: * <ul> * <li><b>QUERY</b> - query a feed, entry, or description document.</li> * <li><b>INSERT</b> - insert a new entry into a feed.</li> * <li><b>UPDATE</b> - update an existing entry within a feed.</li> * <li><b>PATCH</b> - patch an existing entry within a feed.</li> * <li><b>DELETE</b> - delete an existing entry within a feed.</li> * <li><b>BATCH</b> - execute several insert/update/delete operations</li> * </ul> */ public enum RequestType { QUERY, INSERT, UPDATE, PATCH, DELETE, BATCH } /** * Sets the number of milliseconds to wait for a connection to the remote * GData service before timing out. * * @param timeout the read timeout. A value of zero indicates an infinite * timeout. * @throws IllegalArgumentException if the timeout value is negative. * * @see java.net.URLConnection#setConnectTimeout(int) */ public void setConnectTimeout(int timeout); /** * Sets the number of milliseconds to wait for a response from the remote * GData service before timing out. * * @param timeout the read timeout. A value of zero indicates an infinite * timeout. * @throws IllegalArgumentException if the timeout value is negative. * * @see java.net.URLConnection#setReadTimeout(int) */ public void setReadTimeout(int timeout); /** * Sets the entity tag value that will be used to conditionalize the request * if not {@code null}. For a query requests, the tag will cause the target * resource to be returned if the resource entity tag <b>does not match</b> * the specified value (i.e. if the resource has not changed). For update or * delete request types, the entity tag value is used to indicate that the * requested operation should occur only if the specified etag value <b>does * match</b> the specified value (i.e. if the resource has changed). A * request entity tag value may not be associated with other request types. * * @param etag */ public void setEtag(String etag); /** * Sets the If-Modified-Since date precondition to be applied to the * request. If this precondition is set, then the request will be performed * only if the target resource has been modified since the specified date; * otherwise, a {@code NotModifiedException} will be thrown. The default * value is {@code null}, indicating no precondition. * * @param conditionDate the date that should be used to limit the operation * on the target resource. The operation will only be performed if * the resource has been modified later than the specified date. */ public void setIfModifiedSince(DateTime conditionDate); /** * Sets a request header (and logs it, if logging is enabled) * * @param name the header name * @param value the header value */ public void setHeader(String name, String value); /** * Sets request header (and log just the name but not the value, if logging * is enabled) * * @param name the header name * @param value the header value */ public void setPrivateHeader(String name, String value); /** * Returns the {@link URL} that is the target of the GData request */ public URL getRequestUrl(); /** * Returns a stream that can be used to write request data to the GData * service. * * @return OutputStream that can be used to write GData request data. * @throws IOException error obtaining the request output stream. */ public OutputStream getRequestStream() throws IOException; /** * Returns the {@link ContentType} of the data that will be written to the * service by this request or {@code null} if no data is written to the * server by the request. */ public ContentType getRequestContentType(); /** * Executes the GData service request. * * @throws IOException error writing to or reading from GData service. * @throws com.google.gdata.util.ResourceNotFoundException invalid request * target resource. * @throws ServiceException system error executing request. */ public void execute() throws IOException, ServiceException; /** * Returns the content type of the GData response. * * @return ContentType the GData response content type or {@code null} if no * response content. * @throws IllegalStateException attempt to read content type without first * calling {@link #execute()}. * @throws IOException error obtaining the response content type. * @throws ServiceException error obtaining the response content type. */ public ContentType getResponseContentType() throws IOException, ServiceException; /** * Returns an input stream that can be used to read response data from the * GData service. Returns null if response data cannot be read as an input * stream. Use {@link #getParseSource()} instead. * <p> * <b>The caller is responsible for ensuring that the input stream is * properly closed after the response has been read.</b> * * @return InputStream providing access to GData response input stream. * @throws IllegalStateException attempt to read response without first * calling {@link #execute()}. * @throws IOException error obtaining the response input stream. */ public InputStream getResponseStream() throws IOException; /** * Returns the value of the specified response header name or {@code null} * if no response header of this type exists. * * @param headerName name of header * @return header value. */ public String getResponseHeader(String headerName); /** * Returns the value of a header containing a header or {@code null} if no * response header of this type exists or it could not be parsed as a valid * date. * * @param headerName name of header * @return header value. */ public DateTime getResponseDateHeader(String headerName); /** * Returns a parse source that can be used to read response data from the * GData service. Parse source is an abstraction over input streams, * readers, and other forms of input. * <p> * <b>The caller is responsible for ensuring that input streams and readers * contained in the parse source are properly closed after the response has * been read.</b> * * @return ParseSource providing access to GData response data. * @throws IllegalStateException attempt to read response without first * calling {@link #execute()}. * @throws IOException error obtaining the response data. * @throws ServiceException error obtaining the response data. */ public ParseSource getParseSource() throws IOException, ServiceException; /** * Ends all processing associated with this request and releases any * transient resources (such as open data streams) required for execution. */ public void end(); } /** * The GDataRequestFactory interface defines a basic factory interface for * constructing a new GDataRequest interface. */ public interface GDataRequestFactory { /** * Set a header that will be included in all requests. If header of the same * name was previously set, then replace the previous header value. * * @param header the name of the header * @param value the value of the header, if null, then unset that header. */ public void setHeader(String header, String value); /** * Set a header that will be included in all requests and do not log the * value. Useful for values that are sensitive or related to security. If * header of the same name was previously set, then replace the previous * header value. * * @param header the name of the header * @param value the value of the header. If null, then unset that header. */ public void setPrivateHeader(String header, String value); /** * Set authentication token to be used on subsequent requests created via * {@link #getRequest( * com.google.gdata.client.Service.GDataRequest.RequestType, URL, * ContentType)}. * * An {@link IllegalArgumentException} is thrown if an auth token of the * wrong type is passed, or if authentication is not supported. * * @param authToken Authentication token. */ public void setAuthToken(AuthToken authToken); /** * Creates a new GDataRequest instance of the specified RequestType. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the returned * request once they have finished using it. * * @param type the request type * @param requestUrl the target URL for the request * @param contentType the contentType of the data being provided in the * request body. May be {@code null} if no data is provided. */ public GDataRequest getRequest(GDataRequest.RequestType type, URL requestUrl, ContentType contentType) throws IOException, ServiceException; /** * Creates a new GDataRequest instance for querying a service. This method * pushes the query parameters down to the factory method instead of * serializing them as a URL. Some factory implementations prefer to get * access to query parameters in their original form, not as a URL. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the returned * request once they have finished using it. * * @param query the query associated with the request * @param contentType this parameter is unused but remains for backwards * compatibility. */ public GDataRequest getRequest(Query query, ContentType contentType) throws IOException, ServiceException; } /** * Initializes the version information for a specific service type. Subclasses * of {@link Service} will generally call this method from within their static * initializers to bind version information for the associated service. * * @param serviceClass the service type being initialized. * @param defaultVersion the service version expected by this client library. */ protected static Version initServiceVersion( Class<? extends Service> serviceClass, Version defaultVersion) { VersionRegistry versionRegistry = VersionRegistry.ensureRegistry(); Version v = null; try { // Check to see if default has already been defined v = versionRegistry.getVersion(serviceClass); } catch (IllegalStateException ise) { // If not, use system property override or provided default version try { v = VersionRegistry.getVersionFromProperty(serviceClass); } catch (SecurityException e) { // Ignore exception, and just take default. } if (v == null) { v = defaultVersion; } // Do not include any implied versions, which are defaulted separately // by their own service static initialization. versionRegistry.addDefaultVersion(v, false); } return v; } /** * Returns the current {@link Version} of the GData core protocol. * * @return protocol version. */ public static Version getVersion() { return VersionRegistry.get().getVersion(Service.class); } @SuppressWarnings("unchecked") private static Version initProtocolVersion( Class<? extends Service> serviceClass) { // Find the service type with a declared default version that is // closest to the requested service type. Walking up the class hierarchy // allows for the possibility of subclassing without defining a different // protocol type Class<? extends Service> checkClass = serviceClass; VersionRegistry registry = VersionRegistry.get(); while (checkClass != Service.class) { try { return registry.getVersion(checkClass); } catch (IllegalStateException ise) { checkClass = (Class<? extends Service>) checkClass.getSuperclass(); } } // If no matching default, just return the core protocol version try { return Service.getVersion(); } catch (IllegalStateException ise) { return CORE_VERSION; } } /** * Constructs a new Service instance that is configured to accept arbitrary * extension data within feed or entry elements. */ public Service() { // Set the default User-Agent value for requests requestFactory.setHeader("User-Agent", getServiceVersion()); // Initialize the protocol version for this Service instance protocolVersion = initProtocolVersion(getClass()); // The default extension profile is configured to accept arbitrary XML // at the feed or entry level. A client never wants to lose any // foreign markup, so capture everything even if not explicitly // understood. new com.google.gdata.data.Feed().declareExtensions(extProfile); // The default metadata registry contains the basic feed plus the // version transforms for atom and atompub. this.metadataRegistry = new MetadataRegistry(); Feed.registerMetadata(metadataRegistry); AtomVersionTransforms.addTransforms(metadataRegistry); AtompubVersionTransforms.addTransforms(metadataRegistry); } /** * Returns an {@link AltRegistry} instance that is configured with the * default parser/generator configuration for a media service. */ public static AltRegistry getDefaultAltRegistry() { return BASE_REGISTRY; } /** * The DEFAULT_REGISTRY contains the default set of representations and * associated parser/generator configurations for all services. It will be * used as the default configuration for all Service instances unless * {@link #setAltRegistry(AltRegistry)} is called. */ private static final AltRegistry BASE_REGISTRY = new AltRegistry(); static { BASE_REGISTRY.register(AltFormat.ATOM, new AtomDualParser(), new AtomDualGenerator()); BASE_REGISTRY.register(AltFormat.ATOM_SERVICE, new AtomServiceDualParser(), new AtomServiceDualGenerator()); BASE_REGISTRY.register(AltFormat.APPLICATION_XML, null, new AtomDualGenerator(AltFormat.APPLICATION_XML)); // protect against subsequent changes BASE_REGISTRY.lock(); } /** * The version of the service protocol to use for this service instance. It * will be initialized to the service default version but can be set * explicitly by calling {@link #setProtocolVersion(Version)}. */ private Version protocolVersion; /** * Returns the service protocol version that will be used for requests * generated by this service. * * @return service protocol version */ public Version getProtocolVersion() { return protocolVersion; } /** * Sets the service protocol version that will be used for requests associated * with this service. * * @param v new service protocol version. */ public void setProtocolVersion(Version v) { // Ensure that any set version is appropriate for this service type, based // upon the default type that was computed at construction time. if (!protocolVersion.getServiceClass().equals(v.getServiceClass())) { throw new IllegalArgumentException("Invalid service class, " + "was: " + v.getServiceClass() + ", expected: " + protocolVersion.getServiceClass()); } protocolVersion = v; } protected void startVersionScope() { VersionRegistry.get().setThreadVersion(protocolVersion); } protected void endVersionScope() { VersionRegistry.get().resetThreadVersion(); } /** * Returns information about the service version. */ public String getServiceVersion() { return SERVICE_VERSION; } protected ExtensionProfile extProfile = new ExtensionProfile(); /** * Returns the {@link ExtensionProfile} that defines any expected extensions * to the base RSS/Atom content model. */ public ExtensionProfile getExtensionProfile() { return extProfile; } /** * Sets the {@link ExtensionProfile} that defines any expected extensions to * the base RSS/Atom content model. */ public void setExtensionProfile(ExtensionProfile v) { this.extProfile = v; } protected final MetadataRegistry metadataRegistry; /** * Returns the {@link MetadataRegistry} that defines the expected metadata. */ public MetadataRegistry getMetadataRegistry() { return metadataRegistry; } /** * Returns the {@link Schema} that contains the metadata about * element types parsed or generated by this service. */ public Schema getSchema() { return metadataRegistry.createSchema(); } /** * The GDataRequestFactory associated with this service. The default is the * base HttpGDataRequest Factory class. */ protected GDataRequestFactory requestFactory = new HttpGDataRequest.Factory(); /** * Returns the GDataRequestFactory currently associated with the service. */ public GDataRequestFactory getRequestFactory() { return requestFactory; } /** * Sets the GDataRequestFactory currently associated with the service. */ public void setRequestFactory(GDataRequestFactory requestFactory) { this.requestFactory = requestFactory; } /** * Set a header that will be included in all requests. If header of the same * name was previously set, then replace the previous header value. * * @param header the name of the header * @param value the value of the header, if null, then unset that header. */ public void setHeader(String header, String value) { getRequestFactory().setHeader(header, value); } /** * Set a header that will be included in all requests and do not log the * value. Useful for values that are sensitive or related to security. If * header of the same name was previously set, then replace the previous * header value. * * @param header the name of the header * @param value the value of the header. If null, then unset that header. */ public void setPrivateHeader(String header, String value) { getRequestFactory().setPrivateHeader(header, value); } /** * Adds OAuth Proxy-related headers to the request. The OAuth Proxy * simplifies the OAuth dance on when running in App Engine. * @see <a href="http://sites.google.com/site/oauthgoog/Home/gaeoauthproxy"> * http://sites.google.com/site/oauthgoog/Home/gaeoauthproxy</a> */ public void setOAuthProxyHeaders(Map<String, String> headers) { for (String key : headers.keySet()) { setHeader(key, headers.get(key)); } } /** * Sets the HttpGDataRequest.Factory associate with the service * to use secure connections. */ public void useSsl() { if (!(this.requestFactory instanceof HttpGDataRequest.Factory)) { throw new UnsupportedOperationException("Not a http transport"); } ((HttpGDataRequest.Factory) this.requestFactory).useSsl(); } /** * Defines the languages accepted by the application. * * This parameters defines the human language the service should use for * generated strings. Different services support different languages, please * check the service documentation. * * If no language on this list is accepted by the service, and if the list * does not contain {@code *} to accept all languages, the exception * in the exception {@link com.google.gdata.util.NotAcceptableException}. * * The service will choose the best available language on this list. Check the * attribute {@code xml:lang} on the relevant tags, such as atom:content, * atom:title and atom:category. * * @param acceptedLanguages list of accepted languages, as defined * in section 14.4 of RFC 2616 */ public void setAcceptLanguage(String acceptedLanguages) { this.requestFactory.setHeader( GDataProtocol.Header.ACCEPT_LANGUAGE, acceptedLanguages); } /** * Creates a new GDataRequest for use by the service. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. */ public GDataRequest createRequest(GDataRequest.RequestType type, URL requestUrl, ContentType inputType) throws IOException, ServiceException { GDataRequest request = requestFactory.getRequest(type, requestUrl, inputType); setTimeouts(request); return request; } /** * Creates a new GDataRequest for querying the service. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. */ protected GDataRequest createRequest(Query query, ContentType inputType) throws IOException, ServiceException { GDataRequest request = requestFactory.getRequest(query, inputType); setTimeouts(request); return request; } /** * Sets timeout value for GDataRequest. */ public void setTimeouts(GDataRequest request) { if (connectTimeout >= 0) { request.setConnectTimeout(connectTimeout); } if (readTimeout >= 0) { request.setReadTimeout(readTimeout); } } /** * Content type of data posted to the GData service. Defaults to Atom using * UTF-8 character set. */ private ContentType contentType = ContentType.ATOM; /** * Returns the default ContentType for data associated with this GData * service. */ public ContentType getContentType() { return contentType; } /** * Sets the default ContentType for writing data to the GData service. */ public void setContentType(ContentType contentType) { this.contentType = contentType; } /** * Client-configured connection timeout value. A value of -1 indicates the * client has not set any timeout. */ protected int connectTimeout = -1; /** * Sets the default wait timeout (in milliseconds) for a connection to the * remote GData service. * * @param timeout the read timeout. A value of zero indicates an infinite * timeout. * @throws IllegalArgumentException if the timeout value is negative. * * @see java.net.URLConnection#setConnectTimeout(int) */ public void setConnectTimeout(int timeout) { if (timeout < 0) { throw new IllegalArgumentException("Timeout value cannot be negative"); } connectTimeout = timeout; } /** * Client configured read timeout value. A value of -1 indicates the client * has not set any timeout. */ int readTimeout = -1; /** * Sets the default wait timeout (in milliseconds) for a response from the * remote GData service. * * @param timeout the read timeout. A value of zero indicates an infinite * timeout. * @throws IllegalArgumentException if the timeout value is negative. * * @see java.net.URLConnection#setReadTimeout(int) */ public void setReadTimeout(int timeout) { if (timeout < 0) { throw new IllegalArgumentException("Timeout value cannot be negative"); } readTimeout = timeout; } /** * The alternate representation registry that describes formats supported by * the remote GData service. */ private AltRegistry altRegistry = BASE_REGISTRY; /** * Returns the alternate registration registry that describes representations * that may be parsed from or generated to the remote GData service. */ public AltRegistry getAltRegistry() { return altRegistry; } public void setAltRegistry(AltRegistry altRegistry) { this.altRegistry = altRegistry; } private boolean strictValidation = true; /** * Returns {@code true} if strict validation is enabled for * this service. */ public boolean getStrictValidation() { return strictValidation; } /** * Enables or disables strict validation. It is enabled by * default. When this flag is enabled, the client library rejects * unknown attributes and validates both input and output data. */ public void setStrictValidation(boolean strictValidation) { this.strictValidation = strictValidation; } // Helper method that narrows the scope of unchecked (but safe) class casting. @SuppressWarnings("unchecked") protected <T> Class<T> classOf(T object) { return (Class<T>) object.getClass(); } /** * Returns the Atom introspection Service Document associated with a * particular feed URL. This document provides service metadata about the set * of Atom services associated with the target feed URL. * * @param feedUrl the URL associated with a feed. This URL can not include any * query parameters. * @param serviceClass the class used to represent a service document, * not {@code null}. * @return ServiceDocument resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws com.google.gdata.util.ParseException error parsing the returned * service data. * @throws com.google.gdata.util.ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving service document. */ public <S extends IServiceDocument> S introspect(URL feedUrl, Class<S> serviceClass) throws IOException, ServiceException { String feedQuery = feedUrl.getQuery(); String altParam = GDataProtocol.Parameter.ALT + "=" + AltFormat.ATOM_SERVICE.getName(); if (feedQuery == null || feedQuery.indexOf(altParam) == -1) { char appendChar = (feedQuery == null) ? '?' : '&'; feedUrl = new URL(feedUrl.toString() + appendChar + altParam); } GDataRequest request = createFeedRequest(feedUrl); try { startVersionScope(); request.execute(); return parseResponseData(request, serviceClass); } finally { endVersionScope(); request.end(); } } /** * Returns the Feed associated with a particular feed URL, if it's been * modified since the specified date. * * @param feedUrl the URL associated with a feed. This URL can include GData * query parameters. * @param feedClass the class used to represent a service Feed. * @param ifModifiedSince used to set a precondition date that indicates the * feed should be returned only if it has been modified after the * specified date. A value of {@code null} indicates no precondition. * @return Feed resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws com.google.gdata.util.NotModifiedException if the feed resource has * not been modified since the specified precondition date. * @throws com.google.gdata.util.ParseException error parsing the returned * feed data. * @throws com.google.gdata.util.ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving feed. */ @SuppressWarnings("unchecked") public <F extends IFeed> F getFeed(URL feedUrl, Class<F> feedClass, DateTime ifModifiedSince) throws IOException, ServiceException { GDataRequest request = createFeedRequest(feedUrl); return getFeed(request, feedClass, ifModifiedSince); } /** * Returns the Feed associated with a particular feed URL if the entity tag * associated with it has changed. * * @param feedUrl the URL associated with a feed. This URL can include GData * query parameters. * @param feedClass the class used to represent a service Feed. * @param etag used to provide an entity tag that indicates the feed should be * returned only if the entity tag of the current representation is * different from the provided value. A value of {@code null} indicates * unconditional return. * @throws IOException error sending request or reading the feed. * @throws NotModifiedException if the feed resource entity tag matches the * provided value. * @throws com.google.gdata.util.ParseException error parsing the returned * feed data. * @throws com.google.gdata.util.ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving feed. */ @SuppressWarnings("unchecked") public <F extends IFeed> F getFeed(URL feedUrl, Class<F> feedClass, String etag) throws IOException, ServiceException { GDataRequest request = createFeedRequest(feedUrl); return getFeed(request, feedClass, etag); } /** * Returns the Feed associated with a particular feed URL. * * @param feedUrl the URL associated with a feed. This URL can include GData * query parameters. * @param feedClass the class used to represent a service Feed. * @return Feed resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws com.google.gdata.util.ParseException error parsing the returned * feed data. * @throws com.google.gdata.util.ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving feed. */ public <F extends IFeed> F getFeed(URL feedUrl, Class<F> feedClass) throws IOException, ServiceException { return getFeed(feedUrl, feedClass, (String) null); } /** * Returns the feed resulting from execution of a query. * * @param query feed query. * @param feedClass the class used to represent query results. * @return feed resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws ParseException error parsing the returned feed data. * @throws ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving feed. */ public <F extends IFeed> F getFeed(Query query, Class<F> feedClass) throws IOException, ServiceException { return getFeed(query, feedClass, (String) null); } /** * Returns the Feed resulting from executing a query, if it's been modified * since the specified date. * * @param query feed query. * @param feedClass the class used to represent a service Feed. * @param ifModifiedSince used to set a precondition date that indicates the * feed should be returned only if it has been modified after the * specified date. A value of {@code null} indicates no precondition. * @return Feed resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws ServiceException system error retrieving feed. */ public <F extends IFeed> F getFeed(Query query, Class<F> feedClass, DateTime ifModifiedSince) throws IOException, ServiceException { GDataRequest request = createFeedRequest(query); return getFeed(request, feedClass, ifModifiedSince); } /** * Returns the Feed resulting from query execution, if if the entity tag * associated with it has changed. * * @param query feed query. * @param feedClass the class used to represent query results. * @param etag used to provide an entity tag that indicates the feed should be * returned only if the entity tag of the current representation is * different from the provided value. A value of {@code null} indicates * unconditional return. * @throws IOException error sending request or reading the feed. * @throws NotModifiedException if the feed resource entity tag matches the * provided value. * @throws ParseException error parsing the returned feed data. * @throws ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving feed. */ public <F extends IFeed> F getFeed(Query query, Class<F> feedClass, String etag) throws IOException, ServiceException { GDataRequest request = createFeedRequest(query); return getFeed(request, feedClass, etag); } /** * Returns the Feed associated with a particular feed URL, if it's been * modified since the specified date. * * @param request the GData request. * @param feedClass the class used to represent a service Feed, * not {@code null}. * @param ifModifiedSince used to set a precondition date that indicates the * feed should be returned only if it has been modified after the * specified date. A value of {@code null} indicates no precondition. * @return Feed resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws ServiceException system error retrieving feed. */ private <F extends IFeed> F getFeed(GDataRequest request, Class<F> feedClass, DateTime ifModifiedSince) throws IOException, ServiceException { try { startVersionScope(); request.setIfModifiedSince(ifModifiedSince); request.execute(); return parseResponseData(request, feedClass); } finally { endVersionScope(); request.end(); } } /** * Returns the feed associated with a particular feed URL if its entity tag * is different than the provided value. * * @param request the GData request. * @param feedClass the class used to represent the resulting feed, * not {@code null}. * @param etag used to provide an entity tag that indicates the feed should be * returned only if the entity tag of the current representation is * different from the provided value. A value of {@code null} indicates * unconditional return. * @return Feed resource referenced by the input URL. * @throws IOException error sending request or reading the feed. * @throws NotModifiedException if the feed resource entity tag matches the * provided value. * @throws ParseException error parsing the returned feed data. * @throws ResourceNotFoundException invalid feed URL. * @throws ServiceException system error retrieving feed. */ @SuppressWarnings("unchecked") private <F extends IFeed> F getFeed(GDataRequest request, Class<F> feedClass, String etag) throws IOException, ServiceException { try { startVersionScope(); request.setEtag(etag); request.execute(); return parseResponseData(request, feedClass); } finally { endVersionScope(); request.end(); } } /** * Executes a GData feed request against the target service and returns the * resulting feed results via an input stream. * * @param feedUrl URL that defines target feed. * @return GData request instance that can be used to read the feed data. * @throws IOException error communicating with the GData service. * @throws ServiceException creation of query feed request failed. * * @see Query#getUrl() */ public GDataRequest createFeedRequest(URL feedUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.QUERY, feedUrl, contentType); } /** * Executes a GData query against the target service and returns the * {@link IFeed} containing entries that match the query result. * * @param query Query instance defining target feed and query parameters. * @param feedClass the Class used to represent a service Feed, * not {@code null}. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ServiceForbiddenException feed does not * support the query. * @throws com.google.gdata.util.ParseException error parsing the returned * feed data. * @throws ServiceException query request failed. */ public <F extends IFeed> F query(Query query, Class<F> feedClass) throws IOException, ServiceException { // A query is really same as getFeed against the combined feed + query URL return query(query, feedClass, (String) null); } /** * Executes a GData query against the target service and returns the * {@link IFeed} containing entries that match the query result, if it's been * modified since the specified date. * * @param query Query instance defining target feed and query parameters. * @param feedClass the Class used to represent a service Feed, * not {@code null}. * @param ifModifiedSince used to set a precondition date that indicates the * query result feed should be returned only if contains entries that * have been modified after the specified date. A value of {@code null} * indicates no precondition. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.NotModifiedException if the query resource * does not contain entries modified since the specified precondition * date. * @throws com.google.gdata.util.ServiceForbiddenException feed does not * support the query. * @throws com.google.gdata.util.ParseException error parsing the returned * feed data. * @throws ServiceException query request failed. */ public <F extends IFeed> F query(Query query, Class<F> feedClass, DateTime ifModifiedSince) throws IOException, ServiceException { // A query is really same as getFeed against the combined feed + query URL return getFeed(query, feedClass, ifModifiedSince); } /** * Executes a GData query against the target service and returns the * {@link IFeed} containing entries that match the query result if the etag * for the target feed does not match the provided value. * * @param query Query instance defining target feed and query parameters. * @param feedClass the Class used to represent a service Feed, * not {@code null}. * @param etag used to provide an entity tag that indicates the query should be * be performed only if the entity tag of the current representation is * different from the provided value. A value of {@code null} indicates * unconditional return. * @throws IOException error communicating with the GData service. * @throws NotModifiedException if the feed resource entity tag matches the * provided value. * @throws com.google.gdata.util.ServiceForbiddenException feed does not * support the query. * @throws com.google.gdata.util.ParseException error parsing the returned * feed data. * @throws ServiceException query request failed. */ public <F extends IFeed> F query(Query query, Class<F> feedClass, String etag) throws IOException, ServiceException { // A query is really same as getFeed against the combined feed + query URL return getFeed(query, feedClass, etag); } /** * Executes a GData query request against the target service and returns the * resulting feed results via an input stream. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param query feed query. * @return GData request instance that can be used to read the feed data. * @throws IOException error communicating with the GData service. * @throws ServiceException creation of query feed request failed. * * @see Query#getUrl() */ public GDataRequest createFeedRequest(Query query) throws IOException, ServiceException { return createRequest(query, contentType); } /** * Returns an Atom entry instance, given the URL of the entry. * * @param entryUrl resource URL for the entry. * @param entryClass class used to represent service entries, * not {@code null}. * @return the entry referenced by the URL parameter. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ParseException error parsing the returned * entry. * @throws com.google.gdata.util.ResourceNotFoundException if the entry URL is * not valid. * @throws com.google.gdata.util.ServiceForbiddenException if the GData * service cannot get the entry resource due to access constraints. * @throws ServiceException if a system error occurred when retrieving the * entry. */ public <E extends IEntry> E getEntry(URL entryUrl, Class<E> entryClass) throws IOException, ServiceException { return getEntry(entryUrl, entryClass, (String) null); } /** * Returns an Atom entry instance, given the URL of the entry and an * if-modified-since date. * * @param entryUrl resource URL for the entry. * @param entryClass class used to represent service entries, * not {@code null}. * @param ifModifiedSince used to set a precondition date that indicates the * entry should be returned only if it has been modified after the * specified date. A value of {@code null} indicates no precondition. * @return the entry referenced by the URL parameter. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.NotModifiedException if the entry resource * has not been modified after the specified precondition date. * @throws com.google.gdata.util.ParseException error parsing the returned * entry. * @throws com.google.gdata.util.ResourceNotFoundException if the entry URL is * not valid. * @throws com.google.gdata.util.ServiceForbiddenException if the GData * service cannot get the entry resource due to access constraints. * @throws ServiceException if a system error occurred when retrieving the * entry. */ public <E extends IEntry> E getEntry(URL entryUrl, Class<E> entryClass, DateTime ifModifiedSince) throws IOException, ServiceException { ParseSource entrySource = null; GDataRequest request = createEntryRequest(entryUrl); try { startVersionScope(); request.setIfModifiedSince(ifModifiedSince); request.execute(); return parseResponseData(request, entryClass); } finally { endVersionScope(); request.end(); } } /** * Returns an Atom entry instance given the URL of the entry, if its current * entity tag is different than the provided value. * * @param entryUrl resource URL for the entry. * @param entryClass class used to represent service entries, * not {@code null}. * @param etag used to provide an entity tag that indicates the entry should * be returned only if the entity tag of the current representation is * different from the provided value. A value of {@code null} indicates * unconditional return. * @throws IOException error communicating with the GData service. * @throws NotModifiedException if the entry resource entity tag matches the * provided value. * @throws com.google.gdata.util.ParseException error parsing the returned * entry. * @throws com.google.gdata.util.ResourceNotFoundException if the entry URL is * not valid. * @throws com.google.gdata.util.ServiceForbiddenException if the GData * service cannot get the entry resource due to access constraints. * @throws ServiceException if a system error occurred when retrieving the * entry. */ public <E extends IEntry> E getEntry(URL entryUrl, Class<E> entryClass, String etag) throws IOException, ServiceException { ParseSource entrySource = null; GDataRequest request = createEntryRequest(entryUrl); try { startVersionScope(); request.setEtag(etag); request.execute(); return parseResponseData(request, entryClass); } finally { endVersionScope(); request.end(); } } /** * Returns a GDataRequest instance that can be used to access an entry's * contents as a stream, given the URL of the entry. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param entryUrl resource URL for the entry. * @return GData request instance that can be used to read the entry. * @throws IOException error communicating with the GData service. * @throws ServiceException entry request creation failed. */ public GDataRequest createEntryRequest(URL entryUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.QUERY, entryUrl, contentType); } /** * Inserts a new {@link IEntry} into a feed associated * with the target service. It will return the inserted entry, including any * additional attributes or extensions set by the GData server. * * @param feedUrl the POST URI associated with the target feed. * @param entry the new entry to insert into the feed. * @return the newly inserted Entry returned by the service. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ParseException error parsing the return entry * data. * @throws com.google.gdata.util.ServiceForbiddenException the inserted Entry * has associated media content and can only be inserted using a media * service. * @throws ServiceException insert request failed due to system error. * * @see IFeed#getEntryPostLink() */ @SuppressWarnings("unchecked") public <E extends IEntry> E insert(URL feedUrl, E entry) throws IOException, ServiceException { if (entry == null) { throw new NullPointerException("Must supply entry"); } GDataRequest request = createInsertRequest(feedUrl); try { startVersionScope(); writeRequestData(request, entry); request.execute(); return parseResponseData(request, classOf(entry)); } finally { endVersionScope(); request.end(); } } /** * Executes several operations (insert, update or delete) on the entries that * are part of the input {@link IFeed}. It will return another feed that * describes what was done while executing these operations. * * It is possible for one batch operation to fail even though other operations * have worked, so this method won't throw a ServiceException unless something * really wrong is going on. You need to check the entries in the returned * feed to know which operations succeeded and which operations failed (see * {@link com.google.gdata.data.batch.BatchStatus} and * {@link com.google.gdata.data.batch.BatchInterrupted} extensions.) * * @param feedUrl the POST URI associated with the target feed. * @param inputFeed a description of the operations to execute, described * using tags in the batch: namespace * @return a feed with the result of each operation in a separate entry * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ParseException error parsing the return entry * data. * @throws ServiceException insert request failed due to system error. * @throws BatchInterruptedException if something really wrong was detected by * the server while parsing the request, like invalid XML data. Some * operations might have succeeded when this exception is thrown. * Check {@link BatchInterruptedException#getIFeed()}. * * @see IFeed#getEntryPostLink() */ @SuppressWarnings("unchecked") public <F extends IFeed> F batch(URL feedUrl, F inputFeed) throws IOException, ServiceException, BatchInterruptedException { GDataRequest request = createInsertRequest(feedUrl); try { startVersionScope(); writeRequestData(request, inputFeed); request.execute(); F resultFeed = parseResponseData(request, classOf(inputFeed)); // Detect BatchInterrupted int count = resultFeed.getEntries().size(); BatchUtils.throwIfInterrupted(resultFeed); return resultFeed; } finally { endVersionScope(); request.end(); } } /** * Creates a new GDataRequest that can be used to insert a new entry into a * feed using the request stream and to read the resulting entry content from * the response stream. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param feedUrl the POST URI associated with the target feed. * @return GDataRequest to interact with remote GData service. * @throws IOException error reading from or writing to the GData service. * @throws ServiceException insert request failed. */ public GDataRequest createInsertRequest(URL feedUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.INSERT, feedUrl, contentType); } /** * Creates a new GDataRequest that can be used to execute several * insert/update/delete operations in one request by writing a feed into the * request stream to read a feed containing the result of the batch operations * from the response stream. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param feedUrl the POST URI associated with the target feed. * @return GDataRequest to interact with remote GData service. * @throws IOException error reading from or writing to the GData service. * @throws ServiceException insert request failed. */ public GDataRequest createBatchRequest(URL feedUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.BATCH, feedUrl, contentType); } /** * Updates an existing {@link IEntry} by writing it to * the specified entry edit URL. The resulting Entry (after update) will be * returned. * * @param entryUrl the edit URL associated with the entry. * @param entry the modified Entry to be written to the server. * @return the updated Entry returned by the service. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ParseException error parsing the updated * entry data. * @throws ServiceException update request failed due to system error. * * @see IEntry#getEditLink() */ @SuppressWarnings("unchecked") public <E extends IEntry> E update(URL entryUrl, E entry) throws IOException, ServiceException { // If the entry has a strong etag, use it as a precondition. String etag = entry.getEtag(); if (GDataProtocol.isWeakEtag(etag)) { etag = null; } return update(entryUrl, entry, etag); } /** * Updates an existing {@link IEntry} by writing it to the specified entry * edit URL. The resulting entry (after update) will be returned. This update * is conditional upon the provided tag matching the current entity tag for * the entry. If (and only if) they match, the update will be performed. * * @param entryUrl the edit URL associated with the entry. * @param entry the modified entry to be written to the server. * @param etag the entity tag value that is the expected value for the target * resource. A value of {@code null} will not set an etag precondition * and a value of <code>"*"</code> will perform an unconditional * update. * @return the updated Entry returned by the service. * @throws IOException error communicating with the GData service. * @throws PreconditionFailedException if the resource entity tag does not * match the provided value. * @throws com.google.gdata.util.ParseException error parsing the updated * entry data. * @throws ServiceException update request failed due to system error. * * @see IEntry#getEditLink() */ public <E extends IEntry> E update(URL entryUrl, E entry, String etag) throws IOException, ServiceException { GDataRequest request = createUpdateRequest(entryUrl); try { startVersionScope(); request.setEtag(etag); writeRequestData(request, entry); request.execute(); return parseResponseData(request, classOf(entry)); } finally { endVersionScope(); request.end(); } } /** * Creates a new GDataRequest that can be used to update an existing Atom * entry. The updated entry content can be written to the GDataRequest request * stream and the resulting updated entry can be obtained from the * GDataRequest response stream. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param entryUrl the edit URL associated with the entry. * @throws IOException error communicating with the GData service. * @throws ServiceException creation of update request failed. */ public GDataRequest createUpdateRequest(URL entryUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.UPDATE, entryUrl, contentType); } /** * Patches an existing {@link IEntry} by removing a set of selected fields and * then merging a partial entry representation into the resource at the * specified entry edit URL. The resulting entry (after update) will be * returned. * * @param entryUrl the edit URL associated with the entry. * @param fields selection representing the set of fields to be patched from * the resource. * @param entry the partial entry to be merged with current resource. * @return the patched Entry returned by the service. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ParseException error parsing the returned * entry data. * @throws ServiceException update request failed due to system error. * * @see IEntry#getEditLink() */ @SuppressWarnings("unchecked") public <E extends IEntry> E patch(URL entryUrl, String fields, E entry) throws IOException, ServiceException { // If the entry has a strong etag, use it as a precondition. String etag = null; if (entry != null) { etag = entry.getEtag(); if (GDataProtocol.isWeakEtag(etag)) { etag = null; } } return patch(entryUrl, fields, entry, etag); } /** * Patches an existing {@link IEntry} by removing a set of selected fields and * then merging a partial entry representation into the resource at the * specified entry edit URL. The resulting entry (after update) will be * returned. This update is conditional upon the provided tag matching the * current entity tag for the entry. If (and only if) they match, the patch * will be performed. * * @param entryUrl the edit URL associated with the entry. * @param fields selection representing the set of fields to be removed from * the resource. * @param entry the partial entry to be merged with current resource. * @param etag the entity tag value that is the expected value for the target * resource. A value of {@code null} will not set an etag precondition * and a value of <code>"*"</code> will perform an unconditional * update. * @return the patched Entry returned by the service. * @throws IOException error communicating with the GData service. * @throws PreconditionFailedException if the resource entity tag does not * match the provided value. * @throws com.google.gdata.util.ParseException error parsing the patched * entry data. * @throws ServiceException update request failed due to system error. * * @see IEntry#getEditLink() */ public <E extends IEntry> E patch(URL entryUrl, String fields, E entry, String etag) throws IOException, ServiceException { GDataRequest request = createPatchRequest(entryUrl); try { startVersionScope(); request.setEtag(etag); entry.setSelectedFields(fields); writeRequestData(request, entry); request.execute(); return parseResponseData(request, classOf(entry)); } finally { endVersionScope(); request.end(); } } /** * Creates a new GDataRequest that can be used to update an existing Atom * entry. The updated entry content can be written to the GDataRequest request * stream and the resulting updated entry can be obtained from the * GDataRequest response stream. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param entryUrl the edit URL associated with the entry. * @throws IOException error communicating with the GData service. * @throws ServiceException creation of update request failed. */ public GDataRequest createPatchRequest(URL entryUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.PATCH, entryUrl, ContentType.APPLICATION_XML); } /** * Deletes an existing entry (and associated media content, if any) using the * specified edit URL. * * @param resourceUrl the edit or medit edit url associated with the resource. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ResourceNotFoundException invalid entry URL. * @throws ServiceException delete request failed due to system error. */ public void delete(URL resourceUrl) throws IOException, ServiceException { delete(resourceUrl, null); } /** * Deletes an existing entry (and associated media content, if any) using the * specified edit URI. * * @param resourceUri the edit or medit edit URI associated with the resource. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ResourceNotFoundException invalid entry URI. * @throws ServiceException delete request failed due to system error. */ public void delete(URI resourceUri) throws IOException, ServiceException { delete(resourceUri.toURL(), null); } /** * Deletes an existing entry (and associated media content, if any) using the * specified edit URL. This delete is conditional upon the provided tag * matching the current entity tag for the entry. If (and only if) they match, * the deletion will be performed. * * @param resourceUrl the edit or medit edit url associated with the resource. * @param etag the entity tag value that is the expected value for the target * resource. A value of {@code null} will not set an etag * precondition and a value of <code>"*"</code> will perform an * unconditional delete. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ResourceNotFoundException invalid entry URL. * @throws ServiceException delete request failed due to system error. */ public void delete(URL resourceUrl, String etag) throws IOException, ServiceException { GDataRequest request = createDeleteRequest(resourceUrl); try { startVersionScope(); request.setEtag(etag); request.execute(); } finally { request.end(); } } /** * Deletes an existing entry (and associated media content, if any) using the * specified edit URI. This delete is conditional upon the provided tag * matching the current entity tag for the entry. If (and only if) they match, * the deletion will be performed. * * @param resourceUri the edit or medit edit URI associated with the resource. * @param etag the entity tag value that is the expected value for the target * resource. A value of {@code null} will not set an etag * precondition and a value of <code>"*"</code> will perform an * unconditional delete. * @throws IOException error communicating with the GData service. * @throws com.google.gdata.util.ResourceNotFoundException invalid entry URI. * @throws ServiceException delete request failed due to system error. */ public void delete(URI resourceUri, String etag) throws IOException, ServiceException { delete(resourceUri.toURL(), etag); } /** * Creates a new GDataRequest that can be used to delete an Atom entry. For * delete requests, no input is expected from the request stream nor will any * response data be returned. * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param entryUrl the edit URL associated with the entry. * @throws IOException error communicating with the GData service. * @throws ServiceException creation of delete request failed. */ public GDataRequest createDeleteRequest(URL entryUrl) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.DELETE, entryUrl, contentType); } /** * Returns an InputStream that contains the content referenced by a link. * * @param link link that references the target resource. * @return input stream that can be used to access the resource content. * @throws IOException error communication with the remote service. * @throws ServiceException resource access failed due to system error. * * @deprecated Use {@link #createLinkQueryRequest(ILink)} instead. */ @Deprecated public InputStream getStreamFromLink(ILink link) throws IOException, ServiceException { GDataRequest request = createLinkQueryRequest(link); request.execute(); InputStream resultStream = request.getResponseStream(); return resultStream; } /** * Returns a query (GET) request that targets the provided link. This * can be used to execute the request and access the link's content via * the response stream of the request (if successful). * <p> * Clients should be sure to call {@link GDataRequest#end()} on the * returned request once they have finished using it. * * @param link link to target resource for created request * @return query request to retrieve linked content. * @throws IOException error communicating with the GData service. * @throws ServiceException creation of query request failed. */ public GDataRequest createLinkQueryRequest(ILink link) throws IOException, ServiceException { return createRequest(GDataRequest.RequestType.QUERY, new URL(link.getHref()), null); } /** * The ClientStreamProperties class is an abstract adaptor class that * implements the {@link StreamProperties} interface for content to be written * to or read from the target service based upon its attributes and a * {@link GDataRequest}. * <p> * Subclasses must implement the {@link StreamProperties#getContentType()} * method since the expected content type depends on the direction of data * transfer for the request. */ protected abstract class ClientStreamProperties implements StreamProperties { protected final GDataRequest req; protected final UriParameterMap queryMap; protected ClientStreamProperties(GDataRequest req) { this.queryMap = computeQueryMap(req); this.req = req; } protected ClientStreamProperties() { this.queryMap = UriParameterMap.EMPTY_MAP; this.req = null; } public GDataRequest getGDataRequest() { return req; } public Version getRequestVersion() { return getProtocolVersion(); } public AltRegistry getAltRegistry() { return Service.this.getAltRegistry(); } public boolean isPartial() { return false; } public ExtensionProfile getExtensionProfile() { return Service.this.getExtensionProfile(); } /** * Returns a {@link MetadataContext} based upon the alt format and version * of the request associated with these properties. This can be used by * subclasses to bind root element metadata for request or response types. */ protected MetadataContext getMetadataContext() { // There's no way to identify the current request projection in // the client. This is acceptable because we can still use the full model // to parse since the client code doesn't validate server responses. // Similarly for data sent by the client, the caller will generally only // populate the fields used in the projection so it's OK too, but if they // did populate extraneous fields they will be sent to the server but they // should be ignored there as well. In both cases, there's an implicit // assumption that a projection is a structurally equivalent subset of the // full representation, which seems reasonable. return MetadataContext.forContext(getAltFormat(), null, getProtocolVersion()); } public Collection<String> getQueryParameterNames() { return queryMap.keySet(); } public String getQueryParameter(String name) { return queryMap.getFirst(name); } /** * Returns the {@link AltFormat} describing the representation used for * the current request. * * @return alternate representation format for current request */ protected AltFormat getAltFormat() { String altName = queryMap.getFirst(GDataProtocol.Parameter.ALT); AltFormat altFormat = getAltRegistry().lookupName(altName); return altFormat != null ? altFormat : AltFormat.ATOM; } /** * Returns the {@link UriParameterMap} containing the decoded query * parameters for the current request. * * @return query parameter map containing decoded query parameters */ protected UriParameterMap getParameterMap() { return queryMap; } } /** * The ClientInputProperties class is an adaptor class that implements the * {@link InputProperties} interface for content to be read from the target * service based upon its attributes and a {@link GDataRequest}. */ protected class ClientInputProperties extends ClientStreamProperties implements InputProperties { private final Class<?> expectType; protected final ContentType inputType; private ElementMetadata<?, ?> elementMetadata; protected ClientInputProperties(GDataRequest req, Class<?> expectType) throws IOException, ServiceException { super(req); this.expectType = expectType; this.inputType = req.getResponseContentType(); init(); } protected ClientInputProperties(ContentType inputType, Class<?> expectType) throws IOException, ServiceException { this.expectType = expectType; this.inputType = inputType; init(); } private void init() { if (Element.class.isAssignableFrom(expectType)) { ElementKey<?, ?> key = Element.getDefaultKey(expectType.asSubclass(Element.class)); elementMetadata = getSchema().bind(key, getMetadataContext()); } else { elementMetadata = null; } } public ContentType getContentType() { return inputType; } public Class<?> getRootType() { return expectType; } public ElementMetadata<?, ?> getRootMetadata() { return elementMetadata; } @Override public boolean isPartial() { return getQueryParameter(GDataProtocol.Query.FIELDS) != null; } } /** * The ClientOutputProperties class is an adaptor class that implements * the {@link OutputProperties} interface for content to be written to the * target service based upon its attributes and a {@link GDataRequest}. */ public class ClientOutputProperties extends ClientStreamProperties implements OutputProperties { protected final ContentType requestType; private ElementMetadata<?, ?> elementMetadata; public ClientOutputProperties(GDataRequest req, Object source) { super(req); this.requestType = req.getRequestContentType(); init(source); } public ClientOutputProperties(ContentType requestType, Object source) { this.requestType = requestType; init(source); } private void init(Object source) { if (source instanceof Element) { Element element = (Element) source; ElementKey<?, ?> key = element.getElementKey(); elementMetadata = getSchema().bind(key, getMetadataContext()); } else { elementMetadata = null; } } public ContentType getContentType() { return requestType; } public ElementMetadata<?, ?> getRootMetadata() { return elementMetadata; } public String getCallback() { return null; } } /** * Writes the request body to the target service based upon the attributes of * the request and the source object. * * @param req currently executing request * @param source source object to be written * @throws IOException */ public void writeRequestData(GDataRequest req, Object source) throws IOException { writeRequestData(req, new ClientOutputProperties(req, source), source); } /** * Writes the request body to the target service based upon requested * output properties and the source object. * * @param outProps client output properties * @param source source object to be written * @throws IOException */ protected void writeRequestData(GDataRequest req, ClientOutputProperties outProps, Object source) throws IOException { AltFormat outputFormat = altRegistry.lookupType(outProps.getContentType()); if (outputFormat == null) { // If no registered type, see if the target service supports media outputFormat = altRegistry.lookupName(AltFormat.MEDIA.getName()); } if (outputFormat == null) { throw new IllegalStateException("Unsupported request type: " + outProps.getContentType()); } OutputGenerator<?> generator = altRegistry.getGenerator(outputFormat); if (!generator.getSourceType().isAssignableFrom(source.getClass())) { throw new IllegalArgumentException("Invalid source type: " + "expected: " + generator.getSourceType() + " but got: " + source.getClass() + " for output format " + outputFormat); } // The cast here is safe because of the runtime check above @SuppressWarnings("unchecked") OutputGenerator<Object> typedGenerator = (OutputGenerator<Object>) generator; // If request type is partial, disable strict validation boolean disableValidation = !strictValidation || outputFormat.equals(AltFormat.APPLICATION_XML); if (disableValidation) { AbstractExtension.disableStrictValidation(); } try { typedGenerator.generate(req.getRequestStream(), outProps, source); } finally { // If request type is partial, renable strict validation if (disableValidation) { AbstractExtension.enableStrictValidation(); } } } /** * Parses the response stream for a request based upon request properties and * an expected result type. The parser will be selected based upon the * request alt type or response content type and used to parse the response * content into the result object. * * @param <E> expected result type * @param req request that has been executed but not yet read from. * @param resultType expected result type, not {@code null}. * @return an instance of the expected result type resulting from the parse. * @throws IOException * @throws ServiceException */ public <E> E parseResponseData(GDataRequest req, Class<E> resultType) throws IOException, ServiceException { InputProperties inputProperties = new ClientInputProperties(req, resultType); return parseResponseData( req.getParseSource(), inputProperties, resultType); } /** * Parses the response stream for a request based upon response content type * and an expected result type. The parser will be selected based upon the * request alt type or response content type and used to parse the response * content into the result object. * * @param <E> expected result type * @param responseType content type of the response to parse. * @param resultType expected result type, not {@code null}. * @return an instance of the expected result type resulting from the parse. * @throws IOException * @throws ServiceException */ protected <E> E parseResponseData( ParseSource source, ContentType responseType, Class<E> resultType) throws IOException, ServiceException { InputProperties inputProperties = new ClientInputProperties(responseType, resultType); return parseResponseData(source, inputProperties, resultType); } private <E> E parseResponseData( ParseSource source, InputProperties inputProperties, Class<E> resultType) throws IOException, ServiceException { Preconditions.checkNotNull("resultType", resultType); AltFormat inputFormat = null; String alt = inputProperties.getQueryParameter(GDataProtocol.Parameter.ALT); if (alt != null) { inputFormat = altRegistry.lookupName(alt); } if (inputFormat == null) { inputFormat = altRegistry.lookupType(inputProperties.getContentType()); if (inputFormat == null) { throw new ParseException("Unrecognized content type:" + inputProperties.getContentType()); } } InputParser<?> inputParser = altRegistry.getParser(inputFormat); if (inputParser == null) { throw new ParseException("No parser for content type:" + inputFormat); } if (!inputParser.getResultType().isAssignableFrom(resultType)) { throw new IllegalStateException("Input parser (" + inputParser + ") does not produce expected result type: " + resultType); } // The cast here is safe because of the runtime check above @SuppressWarnings("unchecked") InputParser<E> typedParser = (InputParser<E>) inputParser; // Disable validation for partial request in old data model. String fields = inputProperties.getQueryParameter(GDataProtocol.Parameter.FIELDS); boolean disableValidation = !strictValidation || (fields != null && !Element.class.isAssignableFrom(resultType)); if (disableValidation) { AbstractExtension.disableStrictValidation(); } E result; try { result = typedParser.parse(source, inputProperties, resultType); } finally { // Re-enable validation. if (disableValidation) { AbstractExtension.enableStrictValidation(); } } // Associate service with the result if atom content if (result instanceof IAtom) { ((IAtom) result).setService(this); } return result; } /** * Computes a {@link UriParameterMap} containing all query parameters passed * in a request. * * @param req request to parse * @return parameter map containing parsed and decoded query parameters */ private static UriParameterMap computeQueryMap(GDataRequest req) { String query = req.getRequestUrl().getQuery(); if (query == null) { return UriParameterMap.EMPTY_MAP; } return UriParameterMap.parse(query); } }