public class CDAArray extends ArrayResource {
  private static final long serialVersionUID = 6596224363025698245L;
  int total;

  int skip;

  int limit;

  Includes includes;

  private List<CDAError> errors;

  CDAMetadata metadata;

  /**
   * @return total number of resources (linked excluded).
   */
  public int total() {
    return total;
  }

  /**
   * @return number of resources to be skipped.
   */
  public int skip() {
    return skip;
  }

  /**
   * @return limit attribute. How many max resources got requested?
   */
  public int limit() {
    return limit;
  }

  /**
   * @return a list of errors if any present
   */
  public List<CDAError> getErrors() {
    return errors;
  }

  public void setErrors(List<CDAError> errors) {
    this.errors = errors;
  }

  static class Includes {
    @SerializedName("Asset") List<CDAAsset> assets;

    @SerializedName("Entry") List<CDAEntry> entries;
  }
} public class CDAAsset extends LocalizedResource {

  private static final long serialVersionUID = -4645571481643616657L;

  /**
   * @return title of this asset.
   */
  public String title() {
    return getField("title");
  }

  /**
   * @return url to the file of this asset.
   */
  public String url() {
    return fileField("url");
  }

  /**
   * Returns a url with the given image manipulation.
   * <p>
   * If the asset is not pointing to an image (as identified by its mimetype) the {@link #url()} is
   * returned. Same happens if the options are empty or non existing, then the url gets returned.
   * <p>
   * In an error case (for instance, using the same option twice), the last option with the same
   * operation will be used.
   *
   * @param options to manipulate the image the returned url will be pointing to.
   * @return an url reflecting all the options given.
   * @throws IllegalArgumentException if no options are given.
   * @throws IllegalArgumentException if no mimetype was set on asset.
   * @throws IllegalArgumentException if mimetype was not an image.
   * @see ImageOption
   * @see #url()
   */
  public String urlForImageWith(ImageOption... options) {
    if (options == null || options.length == 0) {
      throw new IllegalArgumentException("Do not use empty options argument. "
          + "If you want to manipulate the url by hand, please use `CDAAsset.url()` instead.");
    }

    final String mimeType = mimeType();
    if (mimeType == null || !mimeType.startsWith("image")) {
      throw new IllegalStateException("Asset does not have an image mime type.");
    }

    final Map<String, ImageOption> mappedOptions
        = new LinkedHashMap<>(options.length);

    for (final ImageOption option : options) {
      mappedOptions.put(option.getOperation(), option);
    }

    String url = url();
    for (final ImageOption option : mappedOptions.values()) {
      url = option.apply(url);
    }

    return url;
  }

  /**
   * @return mime-type of this asset.
   */
  public String mimeType() {
    return fileField("contentType");
  }

  /**
   * Helper method to extract a field from the {@code file} map.
   *
   * @param key the key who's value to be returned.
   * @param <T> the type of this field.
   * @return field of this file.
   */
  @SuppressWarnings("unchecked")
  public <T> T fileField(String key) {
    T result = null;
    Map<String, Object> file = getField("file");
    if (file != null) {
      result = (T) file.get(key);
    }
    return result;
  }

  /**
   * Return a string, showing the id and title.
   *
   * @return a human readable string
   */
  @Override public String toString() {
    return "CDAAsset{"
        + "id='" + id() + '\''
        + ", title='" + title() + '\''
        + '}';
  }
} public abstract class CDACallback<T> {
  private final Object lock = new Object();

  private boolean cancelled;

  private Disposable disposable;

  /**
   * Successful response.
   *
   * @param result the result of this operation.
   */
  protected abstract void onSuccess(T result);

  /**
   * Invoked when a network or unexpected exception occurred during the HTTP request.
   *
   * @param error the error occurred.
   */
  protected void onFailure(Throwable error) {
    throw new IllegalStateException(error);
  }

  /**
   * @return true in case {@link #cancel()} was called.
   */
  public boolean isCancelled() {
    synchronized (lock) {
      return cancelled;
    }
  }

  /**
   * Cancels the subscription for this callback, onFailure()/onSuccess() methods will not be
   * called.
   */
  public void cancel() {
    synchronized (lock) {
      cancelled = true;
      unsubscribe();
    }
  }

  void setSubscription(Disposable disposable) {
    synchronized (lock) {
      this.disposable = disposable;
    }
  }

  void unsubscribe() {
    synchronized (lock) {
      if (disposable != null) {
        disposable.dispose();
        disposable = null;
      }
    }
  }
} public class CDAClient {
  private static final int CONTENT_TYPE_LIMIT_MAX = 1000;

  final String spaceId;

  final String environmentId;

  final String token;

  final CDAService service;

  final Cache cache;

  final Executor callbackExecutor;

  final boolean preview;

  CDAClient(Builder builder) {
    this(new Cache(),
        Platform.get().callbackExecutor(),
        createService(builder),
        builder);
    validate(builder);
  }

  CDAClient(Cache cache, Executor executor, CDAService service, Builder builder) {
    this.cache = cache;
    this.callbackExecutor = executor;
    this.service = service;
    this.spaceId = builder.space;
    this.environmentId = builder.environment;
    this.token = builder.token;
    this.preview = builder.preview;
  }

  private void validate(Builder builder) {
    checkNotNull(builder.space, "Space ID must be provided.");
    checkNotNull(builder.environment, "Environment ID must not be null.");

    if (builder.callFactory == null) {
      checkNotNull(builder.token, "A token must be provided, if no call factory is specified.");
    }
  }

  private static CDAService createService(Builder clientBuilder) {
    String endpoint = clientBuilder.endpoint;
    if (endpoint == null) {
      endpoint = ENDPOINT_PROD;
    }

    Retrofit.Builder retrofitBuilder = new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create(ResourceFactory.GSON))
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .callFactory(clientBuilder.createOrGetCallFactory(clientBuilder))
        .baseUrl(endpoint);

    return retrofitBuilder.build().create(CDAService.class);
  }

  /**
   * Returns a {@link FetchQuery} for a given {@code type}, which can be used to fulfill the
   * request synchronously or asynchronously when a callback is provided.
   *
   * @param type resource type. This can be either a {@link CDALocale}, a {@link CDAEntry},
   *             a {@link CDAAsset}, or a {@link CDAContentType}
   * @param <T>  type for avoiding casting on calling side.
   * @return A query to call {@link FetchQuery#all()} or {@link FetchQuery#one(String)} on it.
   * @see #fetchSpace()
   */
  public <T extends CDAResource> FetchQuery<T> fetch(Class<T> type) {
    return new FetchQuery<>(type, this);
  }

  /**
   * Returns a {@link TransformQuery} to transform the default contentful response into a specific
   * custom model type.
   * <p>
   *
   * @param <T> An annotated custom {@link TransformQuery.ContentfulEntryModel} model.
   * @return a query for async calls to Contentful transforming the response to custom types
   * @see TransformQuery.ContentfulEntryModel
   * @see TransformQuery.ContentfulField
   * @see TransformQuery.ContentfulSystemField
   */
  public <T> TransformQuery<T> observeAndTransform(Class<T> type) {
    return new TransformQuery<>(type, this);
  }

  /**
   * Returns an {@link ObserveQuery} for a given {@code type}, which can be used to return
   * an {@link Flowable} that fetches the desired resources.
   *
   * @param type resource type. This can be either a {@link CDALocale}, a {@link CDAEntry},
   *             a {@link CDAAsset}, or a {@link CDAContentType}
   * @param <T>  type for avoiding casting on calling side.
   * @return A query to call {@link ObserveQuery#all()} or {@link ObserveQuery#one(String)} on it.
   * @see #observeSpace()
   */
  public <T extends CDAResource> ObserveQuery<T> observe(Class<T> type) {
    return new ObserveQuery<>(type, this);
  }

  /**
   * Populate the content type cache with _all_ available content types.
   * <p>
   * This method will run through all the content types, saving them in the process and also takes
   * care of paging.
   * <p>
   * This method is synchronous.
   *
   * @return the number of content types cached.
   */
  public int populateContentTypeCache() {
    return observeContentTypeCachePopulation().blockingFirst();
  }

  /**
   * Populate the content type cache with _all_ available content types.
   * <p>
   * This method is synchronous.
   *
   * @param limit the number of content types per page.
   * @return the number of content types cached.
   * @throws IllegalArgumentException if limit is less or equal to 0.
   * @throws IllegalArgumentException if limit is more then 1_000.
   * @see #populateContentTypeCache()
   */
  public int populateContentTypeCache(int limit) {
    if (limit > CONTENT_TYPE_LIMIT_MAX) {
      throw new IllegalArgumentException("Content types per page limit cannot be more then 1000.");
    }
    if (limit <= 0) {
      throw new IllegalArgumentException("Content types per page limit cannot be "
          + "less or equal to 0.");
    }

    return observeContentTypeCachePopulation(limit).blockingFirst();
  }

  /**
   * Populate the content type cache with _all_ available content types.
   * <p>
   * This method will run through all the content types, saving them in the process and also takes
   * care of paging.
   * <p>
   * This method is asynchronous and needs to be subscribed to.
   *
   * @return the flowable representing the asynchronous call.
   */
  public Flowable<Integer> observeContentTypeCachePopulation() {
    return observeContentTypeCachePopulation(CONTENT_TYPE_LIMIT_MAX);
  }

  /**
   * Populate the content type cache with _all_ available content types.
   * <p>
   * This method will run through all the content types, saving them in the process and also takes
   * care of paging.
   * <p>
   * This method is asynchronous and needs to be subscribed to.
   *
   * @param limit the number of content types per page.
   * @return the flowable representing the asynchronous call.
   * @throws IllegalArgumentException if limit is less or equal to 0.
   * @throws IllegalArgumentException if limit is more then 1_000.
   */
  public Flowable<Integer> observeContentTypeCachePopulation(final int limit) {
    if (limit > CONTENT_TYPE_LIMIT_MAX) {
      throw new IllegalArgumentException("Content types per page limit cannot be more then 1000.");
    }
    if (limit <= 0) {
      throw new IllegalArgumentException("Content types per page limit cannot be "
          + "less or equal to 0.");
    }

    return
        observe(CDAContentType.class)
            .orderBy("sys.id")
            .limit(limit)
            .all()
            .map(
                new Function<CDAArray, CDAArray>() {
                  @Override
                  public CDAArray apply(CDAArray array) {
                    if (array.skip() + array.limit() < array.total()) {
                      return nextPage(array);
                    } else {
                      return array;
                    }
                  }

                  private CDAArray nextPage(CDAArray array) {
                    final CDAArray nextArray = observe(CDAContentType.class)
                        .orderBy("sys.id")
                        .limit(limit)
                        .skip(array.skip + limit)
                        .all()
                        .map(this)
                        .blockingFirst();

                    array.skip = nextArray.skip;
                    array.items.addAll(nextArray.items);
                    array.assets.putAll(nextArray.assets);
                    array.entries.putAll(nextArray.entries);

                    return array;
                  }
                }
            )
            .map(new Function<CDAArray, Integer>() {
                   @Override
                   public Integer apply(CDAArray array) {
                     for (CDAResource resource : array.items) {
                       if (resource instanceof CDAContentType) {
                         cache.types().put(resource.id(), (CDAContentType) resource);
                       } else {
                         throw new IllegalStateException(
                             "Requesting a list of content types should not return "
                                 + "any other type.");
                       }
                     }
                     return array.total;
                   }
                 }
            );
  }

  /**
   * Returns a {@link SyncQuery} for initial synchronization via the Sync API.
   *
   * @return query instance.
   */

  public SyncQuery sync() {
    return sync(null, null);
  }

  /**
   * Returns a {@link SyncQuery} for synchronization with the provided {@code syncToken} via
   * the Sync API.
   * <p>
   * If called from a {@link #preview} client, this will always do an initial sync.
   *
   * @param type the type to be sync'ed.
   * @return query instance.
   */
  public SyncQuery sync(SyncType type) {
    return sync(null, null, type);
  }

  /**
   * Returns a {@link SyncQuery} for synchronization with the provided {@code syncToken} via
   * the Sync API.
   * <p>
   * If called from a {@link #preview} client, this will always do an initial sync.
   *
   * @param syncToken sync token.
   * @return query instance.
   */
  public SyncQuery sync(String syncToken) {
    return sync(syncToken, null);
  }

  /**
   * Returns a {@link SyncQuery} for synchronization with an existing space.
   * <p>
   * If called from a {@link #preview} client, this will always do an initial sync.
   *
   * @param synchronizedSpace space to sync.
   * @return query instance.
   */
  public SyncQuery sync(SynchronizedSpace synchronizedSpace) {
    return sync(null, synchronizedSpace);
  }

  private SyncQuery sync(String syncToken, SynchronizedSpace synchronizedSpace) {
    return sync(syncToken, synchronizedSpace, null);
  }

  private SyncQuery sync(String syncToken, SynchronizedSpace synchronizedSpace,
                         SyncType type) {
    if (preview) {
      syncToken = null;
      synchronizedSpace = null;
    }

    SyncQuery.Builder builder = SyncQuery.builder().setClient(this);
    if (synchronizedSpace != null) {
      builder.setSpace(synchronizedSpace);
    }
    if (syncToken != null) {
      builder.setSyncToken(syncToken);
    }
    if (type != null) {
      builder.setType(type);
    }
    return builder.build();
  }

  /**
   * @return the space for this client (synchronously).
   */
  public CDASpace fetchSpace() {
    return observeSpace().blockingFirst();
  }

  /**
   * Asynchronously fetch the space.
   *
   * @param <C>      the type of the callback to be used.
   * @param callback the value of the callback to be called back.
   * @return the space for this client (asynchronously).
   */
  @SuppressWarnings("unchecked")
  public <C extends CDACallback<CDASpace>> C fetchSpace(C callback) {
    return (C) Callbacks.subscribeAsync(observeSpace(), callback, this);
  }

  /**
   * @return an {@link Flowable} that fetches the space for this client.
   */
  public Flowable<CDASpace> observeSpace() {
    return service.space(spaceId).map(new Function<Response<CDASpace>, CDASpace>() {
      @Override
      public CDASpace apply(Response<CDASpace> response) throws Exception {
        return ResourceFactory.fromResponse(response);
      }
    });
  }

  /**
   * Caching
   */
  Flowable<Cache> cacheAll(final boolean invalidate) {
    return cacheLocales(invalidate)
        .flatMap(new Function<List<CDALocale>, Publisher<? extends Map<String, CDAContentType>>>() {
          @Override
          public Publisher<? extends Map<String, CDAContentType>> apply(List<CDALocale> locales) {
            return CDAClient.this.cacheTypes(invalidate);
          }
        })
        .map(new Function<Map<String, CDAContentType>, Cache>() {
          @Override
          public Cache apply(Map<String, CDAContentType> stringCDAContentTypeMap) {
            return cache;
          }
        });
  }

  Flowable<List<CDALocale>> cacheLocales(boolean invalidate) {
    List<CDALocale> locales = invalidate ? null : cache.locales();
    if (locales == null) {
      return service.array(spaceId, environmentId, PATH_LOCALES, new HashMap<>())
          .map(new Function<Response<CDAArray>, List<CDALocale>>() {
                 @Override
                 public List<CDALocale> apply(Response<CDAArray> localesResponse) {
                   final List<CDALocale> locales1 = fromArrayToItems(fromResponse(localesResponse));
                   cache.setLocales(locales1);
                   return locales1;
                 }
               }
          );
    }
    return Flowable.just(locales);
  }

  Flowable<Map<String, CDAContentType>> cacheTypes(boolean invalidate) {
    Map<String, CDAContentType> types = invalidate ? null : cache.types();
    if (types == null) {
      return service.array(
          spaceId,
          environmentId,
          PATH_CONTENT_TYPES,
          new HashMap<>()
      ).map(new Function<Response<CDAArray>, Map<String, CDAContentType>>() {
              @Override
              public Map<String, CDAContentType> apply(Response<CDAArray> arrayResponse) {
                CDAArray array = ResourceFactory.array(arrayResponse, CDAClient.this);
                Map<String, CDAContentType> tmp = new ConcurrentHashMap<>();
                for (CDAResource resource : array.items()) {
                  tmp.put(resource.id(), (CDAContentType) resource);
                }
                cache.setTypes(tmp);
                return tmp;
              }
            }
      );
    }
    return Flowable.just(types);
  }

  Flowable<CDAContentType> cacheTypeWithId(String id) {
    CDAContentType contentType = cache.types().get(id);
    if (contentType == null) {
      return observe(CDAContentType.class)
          .one(id)
          .map(new Function<CDAContentType, CDAContentType>() {
                 @Override
                 public CDAContentType apply(CDAContentType resource) {
                   if (resource != null) {
                     cache.types().put(resource.id(), resource);
                   }
                   return resource;
                 }
               }
          );
    }
    return Flowable.just(contentType);
  }

  /**
   * Clear the java internal cache.
   *
   * @return this client for chaining.
   */
  public CDAClient clearCache() {
    cache.clear();
    return this;
  }

  static String createUserAgent() {
    final Properties properties = System.getProperties();
    return String.format("contentful.java/%s(%s %s) %s/%s",
        PROJECT_VERSION,
        properties.getProperty("java.runtime.name"),
        properties.getProperty("java.runtime.version"),
        properties.getProperty("os.name"),
        properties.getProperty("os.version")
    );
  }

  static Section[] createCustomHeaderSections(Section application, Section integration) {
    final Properties properties = System.getProperties();

    final Platform platform = Platform.get();
    return new Section[]{
        sdk(
            "contentful.java",
            Version.parse(PROJECT_VERSION)),
        platform(
            "java",
            Version.parse(properties.getProperty("java.runtime.version"))
        ),
        os(
            OperatingSystem.parse(platform.name()),
            Version.parse(platform.version())
        ),
        application,
        integration
    };
  }

  /**
   * @return a {@link CDAClient} builder.
   */

  public static Builder builder() {
    return new Builder();
  }

  /**
   * This builder will be used to configure and then create a {@link CDAClient}.
   */
  public static class Builder {
    String space;
    String environment = Constants.DEFAULT_ENVIRONMENT;
    String token;
    String endpoint;

    Logger logger;
    Logger.Level logLevel = Logger.Level.NONE;

    Call.Factory callFactory;

    boolean preview;
    Tls12Implementation tls12Implementation = useRecommendation;

    Section application;
    Section integration;

    private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient();

    Builder() {
    }

    /**
     * Sets the space ID.
     *
     * @param space the space id to be set.
     * @return this builder for chaining.
     */
    public Builder setSpace(String space) {
      this.space = space;
      return this;
    }

    /**
     * Sets the environment ID.
     *
     * @param environment the space id to be set.
     * @return this builder for chaining.
     */
    public Builder setEnvironment(String environment) {
      this.environment = environment;
      return this;
    }

    /**
     * Sets the space access token.
     *
     * @param token the access token, sometimes called authorization token.
     * @return this builder for chaining.
     */
    public Builder setToken(String token) {
      this.token = token;
      return this;
    }

    /**
     * Sets a custom endpoint.
     *
     * @param endpoint the url to be calling to (i.e. https://cdn.contentful.com).
     * @return this builder for chaining.
     */
    public Builder setEndpoint(String endpoint) {
      this.endpoint = endpoint;
      return this;
    }

    /**
     * Sets a custom logger level.
     * <p>
     * If set to {@link Logger.Level}.NONE any custom logger will get ignored.
     *
     * @param logLevel the amount/level of logging to be used.
     * @return this builder for chaining.
     */
    public Builder setLogLevel(Logger.Level logLevel) {
      this.logLevel = logLevel;
      return this;
    }

    /**
     * Sets a custom logger.
     *
     * @param logger the logger to be set.
     * @return this builder for chaining.
     */
    public Builder setLogger(Logger logger) {
      this.logger = logger;
      return this;
    }

    /**
     * Sets the endpoint to point the Preview API.
     *
     * @return this builder for chaining.
     */
    public Builder preview() {
      preview = true;
      return this.setEndpoint(Constants.ENDPOINT_PREVIEW);
    }

    /**
     * Sets a custom HTTP call factory.
     *
     * @param callFactory the factory to be used to create a call.
     * @return this builder for chaining.
     */
    public Builder setCallFactory(Call.Factory callFactory) {
      this.callFactory = callFactory;
      return this;
    }

    Call.Factory createOrGetCallFactory(Builder clientBuilder) {
      final Call.Factory callFactory;

      if (clientBuilder.callFactory == null) {
        callFactory = defaultCallFactoryBuilder().build();
      } else {
        callFactory = clientBuilder.callFactory;
      }

      return callFactory;
    }

    private OkHttpClient.Builder setLogger(OkHttpClient.Builder okBuilder) {
      if (logger != null) {
        switch (logLevel) {
          case BASIC:
            return okBuilder.addInterceptor(new LogInterceptor(logger));
          case FULL:
            return okBuilder.addNetworkInterceptor(new LogInterceptor(logger));
          case NONE:
            break;
          default:
            break;
        }
      } else {
        if (logLevel != Logger.Level.NONE) {
          throw new IllegalArgumentException(
              "Cannot log to a null logger. Please set either logLevel to None, or do set a Logger"
          );
        }
      }
      return okBuilder;
    }

    private OkHttpClient.Builder useTls12IfWanted(OkHttpClient.Builder okBuilder) {
      if (isSdkTlsSocketFactoryWanted()) {
        try {
          okBuilder.sslSocketFactory(new TlsSocketFactory(), getX509TrustManager());
        } catch (GeneralSecurityException exception) {
          throw new IllegalArgumentException(
              "Cannot create TlsSocketFactory for TLS 1.2. "
                  + "Please consider using 'setTls12Implementation(systemProvided)', "
                  + "or update to a system providing TLS 1.2 support.",
              exception);
        }
      }

      return okBuilder;
    }

    X509TrustManager getX509TrustManager() throws NoSuchAlgorithmException, KeyStoreException {
      final TrustManagerFactory trustManagerFactory =
          TrustManagerFactory.getInstance(getDefaultAlgorithm());
      trustManagerFactory.init((KeyStore) null);

      return extractX509TrustManager(trustManagerFactory.getTrustManagers());
    }

    X509TrustManager extractX509TrustManager(TrustManager[] trustManagers)
        throws NoSuchAlgorithmException {
      if (trustManagers != null) {
        for (final TrustManager manager : trustManagers) {
          if (manager instanceof X509TrustManager) {
            return (X509TrustManager) manager;
          }
        }
      }

      throw new NoSuchAlgorithmException(
          "Cannot find a 'X509TrustManager' in system provided managers: '"
              + Arrays.toString(trustManagers) + "'.");
    }

    boolean isSdkTlsSocketFactoryWanted() {
      switch (tls12Implementation) {
        case sdkProvided:
          return true;
        case systemProvided:
          return false;
        default:
        case useRecommendation:
          return Platform.get().needsCustomTLSSocketFactory();
      }
    }

    /**
     * Returns the default Call.Factory.Builder used throughout this SDK.
     * <p>
     * Please use this method last in the building step, since changing settings as in the
     * {@link #token} or others afterwards will not be reflected by this factory.
     * <p>
     * This might be useful if you want to augment the default client, without needing to rely on
     * replicating the current sdk behaviour.
     *
     * @return A {@link Call.Factory} used through out SDK, as if no custom call factory was used.
     */
    public OkHttpClient.Builder defaultCallFactoryBuilder() {
      final Section[] sections = createCustomHeaderSections(application, integration);

      OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
          .connectionPool(OK_HTTP_CLIENT.connectionPool())
          .addInterceptor(new AuthorizationHeaderInterceptor(token))
          .addInterceptor(new UserAgentHeaderInterceptor(createUserAgent()))
          .addInterceptor(new ContentfulUserAgentHeaderInterceptor(sections))
          .addInterceptor(new ErrorInterceptor());

      setLogger(okBuilder);
      useTls12IfWanted(okBuilder);

      return okBuilder;
    }

    /**
     * Overwrite the recommendation from the SDK for using a custom TLS12 socket factory.
     * <p>
     * This SDK recommends a TLS12 socket factory to be used: Either the system one, or an SDK owned
     * implementation. If this recommendation does not fit your needs, feel free to overwrite the
     * recommendation here.
     * <p>
     * Some operation systems and frameworks, esp. Android and Java 1.6, might opt for implementing
     * TLS12 (enforced by Contentful) but do not enable it. The SDK tries to find those situations
     * and recommends to either use the system TLSSocketFactory or a SDK provided one.
     *
     * @param implementation which implementation to be used.
     * @return this builder for ease of chaining.
     */
    public Builder setTls12Implementation(Tls12Implementation implementation) {
      this.tls12Implementation = implementation;
      return this;
    }

    /**
     * Tell the client which application this is.
     * <p>
     * It might be used for internal tracking of Contentfuls tools.
     *
     * @param name    the name of the app.
     * @param version the version in semver of the app.
     * @return this builder for chaining.
     */
    public Builder setApplication(String name, String version) {
      this.application = Section.app(name, Version.parse(version));
      return this;
    }

    /**
     * Set the name of the integration.
     * <p>
     * This custom user agent header will be used for libraries build on top of this library.
     *
     * @param name    of the integration.
     * @param version version of the integration.
     * @return this builder for chaining.
     */
    public Builder setIntegration(String name, String version) {
      this.integration = Section.integration(name, Version.parse(version));
      return this;
    }

    /**
     * Create CDAClient, using the specified configuration options.
     *
     * @return a build CDAClient.
     */
    public CDAClient build() {
      return new CDAClient(this);
    }
  }
} public class CDAContentType extends CDAResource {
  private static final long serialVersionUID = 7901798878659781364L;
  List<CDAField> fields;

  String name;

  String displayField;

  String description;

  /**
   * @return list of fields.
   */
  public List<CDAField> fields() {
    return fields;
  }

  /**
   * @return name of this content type.
   */
  public String name() {
    return name;
  }

  /**
   * @return field to be used for displaying.
   */
  public String displayField() {
    return displayField;
  }

  /**
   * @return description of this content type.
   */
  public String description() {
    return description;
  }

  /**
   * Convert this object into a human readable string.
   *
   * @return a string, containing id, name and description of this type.
   */
  @Override public String toString() {
    return "CDAContentType{"
        + "id='" + id() + '\''
        + ", name='" + name + '\''
        + ", description='" + description + '\''
        + '}';
  }
} public class CDAEntry extends LocalizedResource {
  private static final long serialVersionUID = 5902790363045498307L;
  private CDAContentType contentType;

  /**
   * @return the contentType set.
   */
  public CDAContentType contentType() {
    return contentType;
  }

  /**
   * Set the contentType of this entry.
   * @param contentType the type to be set.
   */
  void setContentType(CDAContentType contentType) {
    this.contentType = contentType;
  }

  /**
   * Create a human readable string of this object.
   * @return a string, containing the id of this content type.
   */
  @Override public String toString() {
    return "CDAEntry{"
        + "id='" + id() + '\''
        + '}';
  }
} public class CDAHttpException extends RuntimeException {
  private static final long serialVersionUID = 637581021148308658L;
  private final Request request;
  private final Response response;
  private final String responseBody;
  private final String stringRepresentation;

  /**
   * Construct an error response.
   * <p>
   * This constructor will fill the exception with easy accessible values, like
   * {@link #responseCode()}. {@link #responseMessage()}, but also
   * {@link #rateLimitReset()}.
   *
   * @param request  the request issuing the error.
   * @param response the response from the server to this faulty request.
   */
  public CDAHttpException(Request request, Response response) {
    super(response.message());
    this.request = request;
    this.response = response;
    this.responseBody = readResponseBody(response);
    this.stringRepresentation = createString();
  }

  private String readResponseBody(Response response) {
    try {
      BufferedSource bufferedSource = response.body().source();
      Timeout timeout = bufferedSource.timeout();
      timeout.deadline(1, TimeUnit.SECONDS);
      return response.body().string();
    } catch (IOException ioException) {
      return "<io exception while parsing body: " + ioException.toString() + ">";
    }
  }

  private String createString() {
    return format(
        Locale.getDefault(),
        "FAILED REQUEST:\n\t%s\n\t╰→ Header{%s}\n\t%s\n\t├→ Body{%s}\n\t╰→ Header{%s}",
        request.toString(),
        headersToString(request.headers()),
        response.toString(),
        responseBody,
        headersToString(response.headers()));
  }

  /**
   * Convert exception to human readable form.
   *
   * @return a string representing this exception.
   */
  @Override
  public String toString() {
    return stringRepresentation;
  }

  /**
   * @return the response code of the request.
   */
  public int responseCode() {
    return response.code();
  }

  /**
   * @return the message the server returned.
   */
  public String responseMessage() {
    return response.message();
  }

  /**
   * @return the errors body, potentially containing more information.
   */
  public String responseBody() {
    return responseBody;
  }

  /**
   * @return the hourly rate limit or -1 if header not send
   */
  public int rateLimitHourLimit() {
    return parseRateLimitHeader("X-Contentful-RateLimit-Hour-Limit");
  }

//BEGIN TO LONG CODE LINES
  /**
   * @return the number of remaining requests that can be made for the current hour or -1 if header not send
   */
//END TO LONG CODE LINES
  public int rateLimitHourRemaining() {
    return parseRateLimitHeader("X-Contentful-RateLimit-Hour-Remaining");
  }

  /**
   * @return the per second rate limit or -1 if header not send
   */
  public int rateLimitSecondLimit() {
    return parseRateLimitHeader("X-Contentful-RateLimit-Second-Limit");
  }

  /**
   * @return the number of remaining requests that can be made per second or -1 if header not send
   */
  public int rateLimitSecondRemaining() {
    return parseRateLimitHeader("X-Contentful-RateLimit-Second-Remaining");
  }

//BEGIN TO LONG CODE LINES
  /**
   * @return the number of seconds until the user can make their next request or -1 if header not send
   */
//END TO LONG CODE LINES
  public int rateLimitReset() {
    return parseRateLimitHeader("X-Contentful-RateLimit-Reset");
  }

  private String headersToString(Headers headers) {
    final StringBuilder builder = new StringBuilder();

    String divider = "";
    for (final String name : headers.names()) {
      final String value = headers.get(name);
      builder.append(divider);
      builder.append(name);
      builder.append(": ");
      builder.append(value);

      if ("".equals(divider)) {
        divider = ", ";
      }
    }

    return builder.toString();
  }

  private int parseRateLimitHeader(String name) {
    try {
      return Integer.parseInt(response.header(name));
    } catch (NumberFormatException e) {
      return -1;
    }
  }
} public class CDALocale extends CDAResource {
  private static final long serialVersionUID = -5710267672379169621L;
  String code;

  String name;

  @SerializedName("fallbackCode")
  String fallbackLocaleCode;

  @SerializedName("default")
  boolean defaultLocale;

  /**
   * @return code of this locale. ('en-US' or similar).
   */
  public String code() {
    return code;
  }

  /**
   * @return human readable name of this locale.
   */
  public String name() {
    return name;
  }

  /**
   * @return the code of a locale to be used for falling back.
   */
  public String fallbackLocaleCode() {
    return fallbackLocaleCode;
  }

  /**
   * @return true if this is the default locale.
   */
  public boolean isDefaultLocale() {
    return defaultLocale;
  }

  /**
   * @return a human readable string, representing the object.
   */
  @Override public String toString() {
    return "CDALocale { " + super.toString() + " "
        + "code = " + code() + ", "
        + "defaultLocale = " + isDefaultLocale() + ", "
        + "fallbackLocaleCode = " + fallbackLocaleCode() + ", "
        + "name = " + name() + " "
        + "}";
  }
} public abstract class CDAResource implements Serializable {
  private static final long serialVersionUID = -160701290783423915L;
  @SerializedName("sys")
  Map<String, Object> attrs;

  /**
   * @return a string representing this object's id.
   */
  public String id() {
    return getAttribute("id");
  }

  /**
   * @return the type of this resource.
   */
  public CDAType type() {
    String type = getAttribute("type");
    return CDAType.valueOf(type.toUpperCase(LOCALE));
  }

  /**
   * @return all of the attributes, this object holds.
   */
  public Map<String, Object> attrs() {
    return attrs;
  }

  /**
   * Retrieve a specific attribute of this resource.
   *
   * @param key a string key associated with the value to be retrieved.
   * @param <T> the type of the attribute to be retrieved.
   * @return the actual value of the attribute, or null, if there the key was not found.
   */
  @SuppressWarnings("unchecked")
  public <T> T getAttribute(String key) {
    return (T) attrs.get(key);
  }


  /**
   * @return a human readable string, representing the object.
   */
  @Override public String toString() {
    return "CDAResource { "
        + "attrs = " + attrs() + ", "
        + "id = " + id() + ", "
        + "type = " + type() + " "
        + "}";
  }
} public class CDAResourceNotFoundException extends RuntimeException {

  private static final long serialVersionUID = -7419778969492055048L;

  /**
   * Create a new exception
   *
   * @param resourceType the type of the resource not found (Entry,Asset,Space)
   * @param resourceId   the actual id of the resource not found.
   */
  public CDAResourceNotFoundException(
      Class<? extends CDAResource> resourceType,
      String resourceId) {
    super(String.format(
        Locale.getDefault(), "Could not find id '%s' of type '%s'.",
        resourceId,
        resourceType.getSimpleName())
    );
  }
} public class CDASpace extends CDAResource {
  private static final long serialVersionUID = 8920494351623297673L;
  String name;

  /**
   * @return name of this space.
   */
  public String name() {
    return name;
  }

  /**
   * Create a String from this object.
   *
   * @return a String containing the id and name of this space
   */
  @Override public String toString() {
    return "CDASpace{"
        + "id='" + id() + '\''
        + ", name='" + name + '\''
        + '}';
  }
} public abstract class LocalizedResource extends CDAResource {
  private static final long serialVersionUID = 5713028146014748949L;

  public class Localizer {
    private final String locale;

    Localizer(String locale) {
      this.locale = locale;
    }

    /**
     * Extracts a field from the fields set of the active locale, result type is inferred.
     *
     * @param key field key.
     * @param <T> type.
     * @return field value, null if it doesn't exist.
     */
    @SuppressWarnings("unchecked")
    public <T> T getField(String key) {
      final Map<String, T> value = (Map<String, T>) fields.get(key);
      if (value == null) {
        return null;
      }

      return getFieldForFallbackLocale(value, locale);
    }

    <T> T getFieldForFallbackLocale(Map<String, T> value, String locale) {
      if (locale == null) {
        return null;
      }

      final T localized = value.get(locale);
      if (localized != null) {
        return localized;
      } else {
        return getFieldForFallbackLocale(value, fallbackLocaleMap.get(locale));
      }
    }
  }

  String defaultLocale;

  Map<String, String> fallbackLocaleMap;

  Map<String, Object> fields;

  Map<String, Object> rawFields;

  /**
   * Creates an object to be used for returning different field in one locale.
   *
   * @param locale pointing to a locale in the environment.
   * @return localizer to localize the fields.
   */
  public Localizer localize(String locale) {
    return new Localizer(locale);
  }

  /**
   * Get a field using the environments default locale.
   *
   * @param key field key.
   * @param <T> type.
   * @return field value, null if it doesn't exist.
   * @see #localize(String)
   */
  public <T> T getField(String key) {
    return localize(defaultLocale).getField(key);
  }

  /**
   * Extracts a field from the fields set of the active locale, result type is inferred.
   *
   * @param locale locale to be used.
   * @param key field key.
   * @param <T> type.
   * @return field value, null if it doesn't exist.
   */
  public <T> T getField(String locale, String key) {
    return localize(locale).getField(key);
  }

  /**
   * Internal method for updating contents of a field.
   * <p>
   * This method is used by the SDK to generate objects based on raw fields.
   *
   * <b>Do not use this field to update data on Contentful. Take a look at the CMA-SDK for that.</b>
   *
   * @param locale locale to be updated.
   * @param key    the key of the field to be updated.
   * @param value  the value of the field to be used.
   */
  @SuppressWarnings("unchecked")
  public void setField(String locale, String key, Object value) {
    ((Map<String, Object>) fields.get(key)).put(locale, value);
  }

  /**
   * @return raw unprocessed fields.
   */
  public Map<String, Object> rawFields() {
    return rawFields;
  }
} public static class BoundingBox {
  private final Location bottomLeft;
  private final Location topRight;

  /**
   * Construct a bounding box with type save arguments.
   *
   * @param bottomLeft bottom left corner of bounding box.
   * @param topRight   top right corner of bounding box.
   */
  public BoundingBox(Location bottomLeft, Location topRight) {
    this.bottomLeft = bottomLeft;
    this.topRight = topRight;
  }

  /**
   * Simple constructor of bounding box taking double parameters instead of locations
   *
   * @param bottom bottom most location of the box.
   * @param left   left most location of the box.
   * @param top    top most location of the box.
   * @param right  right most location of the box.
   */
  public BoundingBox(double bottom, double left, double top, double right) {
    this.bottomLeft = new Location(bottom, left);
    this.topRight = new Location(top, right);
  }

  /**
   * @return returns a url friendly representation.
   */
  @Override public String toString() {
    return String.format("%s,%s", bottomLeft.toString(), topRight.toString());
  }
} public static class BoundingCircle {
  private final Location center;
  private final double radius;

  /**
   * Type safe constructor of bounding circle.
   *
   * @param center position of circle on the globe in geo location.
   * @param radius the radius of the circle in kilometer.
   */
  public BoundingCircle(Location center, double radius) {
    this.center = center;
    this.radius = radius;
  }

  /**
   * Simple constructor of bounding circle.
   *
   * @param centerLatitude  center coordinate on latitude axis.
   * @param centerLongitude center coordinate of circle, on longitude axis.
   * @param radius          in km.
   */
  public BoundingCircle(double centerLatitude, double centerLongitude, double radius) {
    this.center = new Location(centerLatitude, centerLongitude);
    this.radius = radius;
  }

  /**
   * @return returns a url friendly representation.
   */
  @Override public String toString() {
    return String.format("%s,%f", center.toString(), radius);
  }
} public static class Location {
  private final double latitude;
  private final double longitude;

  /**
   * Create a location based on its coodinates.
   *
   * @param latitude  of this location.
   * @param longitude of this location.
   */
  public Location(double latitude, double longitude) {
    this.latitude = latitude;
    this.longitude = longitude;
  }

  /**
   * @return returns a url friendly representation.
   */
  @Override
  public String toString() {
    return String.format("%f,%f", latitude, longitude);
  }
} public class SynchronizedSpace extends ArrayResource {
  private static final long serialVersionUID = 8618757744312417604L;
  String nextPageUrl;

  String nextSyncUrl;

  Set<String> deletedAssets;

  Set<String> deletedEntries;

  /**
   * @return url to the next sync.
   */
  public String nextSyncUrl() {
    return nextSyncUrl;
  }

  /**
   * @return url of next page, containing more data.
   */
  String nextPageUrl() {
    return nextPageUrl;
  }

  /**
   * @return deleted asset ids.
   */
  public Set<String> deletedAssets() {
    return deletedAssets;
  }

  /**
   * @return deleted entry ids.
   */
  public Set<String> deletedEntries() {
    return deletedEntries;
  }
} public static final QueryOperation<Boolean> Exists = new QueryOperation<>("[exists]", true); public static final QueryOperation<String> HasAllOf = new QueryOperation<>("[all]"); public static final QueryOperation<String> HasNoneOf = new QueryOperation<>("[nin]"); public static final QueryOperation<String> HasOneOf = new QueryOperation<>("[in]"); public static final QueryOperation<Location> IsCloseTo = new QueryOperation<>("[near]"); public static final QueryOperation<String> IsEarlierOrAt = new QueryOperation<>("[lte]"); public static final QueryOperation<String> IsEarlierThan = new QueryOperation<>("[lt]"); public static final QueryOperation<String> IsEqualTo = new QueryOperation<>(""); public static final QueryOperation<String> IsLaterOrAt = new QueryOperation<>("[gte]"); public static final QueryOperation<Integer> IsLessThanOrEqualTo
    = new QueryOperation<>("[lte]"); public static final QueryOperation<String> IsNotEqualTo = new QueryOperation<>("[ne]"); public static final QueryOperation<BoundingBox> IsWithinBoundingBoxOf
    = new QueryOperation<>("[within]"); public static final QueryOperation<BoundingCircle> IsWithinCircleOf
    = new QueryOperation<>("[within]"); public static final QueryOperation<String> Matches = new QueryOperation<>("[match]"); public static SyncType onlyDeletedAssets() {
  return new SyncType(DeletedAsset, null);
} public static SyncType onlyEntriesOfType(String type) {
  return new SyncType(Entry, type);
}
import com.contentful.java.cda.CDAArray;
import com.contentful.java.cda.CDAAsset;
import com.contentful.java.cda.CDACallback;
import com.contentful.java.cda.CDAClient;
import com.contentful.java.cda.CDAContentType;
import com.contentful.java.cda.CDAEntry;
import com.contentful.java.cda.CDAHttpException;
import com.contentful.java.cda.CDALocale;
import com.contentful.java.cda.CDAResource;
import com.contentful.java.cda.CDAResourceNotFoundException;
import com.contentful.java.cda.CDASpace;
import com.contentful.java.cda.LocalizedResource;
import com.contentful.java.cda.QueryOperation.BoundingBox;
import com.contentful.java.cda.QueryOperation.BoundingCircle;
import com.contentful.java.cda.QueryOperation.Location;
import com.contentful.java.cda.SynchronizedSpace;
import com.contentful.java.cda.TransformQuery.ContentfulEntryModel;
import com.contentful.java.cda.TransformQuery.ContentfulField;
import com.contentful.java.cda.TransformQuery.ContentfulSystemField;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static com.contentful.java.cda.CDAType.SPACE;
import static com.contentful.java.cda.QueryOperation.Exists;
import static com.contentful.java.cda.QueryOperation.HasAllOf;
import static com.contentful.java.cda.QueryOperation.HasNoneOf;
import static com.contentful.java.cda.QueryOperation.HasOneOf;
import static com.contentful.java.cda.QueryOperation.IsCloseTo;
import static com.contentful.java.cda.QueryOperation.IsEarlierOrAt;
import static com.contentful.java.cda.QueryOperation.IsEarlierThan;
import static com.contentful.java.cda.QueryOperation.IsEqualTo;
import static com.contentful.java.cda.QueryOperation.IsLaterOrAt;
import static com.contentful.java.cda.QueryOperation.IsLessThanOrEqualTo;
import static com.contentful.java.cda.QueryOperation.IsNotEqualTo;
import static com.contentful.java.cda.QueryOperation.IsWithinBoundingBoxOf;
import static com.contentful.java.cda.QueryOperation.IsWithinCircleOf;
import static com.contentful.java.cda.QueryOperation.Matches;
import static com.contentful.java.cda.SyncType.onlyDeletedAssets;
import static com.contentful.java.cda.SyncType.onlyEntriesOfType;
import static com.google.common.truth.Truth.assertThat;
    assertThat(found.items().size()).isEqualTo(1);
    CDAEntry entry = (CDAEntry) found.items().get(0);
    assertThat(entry.<String>getField("name")).isEqualTo("Berlin");
  }

  //"/spaces/{space_id}/entries?skip={value}",
  @Test
  public void fetchWithSkip() {
    CDAArray found = client.fetch(CDAEntry.class)
        .skip(1)
        .orderBy("sys.id")
        .all();

    assertThat(found.items().size()).isEqualTo(9);
    CDAEntry entry = (CDAEntry) found.items().get(0);
    assertThat(entry.<String>getField("name")).isEqualTo("London");
  }

  //"/spaces/{space_id}/entries?include={value}",
  @Test
  public void fetchWithoutIncluding() {
    CDAArray found = client.fetch(CDAEntry.class)
        .include(0)
        .all();

    assertThat(found.items().size()).isEqualTo(10);
    assertThat(found.assets().size()).isEqualTo(0);
  }

  //"/spaces/{space_id}/entries?{attribute}%5Bin%5D={value}",
  @Test
  public void fetchWithInQuery() {
    CDAArray found = client.fetch(CDAEntry.class)
        .where("sys.id", HasOneOf, "finn", "jake")
        .orderBy("sys.id")
        .all();

    assertThat(found.items().size()).isEqualTo(2);
    CDAEntry finn = (CDAEntry) found.items().get(0);
    assertThat(finn.<String>getField("name")).isEqualTo("Finn");

    CDAEntry jake = (CDAEntry) found.items().get(1);
    assertThat(jake.<String>getField("name")).isEqualTo("Jake");
  }

  //"/spaces/{space_id}/entries?{attribute}%5Ball%5D={value}",
  @Test
  public void fetchWithAllQuery() {
    CDAArray found = client.fetch(CDAEntry.class)
        .withContentType("cat")
        .where("fields.likes", HasAllOf, "rainbows", "fish")
        .all();

    assertThat(found.items().size()).isEqualTo(1);
    CDAEntry finn = (CDAEntry) found.items().get(0);
    assertThat(finn.<String>getField("name")).isEqualTo("Nyan Cat");
  }

  //"/spaces/{space_id}/entries?content_type={content_type}&{attribute}%5Bnin%5D={value}",
  @Test
  public void fetchWithNotInQuery() {
    CDAArray found = client.fetch(CDAEntry.class)
        .withContentType("cat")
        .where("sys.id", HasNoneOf, "nyancat")
        .all();

    assertThat(found.items().size()).isEqualTo(2);
  }

  //"/spaces/{space_id}/entries?{attribute}%5Bexists%5D={value}",
  @Test
  public void fetchWithExistsQuery() {
    CDAArray found = client.fetch(CDAEntry.class)
        .where("sys.id", Exists, false) // entries without id
        .all();

    assertThat(found.items().size()).isEqualTo(0);
  }

  //"/spaces/{space_id}/entries?content_type={content_type}&{attribute}%5Bexists%5D={value}",
  @Test
  public void fetchWithExistsQueryOnFields() {
    CDAArray found = client.fetch(CDAEntry.class)
        .withContentType("cat")
        .where("fields.bestFriend", Exists, false) // entries without id
        .all();

    assertThat(found.items()).hasSize(1);
    assertThat(found.entries().get("garfield").<String>getField("name")).isEqualTo("Garfield");
  }

  //"/spaces/{space_id}/entries?content_type={content_type}&{attribute}%5Blte%5D={value}",
  @Test
  public void fetchEntriesInRange() {
    CDAArray found = client.fetch(CDAEntry.class)
        .withContentType("cat")
        .where("fields.birthday", IsEarlierThan, "1980-01-01")
        .all();

    assertThat(found.items().size()).isEqualTo(1);
    CDAEntry nyancat = (CDAEntry) found.items().get(0);
    assertThat(nyancat.<String>getField("name")).isEqualTo("Garfield");
  }

  //"/spaces/{space_id}/entries?{attribute}%5Blte%5D={value}",
  @Test
  public void fetchEntriesVersionLTE100() {
    CDAArray found = client.fetch(CDAEntry.class)
        .where("sys.revision", IsLessThanOrEqualTo, 0)
        .all();

    assertThat(found.total()).isEqualTo(0);
    assertThat(found.items()).hasSize(0);
  }

  //"/spaces/{space_id}/entries?content_type={content_type}&{attribute}%5Blte%5D={value}",
  @Test
  public void fetchEntriesEarlierOrAt() {
    CDAArray found = client.fetch(CDAEntry.class)
        .withContentType("cat")
        .where("fields.birthday", IsEarlierOrAt, "1979-06-18T23:00:00")