/**
 * Extends the basic {@link GoogleService} abstraction to define a service that is preconfigured for
 * access to the Google Spreadsheets data API.
 */
public class SpreadsheetService extends GoogleService {

  /**
   * The abbreviated name of Google Spreadsheets recognized by Google. The service name is used when
   * requesting an authentication token.
   */
  public static final String SPREADSHEET_SERVICE = "wise";

  /** The version ID of the service. */
  public static final String SPREADSHEET_SERVICE_VERSION =
      "GSpread-Java/" + SpreadsheetService.class.getPackage().getImplementationVersion();

  /** GData versions supported by Google Spreadsheets Service. */
  public static final class Versions {

    /**
     * Version 1 of the Spreadsheet Google Data API. This is the initial version of the API and is
     * based upon Version 1 of the GData Protocol.
     */
    public static final Version V1 =
        new Version(SpreadsheetService.class, "1.0", Service.Versions.V1);

    /**
     * Version 2 of the Spreadsheet Google Data API. This version of the API adds full compliance
     * with the Atom Publishing Protocol and is based upon Version 2 of the GData protocol.
     */
    public static final Version V2 =
        new Version(SpreadsheetService.class, "2.0", Service.Versions.V2);

    /**
     * Version 3 of the Spreadsheet Google Data API. This adds a new Table Feed and deprecates the
     * old feeds.
     */
    public static final Version V3 =
        new Version(SpreadsheetService.class, "3.0", Service.Versions.V2);

    private Versions() {}
  }

  /** Default GData version used by the Google Spreadsheets service. */
  public static final Version DEFAULT_VERSION =
      Service.initServiceVersion(SpreadsheetService.class, Versions.V3);

  /**
   * Constructs an instance connecting to the Google Spreadsheets service for an application with
   * the name {@code applicationName}.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   */
  public SpreadsheetService(String applicationName) {
    super(SPREADSHEET_SERVICE, applicationName);
    declareExtensions();
  }

  /**
   * Constructs an instance connecting to the Google Spreadsheets service for an application with
   * the name {@code applicationName} and the given {@code GDataRequestFactory} and {@code
   * AuthTokenFactory}. Use this constructor to override the default factories.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   * @param requestFactory the request factory that generates gdata request objects
   * @param authTokenFactory the factory that creates auth tokens
   */
  public SpreadsheetService(
      String applicationName,
      Service.GDataRequestFactory requestFactory,
      AuthTokenFactory authTokenFactory) {
    super(applicationName, requestFactory, authTokenFactory);
    declareExtensions();
  }

  /**
   * Constructs an instance connecting to the Google Spreadsheets service with name {@code
   * serviceName} for an application with the name {@code applicationName}. The service will
   * authenticate at the provided {@code domainName}.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   * @param protocol name of protocol to use for authentication ("http"/"https")
   * @param domainName the name of the domain hosting the login handler
   */
  public SpreadsheetService(String applicationName, String protocol, String domainName) {
    super(SPREADSHEET_SERVICE, applicationName, protocol, domainName);
    declareExtensions();
  }

  @Override
  public String getServiceVersion() {
    return SPREADSHEET_SERVICE_VERSION + " " + super.getServiceVersion();
  }

  /** Returns the current GData version used by the Google Spreadsheets service. */
  public static Version getVersion() {
    return VersionRegistry.get().getVersion(SpreadsheetService.class);
  }

  /** Declare the extensions of the feeds for the Google Spreadsheets service. */
  private void declareExtensions() {
    new CellFeed().declareExtensions(extProfile);
    new ListFeed().declareExtensions(extProfile);
    new RecordFeed().declareExtensions(extProfile);
    new SpreadsheetFeed().declareExtensions(extProfile);
    new TableFeed().declareExtensions(extProfile);
    new WorksheetFeed().declareExtensions(extProfile);
  }
}
/**
 * Creates a Google data service with extensions specific to Google Base.
 *
 * <p>The methods returning Atom feeds and entries in this class default to returning instances of
 * {@link GoogleBaseFeed} and {@link GoogleBaseEntry}.
 *
 * <h4>Usage example
 *
 * <h4>
 *
 * <pre>
 * GoogleBaseService service = new GoogleBaseService("mycompany-myapp-1.0");
 * service.setUserCredentials(username, password);
 * GoogleBaseQuery query = new GoogleBaseQuery(...);
 * query.setGoogleBaseQuery(...);
 * GoogleBaseFeed feed = service.query(query);
 * </pre>
 */
public class GoogleBaseService extends MediaService {

  /** The official name of the service. */
  public static final String GOOGLE_BASE_SERVICE = "gbase";

  /**
   * Version of this service.
   *
   * @see #getServiceVersion()
   */
  public static final String GOOGLE_BASE_SERVICE_VERSION =
      "GBase-Java/" + GoogleBaseService.class.getPackage().getImplementationVersion();

  /** The Versions class contains all released versions of the Google Base API */
  public static class Versions {

    /**
     * Version 1 of the Google Base API. This is the initial version of the API and is based upon
     * Version 1 of the GData Protocol.
     */
    public static final Version V1 =
        new Version(GoogleBaseService.class, "1.0", Service.Versions.V1);

    /**
     * Version 2 of the Google Base API. This version of the API adds full compliance with the Atom
     * Publishing Protocol and is based upon Version 2 of the GData protocol.
     */
    public static final Version V2 =
        new Version(GoogleBaseService.class, "2.0", Service.Versions.V2);
  }

  /** Version 1 is the current default version for GoogleBaseService. */
  public static final Version DEFAULT_VERSION =
      Service.initServiceVersion(GoogleBaseService.class, GoogleBaseService.Versions.V2);

  /** Name of the application passed to the constructor. */
  protected String application;

  /**
   * Creates a GoogleBaseService connecting to the default authentication domain (www.google.com)
   * using the HTTPS.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   */
  public GoogleBaseService(String applicationName) {
    super(GOOGLE_BASE_SERVICE, applicationName);
    this.application = applicationName;
    addExtensions();
  }

  /**
   * Creates a GoogleBaseService connecting to the default authentication domain (www.google.com)
   * using the HTTPS.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   * @param developerKey developer key (ignored)
   */
  public GoogleBaseService(String applicationName, String developerKey) {
    this(applicationName);
  }

  /**
   * Creates a GoogleBaseService connecting to a specific authentication domain using a specific
   * protocol.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   * @param protocol name of protocol to use for authentication ("http"/"https")
   * @param domainName the name of the domain hosting the login handler
   */
  public GoogleBaseService(String applicationName, String protocol, String domainName) {
    super(GOOGLE_BASE_SERVICE, applicationName, protocol, domainName);
    this.application = applicationName;
    addExtensions();
  }

  /**
   * Creates a GoogleBaseService connecting to a specific authentication domain using a specific
   * protocol.
   *
   * @param applicationName the name of the client application accessing the service. Application
   *     names should preferably have the format [company-id]-[app-name]-[app-version]. The name
   *     will be used by the Google servers to monitor the source of authentication.
   * @param developerKey developer key (ignored)
   * @param protocol name of protocol to use for authentication ("http"/"https")
   * @param domainName the name of the domain hosting the login handler
   */
  public GoogleBaseService(
      String applicationName, String developerKey, String protocol, String domainName) {
    this(applicationName, protocol, domainName);
  }

  /**
   * Returns the Google Base 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 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.
   * @see #getFeed(java.net.URL, Class, com.google.gdata.data.DateTime)
   */
  public GoogleBaseFeed getFeed(URL feedUrl, DateTime ifModifiedSince)
      throws IOException, ServiceException {
    return getFeed(feedUrl, GoogleBaseFeed.class, ifModifiedSince);
  }

  /**
   * Returns the Google base feed associated with a particular feed URL.
   *
   * @param feedUrl the URL associated with a feed. This URL can include GData query parameters.
   * @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.
   * @see #getFeed(java.net.URL, Class)
   */
  public GoogleBaseFeed getFeed(URL feedUrl) throws IOException, ServiceException {
    return getFeed(feedUrl, GoogleBaseFeed.class);
  }

  /**
   * Returns the Google base media feed associated with the specified feed URL. For the call to be
   * valid, the user must be authenticated and own the item. The Google Base item has the link to
   * its media feed registered as a {@link com.google.gdata.data.extensions.FeedLink} extension with
   * the {@code media} relation.
   *
   * @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.
   * @see #getFeed(java.net.URL, Class, com.google.gdata.data.DateTime)
   */
  public GoogleBaseMediaFeed getMediaFeed(URL feedUrl) throws IOException, ServiceException {
    return getFeed(feedUrl, GoogleBaseMediaFeed.class);
  }

  /**
   * Returns the Google Base media feed associated with a particular feed URL, if it's been modified
   * since the specified date.
   *
   * @param feedUrl the URL associated with a media feed for one particular item.
   * @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.
   * @see #getFeed(java.net.URL, Class, com.google.gdata.data.DateTime)
   */
  public GoogleBaseMediaFeed getMediaFeed(URL feedUrl, DateTime ifModifiedSince)
      throws IOException, ServiceException {
    return getFeed(feedUrl, GoogleBaseMediaFeed.class, ifModifiedSince);
  }

  /**
   * Returns an Google Base entry instance, given the URL of the entry and an if-modified-since
   * date.
   *
   * @param entryUrl resource URL for the entry.
   * @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.
   * @see #getEntry(java.net.URL, Class, DateTime)
   */
  public GoogleBaseEntry getEntry(URL entryUrl, DateTime ifModifiedSince)
      throws IOException, ServiceException {
    return getEntry(entryUrl, GoogleBaseEntry.class, ifModifiedSince);
  }

  /**
   * Returns an Google Base entry instance, given the URL of the entry.
   *
   * @param entryUrl resource URL for the entry.
   * @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.
   * @see #getEntry(java.net.URL, Class)
   */
  public GoogleBaseEntry getEntry(URL entryUrl) throws IOException, ServiceException {
    return getEntry(entryUrl, GoogleBaseEntry.class);
  }

  /**
   * Returns the media entry referenced by the entryUrl. If the URL is not pointing to a media
   * entry, an exception will be thrown.
   *
   * @param entryUrl the url to the media entry.
   * @return a media entry object representing the meta-data for the referenced media object (item
   *     image or attachment).
   * @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 GoogleBaseMediaEntry getMediaEntry(URL entryUrl) throws IOException, ServiceException {
    return getEntry(entryUrl, GoogleBaseMediaEntry.class);
  }

  /**
   * Returns the media entry referenced by the entryUrl, if it's been modified since the specified
   * date.
   *
   * @param entryUrl the url to the media entry.
   * @return a media entry object representing the meta-data for the referenced media object (item
   *     image or attachment).
   * @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 GoogleBaseMediaEntry getMediaEntry(URL entryUrl, DateTime ifModifiedSince)
      throws IOException, ServiceException {
    return getEntry(entryUrl, GoogleBaseMediaEntry.class, ifModifiedSince);
  }

  /**
   * Executes a GData query against the target service and returns the {@link GoogleBaseFeed}
   * 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, usually a {@link
   *     GoogleBaseQuery}
   * @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.
   * @see #query(com.google.gdata.client.Query, Class, DateTime)
   */
  public GoogleBaseFeed query(Query query, DateTime ifModifiedSince)
      throws IOException, ServiceException {
    return query(query, GoogleBaseFeed.class, ifModifiedSince);
  }

  /**
   * Executes a GData query against the target service and returns the {@link
   * com.google.gdata.data.Feed} containing entries that match the query result.
   *
   * @param query Query instance defining target feed and query parameters, usually a {@link
   *     GoogleBaseQuery}
   * @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 GoogleBaseFeed query(Query query) throws IOException, ServiceException {
    return query(query, GoogleBaseFeed.class);
  }

  @Override
  public <E extends IEntry> E update(URL url, E e) throws IOException, ServiceException {
    addApplicationAttribute(e);
    return super.update(url, e);
  }

  @Override
  public <E extends IEntry> E insert(URL url, E e) throws IOException, ServiceException {
    addApplicationAttribute(e);
    return super.insert(url, e);
  }

  @Override
  public <F extends IFeed> F batch(URL url, F f)
      throws IOException, ServiceException, BatchInterruptedException {
    addApplicationAttribute(f);
    return super.batch(url, f);
  }

  /**
   * Sets the application attribute using the name passed to the constructor.
   *
   * @param e
   */
  private void addApplicationAttribute(IEntry iEntry) {
    if (!(iEntry instanceof BaseEntry)) {
      throw new IllegalArgumentException("Unexpected entry type: " + iEntry.getClass());
    }
    BaseEntry<?> e = (BaseEntry<?>) iEntry;

    GoogleBaseAttributesExtension attrs = e.getExtension(GoogleBaseAttributesExtension.class);
    if (attrs == null) {
      return;
    }
    attrs.setApplication(application);
  }

  /**
   * Sets the application attribute using the name passed to the constructor on a batch feed.
   *
   * @param batchFeed
   */
  @SuppressWarnings("unchecked")
  private void addApplicationAttribute(IFeed iFeed) {
    if (!(iFeed instanceof BaseFeed)) {
      throw new IllegalArgumentException("Unexpected feed type: " + iFeed);
    }
    BaseFeed<?, ?> batchFeed = (BaseFeed<?, ?>) iFeed;
    BatchOperationType defaultType = BatchUtils.getBatchOperationType(batchFeed);
    if (defaultType == null) {
      defaultType = BatchOperationType.INSERT;
    }
    List<? extends BaseEntry> entries = batchFeed.getEntries();
    for (BaseEntry<?> entry : entries) {
      BatchOperationType type = BatchUtils.getBatchOperationType(entry);
      if (type == null) {
        type = defaultType;
      }
      if (type == BatchOperationType.INSERT || type == BatchOperationType.UPDATE) {
        addApplicationAttribute(entry);
      }
    }
  }

  /** Adds Google Base extensions (g:namespaces) to a Google data service. */
  private void addExtensions() {
    ExtensionProfile extensionProfile = getExtensionProfile();
    GoogleBaseNamespaces.declareAllExtensions(extensionProfile);
    BatchUtils.declareExtensions(extensionProfile);
  }

  /**
   * Returns the google base service version followed by the gdata service version.
   *
   * @return both versions, separated by a space
   * @see #GOOGLE_BASE_SERVICE_VERSION
   */
  @Override
  public String getServiceVersion() {
    return GOOGLE_BASE_SERVICE_VERSION + " " + super.getServiceVersion();
  }
}