package com.contentful.java.cda; //BEGIN TO LONG CODE LINES import com.contentful.java.cda.interceptor.AuthorizationHeaderInterceptor; import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor; import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section; import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.OperatingSystem; import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.Version; import com.contentful.java.cda.interceptor.ErrorInterceptor; import com.contentful.java.cda.interceptor.LogInterceptor; import com.contentful.java.cda.interceptor.UserAgentHeaderInterceptor; import io.reactivex.Flowable; import io.reactivex.functions.Function; import okhttp3.Call; import okhttp3.OkHttpClient; import org.reactivestreams.Publisher; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import static com.contentful.java.cda.Constants.ENDPOINT_PROD; import static com.contentful.java.cda.Constants.PATH_CONTENT_TYPES; import static com.contentful.java.cda.Constants.PATH_LOCALES; import static com.contentful.java.cda.ResourceFactory.fromArrayToItems; import static com.contentful.java.cda.ResourceFactory.fromResponse; import static com.contentful.java.cda.Tls12Implementation.useRecommendation; import static com.contentful.java.cda.Util.checkNotNull; import static com.contentful.java.cda.build.GeneratedBuildParameters.PROJECT_VERSION; import static com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.os; import static com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.platform; import static com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.sdk; import static javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm; //END TO LONG CODE LINES /** * Client to be used when requesting information from the Delivery API. Every client is associated * with exactly one Space, but there is no limit to the concurrent number of clients existing at * any one time. Avoid creating multiple clients for the same Space. Use {@link #builder()} * to create a new client instance. */ 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; 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() .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); } } }